bitmap對象池基礎(chǔ)代碼學(xué)習(xí)
GroupedLinkedMap.java的邏輯跟java.util.LinkedHashMap類似, 也是通過LRU算法來記錄最近最少使用的數(shù)據(jù). 只不過這里的數(shù)據(jù)指的是尺寸信息, 而不是具體的某個bitmap對象. 同時, GroupedLinkedMap把每次get()請求都認(rèn)為是一次訪問, 而忽略add和remove操作.
代碼邏輯
對于其中的雙向鏈表的增長邏輯, 如下圖:
首先有一個初始節(jié)點(diǎn)Head :

當(dāng)遇到get(Key)操作時, 會新生成一個節(jié)點(diǎn) 并調(diào)用makeHead(entry); 如下圖: 新生成一個節(jié)點(diǎn)B

在makeHead()方法的最后會更新該新節(jié)點(diǎn)B. 最后變成如下:

于是一次get調(diào)用結(jié)束后, 兩個節(jié)點(diǎn)的關(guān)系就如上圖
當(dāng)再發(fā)生一次get操作: 會新生成一個節(jié)點(diǎn)C , 同樣不管命中與否,都要調(diào)用makeHead()

makeHead()--> updateEntry()

其實(shí)是把新節(jié)點(diǎn)插入到head和B節(jié)點(diǎn)之間了, 變換一下上圖, 最后結(jié)果如下:

為了更清晰的分析其邏輯, 再通過get(key)操作, 插入一個新節(jié)點(diǎn), 最終圖:

