Bitmap 在app应用中是个占内存的大块头,google 公司也一直对它做优化,比如说存储机制,Android 2.3.3及以前版本,Bitmap 的像素点数据是保存在 Native Memory(C/C++),Bitmap对象则是保存在 Dalvik heap(Java); 从Android 3.0开始,像素点数据与Bitmap对象一起存储在 Dalvik heap中。从 Android 8.0开始,Bitmap 的像素点数据又保存在Native Memory中。
Android 3.0时,在 BitmapFactory.Options 中新增 inBitmap 字段,通过这个参数,可以复用之前的 Bitmap,前提是这两个图片占用内存大小是一样的,这样就避免了再创建一个Bitmap,实现了复用。 Android 4.4,inBitmap功能增强了,需要用的图片比着要复用的图片小即可,比如第一次创建了图片A,现在要创建图片B,我们可以复用A的前提是 B <= A。 以上的前提是被复用的图片 BitmapFactory.Options 中的 mIsMutable 字段需要设置为true。简单的例子,这里注意,图片占内存比较 b <= a(还需要版本判断)
BitmapFactory.Options aOption = new BitmapFactory.Options();
aOption.inMutable = true; // 设置 inMutable
Bitmap aBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.a, aOption);
BitmapFactory.Options bOption = new BitmapFactory.Options();
bOption.inBitmap = aBitmap; // 设置 inBitmap 被复用的 Bitmap
Bitmap bBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.b, bOption);
Glide 中的 Bitmap 缓存池复用功能设计的很好,甚至有人把它里面这部分代码给抽取出来单独使用。如果项目中引入了 Glide,可以直接通过 Glide.get(context).getBitmapPool() 来获取 BitmapPool pool 对象,然后 Bitmap bitmap = pool.get(width, height, Bitmap.Config.ARGB_8888) 来获取复用的 Bitmap。Glide 中关于这一块设计的也比较巧妙,GlideBuilder 中的 createGlide() 方法
private BitmapPool bitmapPool;
Glide createGlide() {
...
if (bitmapPool == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
int size = calculator.getBitmapPoolSize();
bitmapPool = new LruBitmapPool(size);
} else {
bitmapPool = new BitmapPoolAdapter();
}
}
...
return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat);
}
在这个方法中创建了 BitmapPool 对象,然后传递给了 Glide,Glide 是个单例模式,所以我们可以全局使用。这里看看 bitmapPool 是怎么创建的,它有个版本判断,大于等于 11 则是 LruBitmapPool 类型,否则是 BitmapPoolAdapter 类型,BitmapPoolAdapter 里面都是空方法,既不能存,也不能取,这么设计是因为低于11的系统版本无Bitmap复用的功能,所以这里有个分割,做了版本兼容,我们看看 LruBitmapPool 的代码
@Override
public synchronized boolean put(Bitmap bitmap) {
...
strategy.put(bitmap);
...
return true;
}
@Override
public synchronized Bitmap get(int width, int height, Bitmap.Config config) {
Bitmap result = getDirty(width, height, config);
...
return result;
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR1)
@Override
public synchronized Bitmap getDirty(int width, int height, Bitmap.Config config) {
final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
...
return result;
}
看过 put() 和 get() 方法,发现里面最终还是用 strategy 来做具体的工作,那么看看 strategy 是什么,它是 LruPoolStrategy,又是个接口,熟悉的配方,看看它是怎么创建的
private static LruPoolStrategy getDefaultStrategy() {
final LruPoolStrategy strategy;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
strategy = new SizeConfigStrategy();
} else {
strategy = new AttributeStrategy();
}
return strategy;
}
这里又是版本判断,本次判断的依据是版本19,看到这,我们也能反应过来是怎么回事了,因为Bitmap复用,19之后和之前的要求不一样,而 SizeConfigStrategy 和 AttributeStrategy 里面的代码则体现出这一逻辑。看下 AttributeStrategy 的代码
public void put(Bitmap bitmap) {
final Key key = keyPool.get(bitmap.getWidth(), bitmap.getHeight(), bitmap.getConfig());
groupedMap.put(key, bitmap);
}
@Override
public Bitmap get(int width, int height, Bitmap.Config config) {
final Key key = keyPool.get(width, height, config);
return groupedMap.get(key);
}
这里面牵涉到两个类,KeyPool 和 GroupedLinkedMap,明显 KeyPool 是来缓存 key 的,而 GroupedLinkedMap 则主要是存放 key 及对应的 Bitmap,先看 KeyPool,它继承了 BaseKeyPool,里面是个上限为20的队列,如果需要对象,先从队列中取,如果队列为空,则new一个对象;往里面存储时,则先判断队列是否达到上限,如果没达到上限,则放入队列中。GroupedLinkedMap 的代码也比较有意思,它其实类似 new HashMap<key,LinkedList>, LinkedEntry 本身类似 LinkedList中的
Node 节点,可以持有前后的引用,成一个串,同时 LinkedEntry 中有个 List 的属性,可以把相同key对应的值都放到一个集合中;head 则作为一个节点,当有新的 LinkedEntry 创建时,则会把新的LinkedEntry放到头部,这样就形成了类似一个LruCache的功能,head 的作用也就在此,它就是一个头部标志,离他越远,说明使用频率就越低,所以回收对象时就有优先级了。 AttributeStrategy 中数据的宽和高的数据要求比较严,因为android版本11-18要求如此。 SizeConfigStrategy则复杂点,大家可以自己读一下。