Android Bitmap深入介绍(二)--- 优化技术

这一篇主要介绍Bitmap相关的一些优化技术,包括加载图片,图片内存管理,图片缓存。

加载图片

图片缩放

我们在加载图片的时候,经常会遇到OOM的问题,也许我们测试的时候图片比较小,但是实际上使用的图片可能

会很大,我最好的方式就是在加载的时候就把图片缩小。Options提供了inJustDecodeBounds来先获取图片的大小,

如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeResource(getResources(), R.id.myimage, options);
int imageHeight = options.outHeight;
int imageWidth = options.outWidth;
String imageType = options.outMimeType; // 图片的mimeType

options.outHeight和options.outWidth会获取图片的宽和高,通过获取到图片的宽和高,就可以使用options的inSample来缩放图片,

在加载图片显示到屏幕的时候,我们最好跟屏幕的密度一致,所以可以通过inSample来设置最终要缩放到多少,另外一方面我们有时候只需要缩略图,也需要进行缩放。下面是Android提供的代码:

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
int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight){
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1 ;
if(reqWidth < width || reqHeight <height){
final int halfWidth = width / 2;
final int halfHeight = height / 2;
// Calculate the largest inSampleSize value that is a power of 2 and keeps both // height and width larger than the requested height and width.
while( (halfWidth / inSampleSize > reqWidth && halfHeight / inSampleSize > reqHeight){
inSampleSize *= 2;
}
}
}

通过这个方法得到inSampleSize,然后通过将options的inJustDecodeBounds设置为false就可以利用BitmapFactory加载图片了,得到的图片是经过缩放了的。

Bitmap.Config

除了根据屏幕的情况,图片在屏幕中显示的大小来缩放图片,另外也可以通过options的inPreferredConfig设置图片的Config,使用RGB_565的图片肯定比RGB_8888占用的内存要小的。

PNG or JPG

另外还需要考虑的一个问题是图片的格式,使用png图片还是jpg图片,jpg压缩率要高意味着解码的时候消耗的时间肯能会更高,它没有alpha通道,但是对于内置在apk里面的图片,如果图片小,那么apk的大小也会变小。另外如果图片的色值丰富的话,用可能有好点,色值单调可能用png好点(比如我们的icon)。

图片内存管理

在前面一篇有介绍过Android中图片的存储相关知识,在Android 2.3以及之前图片会存储在native内存中,Android推荐在Bitmap不再使用的时候,使用recycle方法回收Bitmap,因为图片存储在native内存当中,所以需要手动回收,另外也可以使用引用计数的方式在Bitmap的计数为0时,调用Bitmap的recycle方法。

下面是Android提供的一段引用计数的代码

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
private int mCacheRefCount = 0;
private int mDisplayRefCount = 0;
...
// Notify the drawable that the displayed state has changed.
// Keep a count to determine when the drawable is no longer displayed.
public void setIsDisplayed(boolean isDisplayed) {
synchronized (this) {
if (isDisplayed) {
mDisplayRefCount++;
mHasBeenDisplayed = true;
} else {
mDisplayRefCount--;
}
}
// Check to see if recycle() can be called.
checkState();
}
// Notify the drawable that the cache state has changed.
// Keep a count to determine when the drawable is no longer being cached.
public void setIsCached(boolean isCached) {
synchronized (this) {
if (isCached) {
mCacheRefCount++;
} else {
mCacheRefCount--;
}
}
// Check to see if recycle() can be called.
checkState();
}
private synchronized void checkState() {
// If the drawable cache and display ref counts = 0, and this drawable
// has been displayed, then recycle.
if (mCacheRefCount <= 0 && mDisplayRefCount <= 0 && mHasBeenDisplayed
&& hasValidBitmap()) {
getBitmap().recycle();
}
}
private synchronized boolean hasValidBitmap() {
Bitmap bitmap = getBitmap();
return bitmap != null && !bitmap.isRecycled();
}

另外一方面也可以考虑使用inPurgeable,inPurgeable让Android在需要的时候可以回收像素,减少OOM。需要重新绘制的时候,又重新解码。其实这会导致更多的计算消耗。

在3.0之后开始放到Java层内存中,在3.0之后也增加了inBitmap。inBitmap的使用方式如下:

1
2
3
4
5
6
7
8
Bitmap inBitmap ; //已经使用了的Bitmap
BitmapFactory.Options optiosn = new BitmapFactory.Options();
options.inBitmap = inBitmap ;
Bitmap newBitmap = BitmapFactory.Options.decodeFile(filename, options);

需要注意的时候在4.4之前不支持不同大小的图片使用inBitmap,4.4才开始支持不同的大小,只要inBitmap比需要新加载的图片更大。另外inBitmap其实可以与下一节介绍的缓存一起使用,可以使用缓存了的图片作为inBitmap来加载新的图片。这里也有例子。

缓存方式

如果都是每次使用BitmapFactory从sdcard读取Bitmap,那么将会非常浪费时间,因为从磁盘读取图片是非常慢的,而且有的图片需要经常使用,如果把图片缓存在内存当中将能够很好地节省图片读取的时间。这样图片缓存就出现了。

LruCache

LruCache是一个非常适合缓存图片的类,它是基于LinkedHashMap实现的。把最近经常使用的对象保存在LinkedHashMap里面,把最近没使用的从LinkedHashMap中移除。LinkedHashMap是一个LinkedList和HashMap一起实现的。需要注意一点的是在以前经常使用SoftReference或WeakReference来引用Bitmap来缓存,但是在2.3后,Android虚拟机的垃圾回收机制回收Soft和Weak引用更加积极了,也就是说它会很快就回收软引用和弱引用对象。

Android可以通过Runtime.getRuntime().getMaxMemory()获取最大内存,Android提供了一段小代码来使用LruCache:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Get max available VM memory, exceeding this amount will throw an
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
// int in its constructor.
final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
// Use 1/8th of the available memory for this memory cache.
final int cacheSize = maxMemory / 8;
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
@Override
protected int sizeOf(String key, Bitmap bitmap) {
// The cache size will be measured in kilobytes rather than
// number of items.
return bitmap.getByteCount() / 1024;
}
};

