原文連結:Glide核心設計二:快取管理
引言
Glide作為一個優秀的圖片載入框架,快取管理是必不可少的一部分,這篇文章主要通過各個角度、從整體設計到程式碼實現,深入的分析Glide的快取管理模組,力求在同類分析Glide快取的分析文章中脫穎而出。關於Glide的生命週期繫結,可檢視Glide系列文章Glide核心設計一:皮皮蝦,我們走。
前提
- 本文分析Glide快取管理,將以使用Glide載入網路圖片為例子,如載入本地圖片、Gif資源等使用不是本文的重點。因不管是何種使用方式,快取模組都是一樣的,只抓住網路載入圖片這條主線,邏輯會更清晰。
- 本文將先給出Glide快取管理整體設計的結論,然後再分析原始碼。
整體設計
快取型別
Glide的快取型別分為兩大類,一類是Resource快取,一類是Bitmap快取。
Resource快取
為什麼需要快取圖片Resource,很好理解,因為圖片從網路載入,將圖片快取到本地,當需要再次使用時,直接從快取中取出而無需再次請求網路。
三層快取
Glide在快取Resource使用三層快取,包括:
- 一級快取:快取被回收的資源,使用LRU演算法(Least Frequently Used,最近最少使用演算法)。當需要再次使用到被回收的資源,直接從記憶體返回。
- 二級快取:使用弱引用快取正在使用的資源。當系統執行gc操作時,會回收沒有強引用的資源。使用弱引用快取資源,既可以快取正在使用的強引用資源,也不阻礙系統需要回收無引用資源。
- 三級快取:磁碟快取。網路圖片下載成功後將以檔案的形式快取到磁碟中。
Bitmap快取
Bitmap所佔記憶體大小
Bitmap所佔的記憶體大小由三部分組成:圖片的寬度解析度、高度解析度和Bitmap質量引數。公式是:Bitmap記憶體大小 = (寬pix長pix)質量引數所佔的位數。單位是位元組B。
Bitmap壓縮質量引數
質量引數決定每一個畫素點用多少位(bit)來顯示:
- ALPHA_8就是Alpha由8位組成(1B)
- ARGB_4444就是由4個4位組成即16位(2B)
- ARGB_8888就是由4個8位組成即32位(4B)
- RGB_565就是R為5位,G為6位,B為5位共16位(2B)
Glide預設使用RGB_565,比系統預設使用的ARGB_8888節省一半的資源,但RGB_565無法顯示透明度。
舉個例子:在手機上顯示100pix*200pix的圖片,解壓前15KB,是使用Glide載入(預設RGB_565)Bitmap所佔用的記憶體是:(100x200)x2B = 40000B≈40Kb,比以檔案的形成儲存的增加不少,因為png、jpg等格式的圖片經過壓縮。正因為Bitmap比較消耗記憶體,例如使用Recyclerview等滑動控制元件顯示大量圖片時,將大量的建立和回收Bitmap,導致記憶體波動影響效能。
Bitmap快取演算法
在Glide中,使用BitmapPool來快取Bitmap,使用的也是LRU演算法。當需要使用Bitmap時,從Bitmap的池子中取出合適的Bitmap,若取不到合適的,則再新建立。當Bitmap使用完後,不直接呼叫Bitmap.recycler()回收,而是放入Bitmap的池子。
快取的Key型別
Glide的快取使用
- 從對比中可看出,Resource三層快取所使用的key的構造形式是一樣的,包括圖片id(圖片的Url地址),寬高等引數來標識。對於其他引數,舉一個例子理解:圖片資源從網路載入後,經過解碼(decode)、快取到磁碟、從磁碟中取出、變換資源(加圓角等,transformation)、磁碟快取變換後的圖片資源、轉碼(transcode)顯示。
- Bitmap的快取Key的構造相對簡單得多,由長、寬的解析度以及圖片壓縮引數即可唯一標示一個回收的Bitmap。當需要使用的bitmap時,在BitmapPool中查詢對應的長、寬和config都一樣的Bitmap並返回,而無需重新建立。
Resource快取流程
Resource包括三層快取,通過流程圖看它們之間的關係:
因為記憶體快取優於磁碟快取,所以當需要使用資源時,先從記憶體快取中查詢(一級快取和二級快取都是記憶體快取,其功能不一樣,一級快取用於在記憶體中快取不是正在使用的資源,二級快取是儲存正在使用的資源),再從磁碟快取中查詢。若都找不到,則從網路載入。
滑動控制元件多圖的效能優化
不論是Resource還是Bitmap快取,若顯示的僅是部分照片,並且不存在頻繁使用的場景,則使用Glide沒有太大的優勢。設計快取的目的就是為了在重複顯示時,更快、更省的顯示圖片資源。Glide有針對ListView、Recyclerview等控制元件載入多圖時進行優化。此處討論最常見的場景:Recyclerview顯示多圖,簡略圖如下。
如上圖所示,當圖5劃入介面時,會複用圖一的Item,設定新的圖片之前,會先清空原有圖片的資源,清空時會把Resource資源放入一級快取待將來複用,同時會將回收的Bitmap放入BitmapPool中;當圖5向下隱藏,圖一出現時,圖5的資源會放到一級快取中,圖一的資源則從一級快取中取出,無須重新網路請求,同時所需要的Bitmap也無須重新建立,直接複用。
LRU演算法
BitmapPool的LRU演算法流程圖如下:
類圖
在進行程式碼分析前,先給出跟Glide快取管理相關的類圖(省略類的大部分變數和方法)。
Glide快取管理類圖大圖地址
程式碼實現
根據以上的Glide快取管理的結論及類圖,可自主跟原始碼,跳過以下內容。
Glide.with(Context).load(String).into(ImageView)
Glide.with(Context)
返回RequestManager,主要實現和Fragment、Activity生命週期的繫結,詳情請看Glide核心設計一:皮皮蝦,我們走。
.load(String)
RequestManager的load(String)方法返回DrawableTypeRequest,根據圖片地址返回一個用於建立圖片請求的Request的Builder,程式碼如下:
public DrawableTypeRequest<String> load(String string) {
return (DrawableTypeRequest<String>) fromString().load(string); //呼叫fromString()和load()方法
}複製程式碼
fromString()方法呼叫loadGeneric()方法,程式碼如下:
public DrawableTypeRequest<String> fromString() {
return loadGeneric(String.class);
}
private <T> DrawableTypeRequest<T> loadGeneric(Class<T> modelClass) {
ModelLoader<T, InputStream> streamModelLoader = Glide.buildStreamModelLoader(modelClass, context);
ModelLoader<T, ParcelFileDescriptor> fileDescriptorModelLoader =
Glide.buildFileDescriptorModelLoader(modelClass, context);
if (modelClass != null && streamModelLoader == null && fileDescriptorModelLoader == null) {
throw new IllegalArgumentException("Unknown type " + modelClass + ". You must provide a Model of a type for"
+ " which there is a registered ModelLoader, if you are using a custom model, you must first call"
+ " Glide#register with a ModelLoaderFactory for your custom model class");
}
return optionsApplier.apply( //傳遞的引數中建立了一個DrawableTypeRequest並返回該物件
new DrawableTypeRequest<T>(modelClass, streamModelLoader, fileDescriptorModelLoader, context,
glide, requestTracker, lifecycle, optionsApplier));
}複製程式碼
DrawableTypeRequest的load()方法如下:
@Override
public DrawableRequestBuilder<ModelType> load(ModelType model) {
super.load(model);
return this;
}複製程式碼
DrawableTypeRequest父類是DrawableRequestBuilder,父類的父類是GenericRequestBuilder,呼叫super.load()方法如下:
public GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> load(ModelType model) {
this.model = model;
isModelSet = true;
return this;
}複製程式碼
以上程式碼可知,快取管理的主要實現程式碼並不在.load(Sting)程式碼,接下來繼續分析.into(ImageView)程式碼。
.into(ImageView)
GenericRequestBuilder的into(ImageView)程式碼如下:
public Target<TranscodeType> into(ImageView view) {
Util.assertMainThread();
if (view == null) {
throw new IllegalArgumentException("You must pass in a non null View");
}
if (!isTransformationSet && view.getScaleType() != null) {
switch (view.getScaleType()) { //根據圖片的scaleType做相應處理
case CENTER_CROP:
applyCenterCrop();
break;
case FIT_CENTER:
case FIT_START:
case FIT_END:
applyFitCenter();
break;
//$CASES-OMITTED$
default:
// Do nothing.
}
}
//呼叫buildImageViewTarget()方法建立了一個Target型別的物件
return into(glide.buildImageViewTarget(view, transcodeClass));
}複製程式碼
以上程式碼主要有兩個功能:
- 根據ScaleType進行圖片的變換
- 將ImageView轉換成一個Target
繼續檢視into(Target)的程式碼:
public <Y extends Target<TranscodeType>> Y into(Y target) {
Util.assertMainThread();
if (target == null) {
throw new IllegalArgumentException("You must pass in a non null Target");
}
if (!isModelSet) {
throw new IllegalArgumentException("You must first set a model (try #load())");
}
Request previous = target.getRequest(); //獲取請求體Request
if (previous != null) { //若ImageView是複用過的,則previous不為空
previous.clear(); //呼叫clear()方法清空ImageView上的圖片資源,此方法會將回收的Resource放入記憶體快取中,並不在記憶體中清空該資源。
requestTracker.removeRequest(previous); //移除老的請求
previous.recycle(); //回收Request使用
}
Request request = buildRequest(target); //獲取新的Request
target.setRequest(request); //將新的request設定到target中
lifecycle.addListener(target); //新增生命週期的監聽
requestTracker.runRequest(request); //啟動Request
return target;
}複製程式碼
以上程式碼,主要將圖片載入的Request繫結到Target中,若原有Target具有舊的Request,得先處理舊的Request,再繫結上新的Request。target.setRequest()和target.getRequest()最終會呼叫ViewTarget的setRequest()方法和getRequest()方法,程式碼如下:
public void setRequest(Request request) {
setTag(request);
}
private void setTag(Object tag) {
if (tagId == null) {
isTagUsedAtLeastOnce = true;
view.setTag(tag);//呼叫view的setTag方法,將Request和view做繫結
} else {
view.setTag(tagId, tag);//呼叫view的setTag方法,將Request和view做繫結
}
}
public Request getRequest() {
Object tag = getTag(); //獲取view 的tag
Request request = null;
if (tag != null) {
if (tag instanceof Request) { //若該tag是Request的一個例項
request = (Request) tag;
} else { //使用者不能給view設定tag,因為該view的tag要用於儲存Glide的Request物件,否則丟擲異常
throw new IllegalArgumentException("You must not call setTag() on a view Glide is targeting");
}
}
return request;
}複製程式碼
以上程式碼可知,Request通過setTag的方式和View進行繫結,當View是複用時,則Request不為空,通過Request可對原來的資源進行快取與回收。此處通過View的setTag()方法繫結Request,可謂妙用。
以上程式碼建立了一個Request,requestTracker.runRequest(request);啟動了Request,呼叫Request的begin()方法,該Request例項是GenericRequest,begin()程式碼如下:
@Override
public void begin() {
startTime = LogTime.getLogTime();
if (model == null) {
onException(null);
return;
}
status = Status.WAITING_FOR_SIZE; //設定等待圖片size的寬高狀態
if (Util.isValidDimensions(overrideWidth, overrideHeight)) { //必須要確定圖片的寬高,確定了則呼叫onSizeReady
onSizeReady(overrideWidth, overrideHeight);
} else { //設定回撥,監聽介面的繪製,當檢測到寬高有效時,回撥onSizeReady方法
target.getSize(this);
}
if (!isComplete() && !isFailed() && canNotifyStatusChanged()) {
target.onLoadStarted(getPlaceholderDrawable());
}
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logV("finished run method in " + LogTime.getElapsedMillis(startTime));
}
}複製程式碼
載入圖片前,必須要確定圖片的寬高,因為需要根據確定的寬高來獲取資源。onSizeReady程式碼如下:
@Override
public void onSizeReady(int width, int height) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logV("Got onSizeReady in " + LogTime.getElapsedMillis(startTime));
}
if (status != Status.WAITING_FOR_SIZE) {//寬高沒準備好,返回
return;
}
status = Status.RUNNING; //狀態改為載入執行中
width = Math.round(sizeMultiplier * width);
height = Math.round(sizeMultiplier * height);
ModelLoader<A, T> modelLoader = loadProvider.getModelLoader();
final DataFetcher<T> dataFetcher = modelLoader.getResourceFetcher(model, width, height);
if (dataFetcher == null) {
onException(new Exception("Failed to load model: \'" + model + "\'"));
return;
}
ResourceTranscoder<Z, R> transcoder = loadProvider.getTranscoder();
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logV("finished setup for calling load in " + LogTime.getElapsedMillis(startTime));
}
loadedFromMemoryCache = true;
//真正的載入任務交給engine
loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder,
priority, isMemoryCacheable, diskCacheStrategy, this);
loadedFromMemoryCache = resource != null;
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime));
}
}複製程式碼
以上程式碼可知,在確定寬高後,將圖片載入的任務交給型別為Engine的物件engine,並呼叫其load方法,程式碼如下:
public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
Util.assertMainThread();
long startTime = LogTime.getLogTime();
final String id = fetcher.getId(); //該id為圖片的網路地址
//快取key的組成部分,使用工廠模式
EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
transcoder, loadProvider.getSourceEncoder());
//使用一級快取,從回收的記憶體快取中查詢EngineResource
EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
if (cached != null) { //命中則直接返回
cb.onResourceReady(cached);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from cache", startTime, key);
}
return null;
}
//從二級快取中查詢
EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
if (active != null) {//命中則直接返回
cb.onResourceReady(active);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return null;
}
EngineJob current = jobs.get(key);
if (current != null) {//該任務已經在執行,只需要新增回撥介面,在任務執行完後呼叫介面告知即可
current.addCallback(cb);
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Added to existing load", startTime, key);
}
return new LoadStatus(cb, current);
}
//一級快取和二級快取都不命中的情況下,啟動新的任務
EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);//建立EngineJob
DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
transcoder, diskCacheProvider, diskCacheStrategy, priority); //建立DecodeJob
EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
jobs.put(key, engineJob);
engineJob.addCallback(cb);
engineJob.start(runnable); //啟動EngineRunnable runnable,使用執行緒池FifoPriorityThreadPoolExecutor管理
if (Log.isLoggable(TAG, Log.VERBOSE)) {
logWithTimeAndKey("Started new load", startTime, key);
}
return new LoadStatus(cb, engineJob);
}複製程式碼
分析至此,我們終於看到實現一級快取和二級快取的相關程式碼,可以猜測三級快取的實現跟EngineRunnable有關。engineJob.start(runnable)會啟動EngineRunnable的start()方法。程式碼如下:
@Override
public void run() {
if (isCancelled) {
return;
}
Exception exception = null;
Resource<?> resource = null;
try {
resource = decode(); //呼叫decode()方法
} catch (Exception e) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Exception decoding", e);
}
exception = e;
}
if (isCancelled) { //請求被取消
if (resource != null) {
resource.recycle();
}
return;
}
if (resource == null) { //載入失敗
onLoadFailed(exception);
} else { //載入成功
onLoadComplete(resource);
}
}複製程式碼
檢視decode()方法如下:
private Resource<?> decode() throws Exception {
if (isDecodingFromCache()) {
return decodeFromCache(); //從磁碟快取中獲取
} else {
return decodeFromSource(); //從網路中獲取資源
}
}複製程式碼
至此,我們看到磁碟快取和網路請求獲取圖片資源的程式碼。檢視onLoadFailed()的程式碼邏輯可知,預設先從磁碟獲取,失敗則從網路獲取。
BitmapPool快取邏輯
以上就是Resource三層快取的程式碼,接下來看BitmapPool的快取實現程式碼。
在decodeFromSource()的程式碼中,會返回一個型別為BitmapResource的物件。在RecyclerView的例子中,當ImageView被複用時,會在Tag中取出Request,呼叫request.clear()程式碼。該方法最終會呼叫BitmapResource的recycler()方法,程式碼如下:
public void recycle() {
if (!bitmapPool.put(bitmap)) {
bitmap.recycle();
}
}複製程式碼
該程式碼呼叫bitmapPool.put(bitmap),bitmapPool的例項是LruBitmapPool程式碼如下:
public synchronized boolean put(Bitmap bitmap) {
if (bitmap == null) {
throw new NullPointerException("Bitmap must not be null");
}
if (!bitmap.isMutable() || strategy.getSize(bitmap) > maxSize || !allowedConfigs.contains(bitmap.getConfig())) {
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Reject bitmap from pool"
+ ", bitmap: " + strategy.logBitmap(bitmap)
+ ", is mutable: " + bitmap.isMutable()
+ ", is allowed config: " + allowedConfigs.contains(bitmap.getConfig()));
}
return false;
}
final int size = strategy.getSize(bitmap);
strategy.put(bitmap);//該strategy的例項是Lru演算法
tracker.add(bitmap); //log跟蹤
puts++; //快取的bitmap數量標記加一
currentSize += size;//快取bitmap的總大小
if (Log.isLoggable(TAG, Log.VERBOSE)) {
Log.v(TAG, "Put bitmap in pool=" + strategy.logBitmap(bitmap));
}
dump(); //僅用於Log
evict(); //判斷是否超出指定的記憶體大小,若超出則移除
return true;
}複製程式碼
可以看出,正常情況下呼叫put方法返回true,證明快取該Bitmap成功,快取成功則不呼叫bitmap.recycler()方法。當需要使用Bitmap時,先從Bitmap中查詢是否有符合條件的Bitmap。在RecyclerView中使用Glide的例子中,將大量複用寬高及Bitmap.Config都相等的Bitmap,極大的優化系統記憶體效能,減少頻繁的建立回收Bitmap。
小結
Glide的快取管理至此就分析完了,主要抓住Resource和Bitmap的快取來講解。在程式碼的閱讀中還發現了工廠、裝飾者等設計模式。Glide的解耦給開發者提供很大的便利性,可根據自身需求設定快取引數,例如預設Bitmap.Config、BitmapPool快取大小等。最後,針對Glide的快取設計,提出幾點小建議:
- Glide雖然預設使用的Bitmap.Config是RGB_565,但在進行transform(例如圓角顯示圖片)時往往預設是ARGB_8888,因為RGB_565沒有透明色,此時可重寫圓角變換的程式碼,繼續使用RGB_565,同時給canvas設定背景色。
- BitmapPool快取的Bitmap大小跟Bitmap的解析度也有關係,在載入圖片的過程中,可呼叫.override(width, height)指定圖片的寬高,再調整ImageView控制元件的大小適應佈局。
- Resource的一級快取和Bitmap都是記憶體快取,雖然極大的提升了複用,但也會導致部分記憶體在系統執行GC時無法釋放。若記憶體達到手機效能瓶頸,應在合適的時機呼叫Glide.get(this).clearMemory()釋放記憶體。