這個問題其實是我前幾天面試時遇到的一個問題,雖然我以前分析過 Glide 的源碼,可是老實說,若是不是面試遇到這類問題,我根本不會留意 Glide 的 Bitmap 複用這塊...無論怎麼說,遇到了這個問題,咱們就來看下 Glide 是如何實現 Bitmap 複用的吧~java
其實,提及「池化」以及對象複用,在 Android 中例子仍是有這麼幾個的。典型的好比 Handler 中的 Message. 當咱們使用 Message 的 obtain 獲取消息的受,其實是從 Message 池中獲取的。Handler 中的 Message 是經過鏈表維護的數據結構,以此來構成一個 「Message 池」。這個池的最大數量由 MAX_POOL_SIZE
這個參數指定,即爲 50.android
那麼,「池化」以及對象複用有什麼好處呢?面試
這是由於對於 Message 這類頻繁使用的對象,若是每次使用的時候直接建立一個對象,那麼可能會因頻繁建立和銷燬致使虛擬機 GC,從而形成頁面卡頓現象,尤爲是在低端設備上面。「池化」以後每次從池子中獲取已經建立的對象進行復用,從而避免了虛擬機頻繁 GC.markdown
對於 Bitmap 這類對象和圖片相關、佔用內存較大的對象,若是頻繁建立和銷燬,對虛擬機的影響可能比 Message 要大得多,所以 Bitmap 複用顯得很是重要。網絡
先看下 Bitmap 是如何進行回收的吧。數據結構
根據官方的建議,在 Android 2.3 及如下的版本中建議使用 recycle()
回收內存,防止 OOM. 可是,使用這個方法的前提是須要確保這個位圖再也不被使用,不然回收以後再使用將會致使運行時錯誤。因此,官方的建議是經過引用計數的方式統計位圖的引用,只有當位圖再也不被引用的時候再真正調用該方法進行回收。app
官方文檔參考:developer.android.com/topic/perfo…框架
在 Android 3.0 上面引入了 BitmapFactory.Options.inBitmap 字段。若是設置了此選項,那麼採用 Options 對象的解碼方法會在加載內容時嘗試重複使用現有位圖。這樣能夠複用現有的 Bitmap,減小對象建立,從而減小發生 GC 的機率。不過,inBitmap 的使用方式存在某些限制。特別是在 Android 4.4(API 級別 19)以前,系統僅支持大小相同的位圖。在 Android 4.4 以後的版本,只要內存大小不小於需求的 Bitmap 均可以複用。ide
因此,當咱們須要在 Android 中使用 Bitmap 的時候,應該考慮進行 Bitmap 複用以提高應用性能。可是,這些複雜的邏輯要如何封裝呢?官方的建議是使用比較成熟的圖片加載框架,好比 Glide. 因此,接下來咱們來分析下 Glide 是如何實現 Bitmap 複用的。oop
咱們直接從 Glide 的 BitmapPool 開始分析。BitmapPool 是一個接口,定義以下:
public interface BitmapPool {
long getMaxSize();
void setSizeMultiplier(float sizeMultiplier);
// 往 pool 中插入 bitmap 以備複用
void put(Bitmap bitmap);
// 從 pool 中獲取 bitmap 以複用
@NonNull Bitmap get(int width, int height, Bitmap.Config config);
@NonNull Bitmap getDirty(int width, int height, Bitmap.Config config);
void clearMemory();
void trimMemory(int level);
}
複製代碼
BitmapPool 經過定義一個 Pool 來讓用戶複用 Bitmap 對象。在 Glide 中,BitmapPool 有一個默認的實現 LruBitmapPool. 顧名思義,也是基於 LRU 的理念設計的。
前面咱們提到過 inBitmap 以 Android 4.4 爲分水嶺,以前和以後的版本在使用上存在版本差別,那麼 BitmapPool 是如何處理這個差別的呢?答案是策略模式。Glide 定義了 LruPoolStrategy 接口,該接口內部定義了增刪相關操做。真實的 Bitmap 數據根據尺寸和顏色等映射關係存儲到 LruPoolStrategy 中。BitmapPool 的 get 和 put 也是經過 LruPoolStrategy 的 get 和 put 完成的。
interface LruPoolStrategy {
void put(Bitmap bitmap);
@Nullable Bitmap get(int width, int height, Bitmap.Config config);
@Nullable Bitmap removeLast();
String logBitmap(Bitmap bitmap);
String logBitmap(int width, int height, Bitmap.Config config);
int getSize(Bitmap bitmap);
}
複製代碼
LruPoolStrategy 默認提供了三個實現,分別是 AttributeStrategy
、SizeConfigStrategy
和 SizeStrategy
. 其中,AttributeStrategy 適用於 Android 4.4 如下的版本,SizeConfigStrategy 和 SizeStrategy 適用於 Android 4.4 及以上的版本。
AttributeStrategy 經過 Bitmap 的 width(圖片寬度)、height(圖片高度) 和 config(圖片顏色空間,好比 ARGB_8888 等) 三個參數做爲 Bitmap 的惟一標識。當獲取 Bitmap 的時候只有這三個條件徹底匹配才行。而 SizeConfigStrategy 使用 size(圖片的像素總數) 和 config 做爲惟一標識。當獲取的時候會先找出 cofig 匹配的 Bitmap(通常就是 config 相同),而後保證該 Bitmap 的 size 大於咱們指望的 size 而且小於指望 size 的 8 倍便可複用(多是爲了節省內存空間)。
所謂的 LRU 就是 BitmapPool 經過 LruPoolStrategy 實現的,具體操做是,在往 BitmapPool 中 put 數據以後會執行下面的操做調整空間大小:
private synchronized void trimToSize(long size) {
while (currentSize > size) {
// 移除尾部的
final Bitmap removed = strategy.removeLast();
if (removed == null) {
currentSize = 0;
return;
}
currentSize -= strategy.getSize(removed);
// ...
// 回收
removed.recycle();
}
}
複製代碼
下面咱們來複習下通常的 Bitmap 加載的步驟。常規的圖片加載過程以下,
// 設置 inJustDecodeBounds 爲 true 來獲取圖片尺寸
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
// 設置 inJustDecodeBounds 爲 false 來真正加載
options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight);
options.inJustDecodeBounds = false;
BitmapFactory.decodeFile(file.getAbsolutePath(), options);
複製代碼
也就是說,首先經過設置 options.inJustDecodeBounds
爲 true 來獲取圖片真實的尺寸,以便設置採樣率。由於咱們通常不會直接加載圖片的全部的像素,而是採樣以後再按需加載,以減小圖片的內存佔用。當真正須要加載的時候,設置 options.inJustDecodeBounds
爲 false,再調用 decode 相關的方法便可。
那麼 Bitmap 複用是如何使用的呢?很簡單,只須要在加載的時候經過 options 的 inBitmap 參數指定一個 Bitmap 對象再 decode 便可:
options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
複製代碼
以前分析 Glide 的源碼的時候,注重的是整個流程,對於不少細節沒用照顧到,這裏我簡化下邏輯。首先,Glide 的 Bitmap 加載流程位於 Downsampler 類中。當從其餘渠道,好比網絡或者磁盤中獲取到一個輸入流 InputStream 以後就能夠進行圖片加載了。下面是 Downsampler 的 decodeFromWrappedStreams
方法,這裏是執行圖片加載的流程,主要代碼的邏輯和功能已經備註到了註釋上面:
private Bitmap decodeFromWrappedStreams(InputStream is, BitmapFactory.Options options, DownsampleStrategy downsampleStrategy, DecodeFormat decodeFormat, ...) throws IOException {
long startTime = LogTime.getLogTime();
// 經過設置 inJustDecodeBounds 讀取圖片的原始尺寸信息
int[] sourceDimensions = getDimensions(is, options, callbacks, bitmapPool);
int sourceWidth = sourceDimensions[0];
int sourceHeight = sourceDimensions[1];
String sourceMimeType = options.outMimeType;
// ...
// 讀取圖片的 exif 信息,若是須要的話,先對圖片進行旋轉
int orientation = ImageHeaderParserUtils.getOrientation(parsers, is, byteArrayPool);
int degreesToRotate = TransformationUtils.getExifOrientationDegrees(orientation);
boolean isExifOrientationRequired = TransformationUtils.isExifOrientationRequired(orientation);
int targetWidth = requestedWidth == Target.SIZE_ORIGINAL ? sourceWidth : requestedWidth;
int targetHeight = requestedHeight == Target.SIZE_ORIGINAL ? sourceHeight : requestedHeight;
ImageType imageType = ImageHeaderParserUtils.getType(parsers, is, byteArrayPool);
// 根據要求計算須要記載的圖片大小和 config,計算結果直接設置給 options 便可
calculateScaling(imageType, is, ..., options);
calculateConfig(is, ..., options, targetWidth, targetHeight);
boolean isKitKatOrGreater = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
if ((options.inSampleSize == 1 || isKitKatOrGreater) && shouldUsePool(imageType)) {
// ...
// 根據圖片的指望尺寸到 BitmapPool 中獲取一個 Bitmap 以複用
if (expectedWidth > 0 && expectedHeight > 0) {
setInBitmap(options, bitmapPool, expectedWidth, expectedHeight);
}
}
// 開始執行 decode 邏輯
Bitmap downsampled = decodeStream(is, options, callbacks, bitmapPool);
callbacks.onDecodeComplete(bitmapPool, downsampled);
// ... 圖片旋轉等後續邏輯
return rotated;
}
複製代碼
上述代碼中的 setInBitmap 方法中即調用了 BitmapPool 的 get 方法用來獲取複用的 Bitmap 對象,其代碼以下:
private static void setInBitmap( BitmapFactory.Options options, BitmapPool bitmapPool, int width, int height) {
@Nullable Bitmap.Config expectedConfig = null;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (options.inPreferredConfig == Config.HARDWARE) {
return;
}
expectedConfig = options.outConfig;
}
if (expectedConfig == null) {
expectedConfig = options.inPreferredConfig;
}
// 調用了 inBitmap
options.inBitmap = bitmapPool.getDirty(width, height, expectedConfig);
}
複製代碼
另外,經過查看 Bitmap 的 inBitmap 文檔註釋,咱們能夠看到可能存在一些狀況致使 inBitmap 過程當中出現異常,那麼 Glide 會不會由於複用 Bitmap 而致使加載過程異常?Glide 又是如何進行處理的呢?參考上述代碼,咱們能夠看到加載圖片調用到了名爲 decodeStream
方法。該方法通過個人簡化以後大體以下:
private static Bitmap decodeStream(InputStream is, BitmapFactory.Options options, DecodeCallbacks callbacks, BitmapPool bitmapPool) throws IOException {
// ...
final Bitmap result;
TransformationUtils.getBitmapDrawableLock().lock();
try {
// 數據加載
result = BitmapFactory.decodeStream(is, null, options);
} catch (IllegalArgumentException e) {
// ...
if (options.inBitmap != null) {
try {
// 輸入流重置
is.reset();
bitmapPool.put(options.inBitmap);
// 清理掉 inBitmap 並進行第二次加載
options.inBitmap = null;
// 再次調用進行加載
return decodeStream(is, options, callbacks, bitmapPool);
} catch (IOException resetException) {
throw bitmapAssertionException;
}
}
throw bitmapAssertionException;
} finally {
TransformationUtils.getBitmapDrawableLock().unlock();
}
if (options.inJustDecodeBounds) {
is.reset();
}
return result;
}
複製代碼
也就是說,Glide 首先會經過設置 inBitmap 複用的方式加載圖片。若是這個過程當中出現了異常,由於此時 inBitmap 不爲空,因此將會進入異常處理流程,此時會清理掉 inBitmap,再次調用 decodeStream
方法二次加載,這個時候就不是 Bitmap 複用的了。因此,Glide 內部會經過錯誤重試機制進行 Bitmap 複用,當複用並出現錯誤的時候,會降級爲非複用的方式第二次進行加載。
以上就是 Glide 中 Bitmap 複用的原理,但願本文對你有所幫助!