Android之图像处理

Android之路—中章

Android中用到很多图片, 今日记录一下图片的一些处理, 一些优化.

Bitmap

Bitmap extends Object implements Parcelable

java.lang.Object

   ↳ android.graphics.Bitmap

Bitmap是图像处理中最重要的类之一, 其中可以使用这个类对图片进行一些颜色变换, 剪切, 旋转和缩放等操作.并可以指定保存图像文件.

内部枚举类

Bitmap 内部有两个枚举类: Config (配置) 和 CompressFormat (压缩格式)

枚举项 解释
ALPHA_8 (1) 颜色信息只由透明度组成,占8位。
RGB_565 (3) 颜色信息由R(Red),G(Green),B(Blue)三部分组成,R占5位,G占6位,B占5位,总共占16位。
ARGB_8888 (5) 颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占8位,总共占32位。
ARGB_4444 颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占4位,总共占16位。
RGBA_F16 (6) 每个像素存储在8个字节上。每个通道(用于半透明的RGB和alpha)都存储为一个半精度浮点值。
HARDWARE (7) 特殊配置,当位图仅存储在图形内存中时。

配置ARGB_8888是Bitmap默认的颜色配置信息,也是最占空间的一种配置.

配置RGBA_F16特别适用于宽色域和HDR (高动态范围图像), 运用下面的代码打包成64位 :

1
long color = (A & 0xffff) << 48 | (B & 0xffff) << 32 | (G & 0xffff) << 16 | (R & 0xffff);

可以调用 Bitmap 中的方法,设置配置:

1
public void setConfig(Config config)
枚举项 解释
JPEG (0) 以JPEG压缩算法进行图像压缩,压缩后的格式可以是”.jpg”或者”.jpeg”,是一种有损压缩
PNG (1) 以PNG压缩算法进行图像压缩,压缩后的格式可以是”.png”,是一种无损压缩
WEBP (2) 以WebP压缩算法进行图像压缩,压缩后的格式可以是”.webp”,是一种有损压缩

质量相同的情况下,WebP格式图像的体积要比JPEG格式图像小40%,但是,WebP格式图像的编码时间比JPEG格式图像长8倍.

常用方法

创建位图,它有多个重载方法.

1
2
3
4
5
//从源位图的(x,y)坐标截取width x height的位图 ①
public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height)

//在上面方法基础上,添加变化矩阵, 是否过滤源位图 ②
public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height,Matrix m, boolean filter)

其实 createBitmap 还有很多重载,但基本都是上述方法②的调用,如下

1
2
3
4
5
6
7
8
public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height) {
return createBitmap(source, x, y, width, height, null, false);
}//①

//指定源位图的不可变位图
public static Bitmap createBitmap(Bitmap src) {
return createBitmap(src, 0, 0, src.getWidth(), src.getHeight());
}//③

方法①返回的是方法②产生的参数, 方法③返回的是方法①,实际还是方法②, 就是设置变化 MatrixNULL , 不过滤源图.

在方法②中 boolean filter 参数为true*时表示 *source 会被过滤,仅仅当变换矩阵 Matrix m 操作不仅包含移动操作,还包含别的操作时才适用.

复制位图

1
public Bitmap copy(Config config, boolean isMutable)

根据当前位图尺寸复制出新的位图. 将传入的配置参数 Config config 作为新位图的配置, 此时的 boolean isMutable 参数应为 true , 此时返回的位图是和原始位图一样的密度和颜色空间 ,如果不支持转换,即 boolean isMutablefalse ,此时该方法返回NULL.

修改位图方法

1
public void reconfigure(int width, int height, Config config)

修改位图的为传入参数的宽,高和配置, 不影响位图的底层分配, 并且不重新创建新的位图.

对应的 get() 和 set() 方法

1
2
3
4
5
6
7
public final int getWidth()	//返回位图宽
public final int getHeight() //返回位图高
public final Config getConfig() //返回位图配置

public void setWidth(int width) //设置位图的宽
public void setHeight(int height) //设置位图的高
public void setConfig(Config config) //设置位图配置

其中 set() 方法内部其实是调用了 reconfigure(int width, int height, Config config) 方法, 只是参数互相用 get() 方法代替.

BitmapFactory

Bitmap创建有很多方法, 但是Bitmap的加载离不开BitmapFactory类

java.lang.Object

   ↳ android.graphics.BitmapFactory

BitmapFactory同样是Android中图像处理的一个重要的类, 可以取其意译理解为一个加工Bitmap的工厂.

BitmapFactory类可以加载来自不同来源的文件,创建为位图对象.

BitmapFactory的Option参数