可以看到head的next始終指向最近被訪問的那個節(jié)點(diǎn). 而head的prev指向最后被訪問的那個節(jié)點(diǎn)
于是我們知道, 如果要刪除最后的那個節(jié)點(diǎn), 就可以通過head立馬找到.
這個列表結(jié)構(gòu), 應(yīng)該也算是一個典型的解決方案. 需要記住.
具體分析其removeLast的代碼:
@Nullable
public V removeLast() {
LinkedEntry<K, V> last = head.prev;
while (!last.equals(head)) {
V removed = last.removeLast();
if (removed != null) {
return removed;
} else {
// We will clean up empty lru entries since they are likely to have been one off or
// unusual sizes and
// are not likely to be requested again so the gc thrash should be minimal. Doing so will
// speed up our
// removeLast operation in the future and prevent our linked list from growing to
// arbitrarily large
// sizes.
removeEntry(last);
keyToEntry.remove(last.key);
last.key.offer();
}
last = last.prev;
}
return null;
}
如果不看else分支中的特殊邏輯, 那么這個方法是非常簡單的, 就是通過head節(jié)點(diǎn)的prev找到最不經(jīng)常被訪問的節(jié)點(diǎn), 并返回.
需要注意的是, GroupedLinkedMap中的數(shù)據(jù)除去雙向鏈表的結(jié)構(gòu), 還有一個map結(jié)構(gòu), value值是LinkedEntry. 而這個自定義的LinkedEntry, 是持有一個列表來保存數(shù)據(jù).
問題:
為什么put操作, 會把新創(chuàng)建的節(jié)點(diǎn)放到隊(duì)尾, 也就是最近不不經(jīng)常使用的位置上. 這是為了什么呢? 難道它的意思是說, 新創(chuàng)建的節(jié)點(diǎn), 一次都沒有被訪問過(get操作) 所以應(yīng)該放在最后的位置上.
--------put操作只會把新建的節(jié)點(diǎn)放在隊(duì)尾, 因?yàn)檫@個節(jié)點(diǎn)從來沒有被訪問過, 只要這種類型的數(shù)據(jù)被訪問了, 更具體來說就是被從對象池中拿去使用了, 那就會把該節(jié)點(diǎn)前移, 所以如果一直沒人用, 那么這種類型的數(shù)據(jù)在以后遇到內(nèi)存不足時, 會被優(yōu)先清理掉, 就是因?yàn)闆]人用.
問題:
BitmapPool中存放的bitmap, 或者更具體的說, GroupedLinkedMap中存放的bitmap, 到底是正在被使用的? 還是沒有被使用的?
--------回答這個問題, 可以通過查看BitmapPool.java這個接口的方法注釋來解答, put()操作就是把bitmap放回緩沖池, 所以其中的bitap是不在用的.
可以嘗試跟蹤一下, 對于略懂應(yīng)用, 在請求圖片時, 都是哪些尺寸的圖片請求最多, 如果說圖片的分類比較集中, 那么可以據(jù)此進(jìn)一步優(yōu)化bitmap對象池.
通過為RequestOptions設(shè)置下面的標(biāo)志, 能夠使得所有的對象池圖片尺寸都按照控件請求的尺寸, 也就是說如果控件要求一個200x300的圖片, 那么不管后臺給的是200x301還是200x400 ,那么都會只裁剪成200x300, 這樣會大大減輕bitmap對象池的占用量和可能導(dǎo)致的回收.
為什么會影響回收? 是因?yàn)槿绻麑ο蟪貎?nèi)的key, 也就是圖片的分辨率種類太多, 那么對象池的空間必然會很快的超過最大限制, 那么進(jìn)而會觸發(fā)一次對象池回收, 回收掉最不常用的一種key , 也就是把其對應(yīng)的列表中的bitmap都進(jìn)行一次recycle回收, 這是很耗時的. 而且都在主線程中完成.
DEFAULT_REQUESTOPTIONS.getOptions().set(FIX_BITMAP_SIZE_TO_REQUESTED_DIMENSIONS, true);
SizeConfigStrategy.java
groupedMap變量定義:
private final GroupedLinkedMap<Key, Bitmap> groupedMap = new GroupedLinkedMap<>();
他的key信息包括尺寸和config.
sortedSizes變量, 定義:
private final Map<Bitmap.Config, NavigableMap<Integer, Integer>> sortedSizes = new HashMap<>();
key時Bitmap.Config, value是一個map, 首先這個map的定義:
public interface NavigableMap<K,V> extends SortedMap<K,V> {
所以這個map是排過序的, 當(dāng)然是按key排序的. 它的key是圖片尺寸, 類型為integer, value類型也是integer, 記錄的是圖片尺寸對應(yīng)的數(shù)量.
sortedSizes到底是什么意義? 理解不了呢!!!!
sortedSizes這個map的value值雖然聲明為NavigableMap, 但實(shí)際類型為TreeMap
NavigableMap<Integer, Integer> sizes = sortedSizes.get(config);
if (sizes == null) {
sizes = new TreeMap<>();
sortedSizes.put(config, sizes);
}
需要搞清楚這個bitmap.getConfig()值, 是每個bitmap各部相同嗎? 還是系統(tǒng)內(nèi)都是一樣的, 類似Bitmap.Config.ARGB_8888 ????
-------回答: Bitmap.Config是一個emun類型, 當(dāng)然整個系統(tǒng)內(nèi)只有有限的幾種, 具體到我們的應(yīng)用內(nèi), 只有Bitmap.Config.ARGB_8888 一種. 所以sortedSizes結(jié)構(gòu)就可以解構(gòu)一層, 只需要關(guān)注其中的value類型:NavigableMap<Integer, Integer> sizes
那么這個類型的sizes有序散列表, 代表了什么含義呢/?>????
這個有序散列表的key是圖片的大小, 比如1m, 2m, 或者多少K 是一個數(shù)值.
而這個有序散列表的vaue是這種大小的數(shù)量. 比如目前整個bitmap對象池內(nèi)有1M的圖片3張, 那么value就是3.
在圖片歸還bitmap對象池時, 需要增加這個value值, 如果不存在就置為1
@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);
}
當(dāng)從bitmap對象池內(nèi)請求圖片后, 需要對應(yīng)的減少value的計(jì)數(shù)
@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,
result.getConfig() != null ? result.getConfig() : Bitmap.Config.ARGB_8888);
}
return result;
}
計(jì)數(shù)減一的操作都在decrementBitmapOfSize方法中:
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 == null) {
throw new NullPointerException("Tried to decrement empty size"
+ ", size: " + size
+ ", removed: " + logBitmap(removed)
+ ", this: " + this);
}
if (current == 1) {
sizes.remove(size);
} else {
sizes.put(size, current - 1);
}
}
如果當(dāng)前只有1個, 再減1就是0, 那么就直接從sizes散列表中刪除這一項(xiàng).
否則就是單純的減1.
這個邏輯就是比較簡單易懂了.
同理, 當(dāng)bitmap對象池, 由于占用空間超過最大限制, 而必須刪除一項(xiàng)時, 也需要把sizes中的計(jì)數(shù)減1
@Override
@Nullable
public Bitmap removeLast() {
Bitmap removed = groupedMap.removeLast();
if (removed != null) {
int removedSize = Util.getBitmapByteSize(removed);
decrementBitmapOfSize(removedSize, removed);
}
return removed;
}
剩下最后一個難懂的方法findBestKey, 如下圖:
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;
}
為什么要使用NavigableMap, 是因?yàn)樗幸粋€celling方法, 對于根據(jù)size進(jìn)行的查詢可以返回, 一個大于等于size的bitmap, 這樣仍然可以重用. 而不必強(qiáng)制要求size在map中存在.
說白一點(diǎn): 能找到一個相同尺寸的bitmap拿來重用當(dāng)然好, 但是如果找不到, 就找一個稍微大一點(diǎn)的也行. 也能重用.
當(dāng)然, ceilingKey(key) 返回的是大于等于key的, 最接近key的key, 所以只要有等于key的值, 肯定還是優(yōu)先使用key 這個尺寸本身.
AttributeStrategy.java
在K版本之前的版本上作為LruBitmapPool的具體實(shí)現(xiàn), 邏輯比較簡單了, K版本之前對bitmap的復(fù)用, 要求比較高, 只有尺寸完全一致的bitmap才能被復(fù)用, 所以其內(nèi)部實(shí)現(xiàn)很簡單, 就是通過GroupedLinkedMap來保存bitmap, 可以是bitmap 對應(yīng)的key(size, config).
LruBitmapPool.java
根據(jù)版本來選擇對應(yīng)的緩存策略實(shí)現(xiàn)類:
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;
}
dump()
dumpUnchecked()
getDefaultAllowedConfigs()
ThrowingBitmapTracker
NullBitmapTracker
---------------這幾個都可以先不看, 都沒用.
唯一要關(guān)注的是put() 和 get()方法.
實(shí)際上對bitmap的緩存都是通過private final LruPoolStrategy strategy; 來委托實(shí)現(xiàn)的.
最復(fù)雜的方法trimToSize() 從優(yōu)化的經(jīng)歷來看這里面的 removed.recycle()操作非常耗時. 能在非UI線程recycle嗎?
private synchronized void trimToSize(long size) {
while (currentSize > size) {
final Bitmap removed = strategy.removeLast();
// TODO: This shouldn't ever happen, see #331.
if (removed == null) {
if (Log.isLoggable(TAG, Log.WARN)) {
Log.w(TAG, "Size mismatch, resetting");
dumpUnchecked();
}
currentSize = 0;
return;
}
tracker.remove(removed);
currentSize -= strategy.getSize(removed);
evictions++;
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Evicting bitmap=" + strategy.logBitmap(removed));
}
dump();
removed.recycle();
}
}
我們可以想辦法減少trimToSize()的執(zhí)行, 從而避免在ui線程回收bitmap導(dǎo)致的耗時.
bitmapPool等緩存池的大小的設(shè)置, 是通過MemorySizeCalculator來設(shè)置的.
可以看到對于O版本以上的系統(tǒng), 設(shè)置的bitmappool尺寸會比較小.
private final int bitmapPoolSize;
int screenSize = widthPixels * heightPixels * BYTES_PER_ARGB_8888_PIXEL;
int targetBitmapPoolSize = Math.round(screenSize * builder.bitmapPoolScreens);

如果是O版本之前的系統(tǒng), 就是4屏幕的圖片大小空間作為緩存, 而從O版本開始就只有1屏幕了. 原因看上面的注釋.
但是還有一點(diǎn),如果是高于O版本的低內(nèi)存設(shè)備, 那么不會設(shè)置圖片bitmap緩存, 會創(chuàng)建一個BitmapPoolAdapter實(shí)例作為一個虛緩存, 其實(shí)每次取bitmap時都是create操作.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isLowMemoryDevice(activityManager)) {
bitmapPoolScreens = 0;
}