Android Bitmap
Bitmap是Android系统中的图像处理中最重要类之一。Bitmap可以获取图像文件信息,对图像进行剪切、旋转、缩放,压缩等操作,并可以以指定格式保存图像文件。
在Android开发中,我们经常与Bitmap打交道,而对Bitmap的不恰当的操作经常会导致OOM(Out of Memory)。
1. Bitmap
Android中的Bitmap对象是对位图的抽象,它可以从文件系统、资源文件夹、网络等各种不同的来源获取。位图可以看做是像素点的集合,本质上就是通过一系列二进制位来描述一张图片,具有不同色彩格式的位图使用不同数量的二进制位来描述一个像素点,因而图片质量和图片大小也就不同。
2. Bitmap的颜色配置信息与压缩方式信息
Bitmap中有两个内部枚举类:Config和CompressFormat:
- Config是用来设置颜色配置信息的
- CompressFormat是用来设置压缩方式的。
2.1 Config
名称 | 解释 |
---|---|
Bitmap.Config.ALPHA_8 | 颜色信息只由透明度组成,占8位。 |
Bitmap.Config.ARGB_4444 | 颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占4位,总共占16位。 |
Bitmap.Config.ARGB_8888 | 颜色信息由透明度与R(Red),G(Green),B(Blue)四部分组成,每个部分都占8位,总共占32位。是Bitmap默认的颜色配置信息,也是最占空间的一种配置。 |
Bitmap.Config.RGB_565 | 颜色信息由R(Red),G(Green),B(Blue)三部分组成,R占5位,G占6位,B占5位,总共占16位。 |
通常我们优化Bitmap时,当需要做性能优化或者防止OOM(Out Of Memory),我们通常会使用Bitmap.Config.RGB_565这个配置,因为Bitmap.Config.ALPHA_8只有透明度,显示一般图片没有意义,Bitmap.Config.ARGB_4444显示图片不清楚,Bitmap.Config.ARGB_8888占用内存最多。
2.2 CompressFormat
名称 | 解释 |
---|---|
Bitmap.CompressFormat.JPEG | 表示以JPEG压缩算法进行图像压缩,压缩后的格式可以是".jpg"或者".jpeg",是一种有损压缩。 |
Bitmap.CompressFormat.PNG | 表示以PNG压缩算法进行图像压缩,压缩后的格式可以是".png",是一种无损压缩。 |
Bitmap.CompressFormat.WEBP | 表示以WebP压缩算法进行图像压缩,压缩后的格式可以是".webp",是一种有损压缩,质量相同的情况下,WebP格式图像的体积要比JPEG格式图像小40%。美中不足的是,WebP格式图像的编码时间“比JPEG格式图像长8倍”。 |
3. Bitmap 究竟占多大内存
此处参阅Android 开发绕不过的坑:你的 Bitmap 究竟占多大内存?
Bitmap 在内存当中占用的大小其实取决于:
- 色彩格式,前面我们已经提到,如果是 ARGB8888 那么就是一个像素4个字节,如果是 RGB565 那就是2个字节
- 原始文件存放的资源目录(是 hdpi 还是 xxhdpi 可不能傻傻分不清楚哈)
- 目标屏幕的密度
- 最终输出的 outputBitmap 的大小是scaledWidth*scaledHeight
一张522*686的 PNG 图片,我把它放到 drawable-xxhdpi 目录下,在三星s6上加载,占用内存2547360B。
if (willScale && decodeMode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
在我们的例子中,
scaledWidth = int( 522 * 640 / 480f + 0.5) = int(696.5) = 696
scaledHeight = int( 686 * 640 / 480f + 0.5) = int(915.16666…) = 915
915 * 696 * 4 = 2547360
3. Bitmap的优化策略
3.1 Recycle
从上面的注解中我们可以看到,当确认了这个Bitmap实例在后续的操作中不再需要后可以调用recycle方法,该方法是不可逆的。
在Android2.3时代,Bitmap的引用是放在堆中的,而Bitmap的数据部分是放在栈中的,需要用户调用recycle方法手动进行内存回收,而在Android2.3之后,整个Bitmap,包括数据和引用,都放在了堆中,这样,整个Bitmap的回收就全部交给GC了,这个recycle方法就再也不需要使用了。
3.2 LruCache
A cache that holds strong references to a limited number of values. Each time a value is accessed, it is moved to the head of a queue. When a value is added to a full cache, the value at the end of that queue is evicted and may become eligible for garbage collection.
LRU 的工作原理,最近使用的会放进队列的头部,最久未使用的放进队列的尾部,会首先删除队尾元素。
如果你 cache 的某个值需要明确释放,重写 entryRemoved 方法;如果 key 相对应的 item 丢掉,重写create(),这简化了调用代码,即使丢失了也总会返回。
- 该类是线程安全的
- 该类不允许空值和空 key
Sample of using LruCache:
Set<SoftReference<Bitmap>> mReusableBitmaps;
private LruCache<String, BitmapDrawable> mMemoryCache;
// If you're running on Honeycomb or newer, create a
// synchronized HashSet of references to reusable bitmaps.
if (Utils.hasHoneycomb()) {
mReusableBitmaps =
Collections.synchronizedSet(new HashSet<SoftReference<Bitmap>>());
}
mMemoryCache = new LruCache<String, BitmapDrawable>(mCacheParams.memCacheSize) {
// Notify the removed entry that is no longer being cached.
@Override
protected void entryRemoved(boolean evicted, String key,
BitmapDrawable oldValue, BitmapDrawable newValue) {
if (RecyclingBitmapDrawable.class.isInstance(oldValue)) {
// The removed entry is a recycling drawable, so notify it
// that it has been removed from the memory cache.
((RecyclingBitmapDrawable) oldValue).setIsCached(false);
} else {
// The removed entry is a standard BitmapDrawable.
if (Utils.hasHoneycomb()) {
// We're running on Honeycomb or later, so add the bitmap
// to a SoftReference set for possible use with inBitmap later.
mReusableBitmaps.add
(new SoftReference<Bitmap>(oldValue.getBitmap()));
}
}
}
....
}
3.3 inSampleSize
其实有些类似缩小多少倍的感觉,不过变化是这样的:例如inSampleSize==4,那么长、宽都变为原来的四分之一,而像素数则变为原来的十六分之一,毕竟像素=长*宽
这里例举一个选择图像后通过inSampleSize修改图像大小的操作,在onActivityResult中进行的:
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == SELECT_PIC_RESULT_CODE && resultCode == RESULT_OK && data != null) {
Uri uri = data.getData();//获取图像地址
// 在解码的时候避免内存的分配,它会返回一个null的Bitmap
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
try {
InputStream inputStream = getContentResolver().openInputStream(uri);
BitmapFactory.decodeStream(inputStream, null, options);
// 获取到原始图片的尺寸
int height = options.outHeight;
int width = options.outWidth;
int sampleSize = 1;
int max = Math.max(height, width);
if (max > maxSize) {
int nw = width / 2;
int nh = height / 2;
while ((nw / sampleSize) > maxSize || (nh / sampleSize) > maxSize) {
sampleSize *= 2;
}
}
options.inSampleSize = sampleSize;
options.inJustDecodeBounds = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
mBitmap = BitmapFactory.decodeStream(getContentResolver().
openInputStream(uri), null, options);
mIvProcess.setImageBitmap(mBitmap);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
}
在程序中,调整图片的大小需要通过设置Options的inJustDecodeBounds属性为true,将图片的width和height属性读出来,我们可以利用这些属性对Bitmap进行压缩,同时通过Options.inSampleSize属性设置压缩比。
3.4 三级缓存
现在的Android应用程序中,不可避免的都会使用到图片,如果每次加载图片的时候都要从网络重新拉取,这样不但很耗费用户的流量,而且图片加载的也会很慢,用户体验很不好。所以一个应用的图片缓存策略是很重要的。通常情况下,Android应用程序中图片的缓存策略采用“内存-本地-网络”三级缓存策略,首先应用程序访问网络拉取图片,分别将加载的图片保存在本地SD卡中和内存中,当程序再一次需要加载图片的时候,先判断内存中是否有缓存,有则直接从内存中拉取,否则查看本地SD卡中是否有缓存,SD卡中如果存在缓存,则图片从SD卡中拉取,否则从网络加载图片。依据这三级缓存机制,可以让我们的应用程序在加载图片的时候做到游刃有余,有效的避免内存溢出。