参数方法 说明
public boolean inJustDecodeBounds 如果设置为true,不获取图片,不分配内存,但会返回图片的高度宽度信息
public int inSampleSize 图片缩放的倍数
public int outWidth 获取图片的宽度值
public int outHeight 获取图片的高度值
public int inDensity 用于位图的像素压缩比
public int inTargetDensity 用于目标位图的像素压缩比
public Bitmap.Config inPreferredConfig 设置解码器, 即设置 Bitmap 的 Config
public int inScreenDensity 当前屏幕的像素密度

BitmapFactory加载Bitmap

BitmapFactory加载位图的五种基本方法

  • 从文件读取图片

    1
    public static Bitmap decodeFile(String pathName, Options opts)
  • 从文件高效率读取图片

    1
    public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)
  • 从输入流读取图片

    1
    public static Bitmap decodeStream(InputStream is,Rect outPadding,Options opts)
  • 从资源文件读取图片

    1
    public static Bitmap decodeResource(Resources res, int id, Options opts)
  • 从数组读取图片

    1
    public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts)

将这些方法封装成完整的方法

  • 从本地读取文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    public static Bitmap getBitmapbyFileDescriptor(String filePath, int width, int height) {
    try {
    /*---------①---------*/
    FileInputStream fis = new FileInputStream(filePath);
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options);
    /*---------②---------*/
    float srcWidth = options.outWidth;
    float srcHeight = options.outHeight;
    int inSampleSize = 1;
    if (srcHeight > height || srcWidth > width) {
    if (srcWidth > srcHeight) {
    inSampleSize = Math.round(srcHeight / height);//四舍五入
    } else {
    inSampleSize = Math.round(srcWidth / width);
    }
    }
    /*---------③---------*/
    options.inJustDecodeBounds = false;
    options.inSampleSize = inSampleSize;
    return BitmapFactory.decodeFileDescriptor(fis.getFD(), null, options);
    } catch (Exception ex) {
    }
    return null;
    }

    传入参数:文件路径, 像素的宽, 像素的高, 例如在app手机存储路径中有一张 img.jpg 的图片 :

    1
    2
    String filePath = getExternalCacheDir().toString() + "/img.jpg";
    Bitmap bitmap1 = getBitmapbyFileDescriptor(filePath,1024,1024);
  • 从输入流读取文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public static Bitmap getBitmapbyInputStream(InputStream ins, int width, int height) {
    /*---------①---------*/
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeStream(ins, null, options);
    /*---------②---------*/
    float srcWidth = options.outWidth;
    float srcHeight = options.outHeight;
    int inSampleSize = 1;
    if (srcHeight > height || srcWidth > width) {
    if (srcWidth > srcHeight) {
    inSampleSize = Math.round(srcHeight / height);
    } else {
    inSampleSize = Math.round(srcWidth / width);
    }
    }
    /*---------③---------*/
    options.inJustDecodeBounds = false;
    options.inSampleSize = inSampleSize;
    return BitmapFactory.decodeStream(ins, null, options);
    }

    传入参数:输入流字符串, 像素的宽, 像素的高, 例如在工程资源 drawable 文件中有一张 ic_launcher.png 图片 :

    1
    2
    3
    String assPath = "/res/drawable/ic_launcher.png";
    InputStream ins = getClass().getResourceAsStream(assPath);
    Bitmap bitmap2 = getBitmapbyInputStream(ins,1024,1024);
  • Resource资源加载

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public static Bitmap getBitmapbyResource(Resources resources, int resourcesId, int width, int height) {
    /*---------①---------*/
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(resources, resourcesId, options);
    /*---------②---------*/
    float srcWidth = options.outWidth;
    float srcHeight = options.outHeight;
    int inSampleSize = 1;
    if (srcHeight > height || srcWidth > width) {
    if (srcWidth > srcHeight) {
    inSampleSize = Math.round(srcHeight / height);
    } else {
    inSampleSize = Math.round(srcWidth / width);
    }
    }
    /*---------③---------*/
    options.inJustDecodeBounds = false;
    options.inSampleSize = inSampleSize;
    return BitmapFactory.decodeResource(resources, resourcesId, options);
    }

    传入参数:当前资源文件, 图片id , 像素的宽, 像素的高, 例如在工程资源drawable文件中有一张 ic_launcher.png 图片 :

    1
    Bitmap bitmap3 = getBitmapbyResource(getResources(),R.drawable.ic_launcher,1024,1024);
  • Assets资源加载

    当调用资源文件夹assets中的图片时, 需要上下文 context 创建一个 AssetManager 对象, 来获取资源图片, 这是一个提供低级别的访问应用资源的Api.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public Bitmap getBitmapbyAssetsFile(Context context, String filePath) {
    Bitmap image = null;
    AssetManager am = context.getResources().getAssets();
    try {
    InputStream is = am.open(filePath);
    image = BitmapFactory.decodeStream(is);
    is.close();
    } catch (IOException e) {
    e.printStackTrace();
    }
    return image;
    }

    传入参数:上下文 context , 资源文件名字符串 例如在工程资源 assets 文件中有一张 ic_launcher.png 图片 :

    1
    2
    String assPath = "ic_launcher.png";
    Bitmap bitmap4 = getBitmapbyAssetsFile(this,assPath);
  • 从二进制数据读取图片

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    public static Bitmap getBitmapbyByteArray(byte[] data, int width, int height) {
    /*---------①---------*/
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeByteArray(data, 0, data.length, options);
    /*---------②---------*/
    float srcWidth = options.outWidth;
    float srcHeight = options.outHeight;
    int inSampleSize = 1;
    if (srcHeight > height || srcWidth > width) {
    if (srcWidth > srcHeight) {
    inSampleSize = Math.round(srcHeight / height);
    } else {
    inSampleSize = Math.round(srcWidth / width);
    }
    }
    /*---------③---------*/
    options.inJustDecodeBounds = false;
    options.inSampleSize = inSampleSize;
    return BitmapFactory.decodeByteArray(data, 0, data.length, options);
    }

    从二进制数组读取时,很容易就返回NULL, 因为inJustDecodeBounds设置为true,不获取图片,不分配内存,只会返回图片的高度宽度信息, 所以要设置为false后才调用加载方式.