DiskLruCache

我们的图片可能是从网络中下载下来的,但是我们的应用中可能也没办法把图片全部放进内存,我们需要把图片保存在Disk中,另外一方面我们也不想要每次都从网络中下载图片。LruDiskCache就提供了一种磁盘缓存。当然如果图片访问非常频繁,使用ContentProvider的话将会更好。

Ashmem

除了一般的缓存方式,强大的Fresco在5.0之前利用ashmem来缓存图片,将图片保存在ashmem当中,这样就不会占用太多Java堆内存而导致出现OOM的情况。下面是一段简单的将图片存在ashmem中并且读取出来的例子:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
private void testBitmapMemoryFile(){
// Log.i(LOGTAG,""+bitmap.getConfig().name());
InputStream is = getResources().openRawResource(R.raw.test4);
byte[] imgArr = new byte[10 * 1024 * 1024];
int imgBytes = 0;
try {
while (is.available() > 0) {
int bytes = is.read(imgArr, imgBytes, 1024);
imgBytes += bytes;
Log.i(LOGTAG, "bytes read:" + bytes);
}
is.close();
Log.i(LOGTAG, "bytes : " + imgBytes);
 memoryFile = new MemoryFile(null, imgBytes);
OutputStream os = memoryFile.getOutputStream();
 os.write(imgArr, 0, imgBytes);
imgArr = null;
os.flush();
 os.close();

BitmapFactory.Options options = new BitmapFactory.Options();
options.inPurgeable = true;
fd = getMemoryFileFd(memoryFile,null,options);
if (fd == null) {
memoryFile.close();
Log.e(LOGTAG, "fd read error");
return;
}
Log.i(LOGTAG, "fd read ok");
bitmap = BitmapFactory.decodeFileDescriptor(fd);
iv.setImageBitmap(bitmap);
memoryFile.close();
} catch (IOException e) {
e.printStackTrace();
}
}

上面的代码是将一张raw目录下面的图片保存到AshmemFile当中,然后再利用BitmapFactory读取图片。并且使用inPurgeable标志。AshmemFile和inPurgeable合起来使用。

配置改变时缓存

Activity可能会经常遇到旋转屏幕的情况,会被重新加载,另外在跳转的时候也可能会被finish掉,返回来又得重新加载。这种时候如果对于已经加载了的东西都全部重新加载那会非常耗费时间,这种时候保存Cache,不重新加载将会是一个非常的方式,比如在Fragment中使用了Cache,可以使用setRetainInstance(true),避免重新创建Fragment,这样也能避免加载Fragment中的Cache。

总结

这篇文章主要介绍了图片加载过程中的相关配置,比如利用Options的参数配置输出图片的大小,Bitmap.Config配置解码图片像素格式,以及如何使用PNG和JPG图片。另外介绍了图片缓存(LRUCache和AshmemFile)以及图片存储管理。


参考

  1. https://developer.android.com/training/displaying-bitmaps/index.html
  2. https://mp.weixin.qq.com/s?__biz=MzA3NTYzODYzMg==&mid=403263974&idx=1&sn=b0315addbc47f3c38e65d9c633a12cd6&scene=0&key=41ecb04b051110037b72d05bba1495f596e848534fc51afe877d63329a16dc24dc1d3606aaaba3745a05bfdb8c624a74&ascene=0&uin=Mjc3OTU3Nzk1&devicetype=iMac+MacBookPro10%2C1+OSX+OSX+10.10.5+build%2814F27%29&version=11020201&pass_ticket=kK4%2F6316QveG8O0vFtthPfBeKkNjyaL4HapsUAokHL5mUKCgI5hKTIKMc3D8uyqk