介绍
Glide在解码Bitmap的时候采用了BitmapPool复用池的方式,达到高效利用内存,减少创建内存的开销。复用效果如下:
复用前.png 内存复用.png
未使用复用的情况下,每次解码都会申请一块新的内存,如果使用复用bitmap对象,解码的时候会去池子中找出合适大小的bitmap,使用这个bitmap对象的内存。
bitmap复用并不会减少内存大小,而是减少了内存分配和回收带来的内存抖动导致页面卡顿,以及内存溢出问题。Google的开发者文档中有如下提示:
复用介绍.png
android4.4以上被复用的bitmap内存大小必须大于等于要解码的bitmap的内存大小,才可以复用bitmap
4.4以下3.0以上要解码的bitmap必须是jpeg或者png格式,而且和复用的bitmap大小一样,inSampleSize=1,另外复用的bitmap必须要设置inPreferredConfig
分析
bitmapPool接口
** An interface for a pool that allows users to reuse {@link android.graphics.Bitmap} objects. */
public interface BitmapPool {
/** Returns the current maximum size of the pool in bytes. */
long getMaxSize();
/**
* Multiplies the initial size of the pool by the given multiplier to dynamically and
* synchronously allow users to adjust the size of the pool.
*
* <p>If the current total size of the pool is larger than the max size after the given multiplier
* is applied, {@link Bitmap}s should be evicted until the pool is smaller than the new max size.
*
* @param sizeMultiplier The size multiplier to apply between 0 and 1.
*/
void setSizeMultiplier(float sizeMultiplier);
/**
* Adds the given {@link android.graphics.Bitmap} if it is eligible to be re-used and the pool can
* fit it, or calls {@link Bitmap#recycle()} on the Bitmap and discards it.
*
* <p>Callers must <em>not</em> continue to use the Bitmap after calling this method.
*
* @param bitmap The {@link android.graphics.Bitmap} to attempt to add.
* @see android.graphics.Bitmap#isMutable()
* @see android.graphics.Bitmap#recycle()
*/
void put(Bitmap bitmap);
/**
* Returns a {@link android.graphics.Bitmap} of exactly the given width, height, and
* configuration, and containing only transparent pixels.
*
* <p>If no Bitmap with the requested attributes is present in the pool, a new one will be
* allocated.
*
* <p>Because this method erases all pixels in the {@link Bitmap}, this method is slightly slower
* than {@link #getDirty(int, int, android.graphics.Bitmap.Config)}. If the {@link
* android.graphics.Bitmap} is being obtained to be used in {@link android.graphics.BitmapFactory}
* or in any other case where every pixel in the {@link android.graphics.Bitmap} will always be
* overwritten or cleared, {@link #getDirty(int, int, android.graphics.Bitmap.Config)} will be
* faster. When in doubt, use this method to ensure correctness.
*
* <pre>
* Implementations can should clear out every returned Bitmap using the following:
*
* {@code
* bitmap.eraseColor(Color.TRANSPARENT);
* }
* </pre>
*
* @param width The width in pixels of the desired {@link android.graphics.Bitmap}.
* @param height The height in pixels of the desired {@link android.graphics.Bitmap}.
* @param config The {@link android.graphics.Bitmap.Config} of the desired {@link
* android.graphics.Bitmap}.
* @see #getDirty(int, int, android.graphics.Bitmap.Config)
*/
@NonNull
Bitmap get(int width, int height, Bitmap.Config config);
/**
* Identical to {@link #get(int, int, android.graphics.Bitmap.Config)} except that any returned
* {@link android.graphics.Bitmap} may <em>not</em> have been erased and may contain random data.
*
* <p>If no Bitmap with the requested attributes is present in the pool, a new one will be
* allocated.
*
* <p>Although this method is slightly more efficient than {@link #get(int, int,
* android.graphics.Bitmap.Config)} it should be used with caution and only when the caller is
* sure that they are going to erase the {@link android.graphics.Bitmap} entirely before writing
* new data to it.
*
* @param width The width in pixels of the desired {@link android.graphics.Bitmap}.
* @param height The height in pixels of the desired {@link android.graphics.Bitmap}.
* @param config The {@link android.graphics.Bitmap.Config} of the desired {@link
* android.graphics.Bitmap}.
* @return A {@link android.graphics.Bitmap} with exactly the given width, height, and config
* potentially containing random image data.
* @see #get(int, int, android.graphics.Bitmap.Config)
*/
@NonNull
Bitmap getDirty(int width, int height, Bitmap.Config config);
/** Removes all {@link android.graphics.Bitmap}s from the pool. */
void clearMemory();
/**
* Reduces the size of the cache by evicting items based on the given level.
*
* @param level The level from {@link android.content.ComponentCallbacks2} to use to determine how
* many {@link android.graphics.Bitmap}s to evict.
* @see android.content.ComponentCallbacks2
*/
void trimMemory(int level);
}
接口中的方法名字望文生义也应该知道是干什么,而且注释也很清楚,我们具体看一下实现类,它的实现类也有一个BitmapPoolAdapter适配器适配不使用复用的时候,另外一个就是LruBitmapPool,采用LRU算法管理复用池。
首先看一下LruBitmapPool的结构
public class LruBitmapPool implements BitmapPool {
private static final String TAG = "LruBitmapPool";
private static final Bitmap.Config DEFAULT_CONFIG = Bitmap.Config.ARGB_8888;//默认格式
private final LruPoolStrategy strategy;//LruPool策略
private final Set<Bitmap.Config> allowedConfigs;//允许复用的格式,在Android的O版本以上移除HARDWARE格式
private final long initialMaxSize;//默认大小
private final BitmapTracker tracker;//bitmap跟踪记录,默认是null实现
private long maxSize;
private long currentSize;//已用大小
private int hits;
private int misses;
private int puts;
private int evictions;
...
public LruBitmapPool(long maxSize) {
this(maxSize, getDefaultStrategy(), getDefaultAllowedConfigs());
}
...
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;
}
...
}
构造方法中根据不同的sdk版本采用不同的策略,现在大部分的应用都是4.4以上版本,所以主要分析SizeConfigStrategy策略即可。SizeConfigStrategy这个类中采用Lru算法,但是会根据不同的bitmap config缓存不同的复用池
- put方法
public synchronized void put(Bitmap bitmap) {
//安全校验
...
//bitmap不支持复用,bitmap大小超出了最大容量,配置表中不支持此格式,这些情况bitmap都回收,不能复用
if (!bitmap.isMutable()
|| strategy.getSize(bitmap) > maxSize
|| !allowedConfigs.contains(bitmap.getConfig())) {
bitmap.recycle();
return;
}
final int size = strategy.getSize(bitmap);
strategy.put(bitmap);
...
evict();
}
strategy.getSize(bitmap);得到bitmap的内存大小,重要的是strategy.put(bitmap);看一下SizeConfigStrategy#put方法
private final KeyPool keyPool = new KeyPool();
private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<>();
private final Map<Bitmap.Config, NavigableMap<Integer, Integer>> sortedSizes = new HashMap<>();
@Override
public void put(Bitmap bitmap) {
int size = Util.getBitmapByteSize(bitmap);
Key key = keyPool.get(size, bitmap.getConfig());
groupedMap.put(key, bitmap);
NavigableMap<Integer, Integer> sizes = getSizesForConfig(bitmap.getConfig());
Integer current = sizes.get(key.size);
sizes.put(key.size, current == null ? 1 : current + 1);
}
groupedMap的key是根据bitmap的config和size生成的,这样groupedMap可以存储不同格式相同内存大小的bitmap。
private NavigableMap<Integer, Integer> getSizesForConfig(Bitmap.Config config) {
NavigableMap<Integer, Integer> sizes = sortedSizes.get(config);
if (sizes == null) {
sizes = new TreeMap<>();
sortedSizes.put(config, sizes);
}
return sizes;
}
根据bitmap的格式从sortedSizes中取出NavigableMap对象,这个对象中的key是bitmap 的内存大小,value是一个引用计数
简单介绍一下NavigableMap:
NavigableMap扩展了 SortedMap,具有了针对给定搜索目标返回最接近匹配项的导航方法。方法 lowerEntry、floorEntry、ceilingEntry 和 higherEntry 分别返回与小于、小于等于、大于等于、大于给定键的键关联的 Map.Entry 对象,如果不存在这样的键,则返回 null。类似地,方法 lowerKey、floorKey、ceilingKey 和 higherKey 只返回关联的键。
- LruBitmapPool#get方法
public Bitmap get(int width, int height, Bitmap.Config config) {
Bitmap result = getDirtyOrNull(width, height, config);
if (result != null) {
//擦除透明 通道
result.eraseColor(Color.TRANSPARENT);
} else {
result = createBitmap(width, height, config);
}
return result;
}
@NonNull
private static Bitmap createBitmap(int width, int height, @Nullable Bitmap.Config config) {
return Bitmap.createBitmap(width, height, config != null ? config : DEFAULT_CONFIG);
}
@Nullable
private synchronized Bitmap getDirtyOrNull(
int width, int height, @Nullable Bitmap.Config config) {
...
final Bitmap result = strategy.get(width, height, config != null ? config : DEFAULT_CONFIG);
...
return result;
}
get先从复用池中筛选合适的内存大小的Bitmap,如果没有就使用Bitmap.createBitmap创建一个
重点分析一下strategy中的get方法
@Override
@Nullable
public Bitmap get(int width, int height, Bitmap.Config config) {
int size = Util.getBitmapByteSize(width, height, config);
Key bestKey = findBestKey(size, config);
Bitmap result = groupedMap.get(bestKey);
if (result != null) {
// Decrement must be called before reconfigure.
decrementBitmapOfSize(bestKey.size, result);
result.reconfigure(width, height, config);
}
return result;
}
//第一步根据bitmap内存大小和格式找出key
private Key findBestKey(int size, Bitmap.Config config) {
Key result = keyPool.get(size, config);
for (Bitmap.Config possibleConfig : getInConfigs(config)) {
NavigableMap<Integer, Integer> sizesForPossibleConfig = getSizesForConfig(possibleConfig);
Integer possibleSize = sizesForPossibleConfig.ceilingKey(size);
if (possibleSize != null && possibleSize <= size * MAX_SIZE_MULTIPLE) {
if (possibleSize != size
|| (possibleConfig == null ? config != null : !possibleConfig.equals(config))) {
keyPool.offer(result);
result = keyPool.get(possibleSize, possibleConfig);
}
break;
}
}
return result;
}
根据config找出NavigableMap,然后使用ceilingKey找出大等于size 的key值possibleSize,possibleSize如果大于size的8倍就造成内存浪费,possibleSize 和size不相等或者配置不同就更新key。get方法根据这个key从groupedMap中取出要复用的bitmap。然后是decrementBitmapOfSize 方法减少bitmap的大小
private void decrementBitmapOfSize(Integer size, Bitmap removed) {
Bitmap.Config config = removed.getConfig();
NavigableMap<Integer, Integer> sizes = getSizesForConfig(config);
Integer current = sizes.get(size);
...
if (current == 1) {
sizes.remove(size);
} else {
sizes.put(size, current - 1);
}
}
这个方法会根据config找出复用管理的NavigableMap然后将引用计数-1;
至此bitmapPool的get和put方法都分析完了,这两个方法都是委托给LruPoolStrategy实现的。
回到put方法中在最后会调用evict();方法有调用trimToSize(maxSize);方法
private synchronized void trimToSize(long size) {
while (currentSize > size) {
final Bitmap removed = strategy.removeLast();
...
tracker.remove(removed);
currentSize -= strategy.getSize(removed);
...
removed.recycle();
}
}
这个方法会一直通过strategy移除最少使用的Bitmap并且回收掉内存,以达到需要的内存大小。
网友评论