以上方法都可以看做以下面循序进行

①创建对象,设置 inJustDecodeBoundstrue , 只获取其长宽信息

②将传入的像素大小与位图大小比较,获取纵横比

③设置 inJustDecodeBoundsfalse , 获取输入流的图片,调用相应的BitmapFactory方法返回对应纵横比的位图.

位图对象之间的转换

  1. Drawable 和 Bitmap

    创建bitmap时, 用Drawable的方式创建位图对象,能够有很多好处,简单看下表:

对比项 清晰度 占用内存 缩放 色差调整 旋转 透明色 绘制速度 像素操作
Bitmap 相同 支持 支持 支持 支持 支持
Drawable 相同 支持 不支持 支持 支持 不支持
1
2
BitmapDrawable bpd = (BitmapDrawable) getResources().getDrawable(R.drawable.xxx);
Bitmap bitmap = bitmapDrawable.getBitmap();

BitmapDrawable里面封装了一个图片对象,调用它的 getBitmap() 方法, 转为为一个Bitmap对象.

  1. Bitmap 和 Byte[]

    1
    2
    3
    4
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    bitmap.compress(Bitmap.CompressFormat.PNG, 100, baos);
    byte[] imgbytes = baos.toByteArray();
    baos.close();

创建二进制数组输出对象, 调用bitmap的压缩方法, 设置压缩方式, 转换为二进制数组.

Bitmap的内存优化

在Android系统中,读取位图Bitmap时,分给虚拟机中的图片的堆栈大小只有8M,如果超出了,就会出现 OOM (OutOfMemory) 异常。所以,对于图片的内存优化,是Android应用开发中比较重要的内容.

  • 及时回收Bitmap

    在Android2.3.3之前推荐使用Bitmap.recycle()方法进行Bitmap的内存回收

    1
    2
    3
    4
    5
    if (!bmp.isRecycle()) {
    bmp.recycle(); //回收图片所占的内存
    bitmap = null;
    system.gc(); //提醒系统及时回收
    }

    虽然调用recycle()并不能保证立即释放占用的内存,但是可以加速Bitmap的内存的释放。
    释放内存以后,就不能再使用该Bitmap对象了,如果再次使用,就会抛出异常。所以一定要保证不再使用的时候释放。可以在Activity的onStop()或者onDestroy()方法中进行回收。

    系统会在内存不足的情况下杀死一些低优先级的进程,以提供给其它进程充足的内存空间。在实际项目开发过程中,作为开发者,尽量会在退出程序的时候使用Process.killProcess(Process.myPid())的方式将自己的进程杀死,而不是仅仅使用Activity.finish()方法的方式关闭掉所有的Activity.

  • 捕获异常

    因为Bitmap非常耗内存,了避免应用在分配Bitmap内存的时候出现OutOfMemory异常以后Crash掉,在实例化Bitmap的代码中,一定要对OutOfMemory异常进行捕获,例如上面加载Bitmap的方法中那样.

  • 缓存通用的Bitmap对象

    使用LruCache对Bitmap进行缓存,当再次使用到这个Bitmap的时候直接获取,而不用重走编码流程。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    private LruCache<String, Bitmap> mMemoryCache;

    //获取LruCache的缓存大小,计算容量
    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    int cacheSize = maxMemory / 8;
    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
    @Override
    protected int sizeOf(String key, Bitmap bitmap) {
    return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
    }
    };

    //LruCache的存
    private void putBitmapToMemoryCache(String key, Bitmap bitmap) {
    if (getBitmapFromMemCache(key) == null) {
    mMemoryCache.put(key, bitmap);
    }
    }
    //LruCache的取
    private Bitmap getBitmapFromMemCache(String key) {
    return mMemoryCache.get(key);
    }