先看下Glide官方文檔對圖片加載性能優(yōu)化的兩個(gè)方面:
- 圖片解碼速度
- 解碼圖片帶來的資源壓力
主要采用的步驟如下:
- 自動汽馋、智能地下采樣(downsampling)和緩存(caching)突倍,以最小化存儲開銷和解碼次數(shù)甲捏;
- 積極的資源重用鞋怀,例如字節(jié)數(shù)組和Bitmap静汤,以最小化昂貴的垃圾回收和堆碎片影響蒂誉;
- 深度的生命周期集成虱肄,以確保僅優(yōu)先處理活躍的Fragment和Activity的請求,并有利于應(yīng)用在必要時(shí)釋放資源以避免在后臺時(shí)被殺掉抖仅。
Glide緩存機(jī)制說明
官方文檔已經(jīng)有了詳細(xì)的使用說明
https://muyangmin.github.io/glide-docs-cn/doc/caching.html
多級緩存邏輯
多級緩存
- 活動資源 (Active Resources) - 現(xiàn)在是否有另一個(gè) View 正在展示這張圖片坊夫?
- 內(nèi)存緩存 (Memory cache) - 該圖片是否最近被加載過并仍存在于內(nèi)存中?
- 資源類型(Resource) - 該圖片是否之前曾被解碼撤卢、轉(zhuǎn)換并寫入過磁盤緩存环凿?
- 數(shù)據(jù)來源 (Data) - 構(gòu)建這個(gè)圖片的資源是否之前曾被寫入過文件緩存?
前兩步檢查圖片是否在內(nèi)存中放吩,如果是則直接返回圖片智听。后兩步則檢查圖片是否在磁盤上,以便快速但異步地返回圖片渡紫。如果四個(gè)步驟都未能找到圖片到推,則Glide會返回到原始資源以取回?cái)?shù)據(jù)(原始文件,Uri, Url等)惕澎。
緩存策略
內(nèi)存緩存策略
內(nèi)存中會緩存上述的活動資源 和 內(nèi)存緩存資源莉测;
活動資源采用采取了HashMap進(jìn)行弱引用進(jìn)行存儲;
內(nèi)存緩存資源采用LRU緩存進(jìn)行存儲集灌;
硬盤緩存策略類型(見DiskCacheStrategy):
- DiskCacheStrategy.DATA:磁盤寫入數(shù)據(jù)為未被加載過程修改過原始數(shù)據(jù)悔雹;
- DiskCacheStrategy.RESOURCE: 將解碼复哆,變換后的資源寫入磁盤;
- DiskCacheStrategy.ALL:遠(yuǎn)程的資源(URL資源)會寫入原始資源和變換后的資源腌零;本地文件資源只會寫入解碼變換后的資源梯找;
- DiskCacheStrategy.NONE: 不寫入任何數(shù)據(jù)到硬盤;
- DiskCacheStrategy.AUTOMATIC:它會嘗試對本地和遠(yuǎn)程圖片使用最佳的策略益涧。當(dāng)你加載遠(yuǎn)程數(shù)據(jù)(比如锈锤,從URL下載)時(shí),
AUTOMATIC
策略僅會存儲未被你的加載過程修改過(比如闲询,變換)的原始數(shù)據(jù)久免,因?yàn)橄螺d遠(yuǎn)程數(shù)據(jù)相比調(diào)整磁盤上已經(jīng)存在的數(shù)據(jù)要昂貴得多。對于本地?cái)?shù)據(jù)扭弧,AUTOMATIC
策略則會僅存儲變換過的縮略圖阎姥,因?yàn)榧词鼓阈枰俅紊闪硪粋€(gè)尺寸或類型的圖片,取回原始數(shù)據(jù)也很容易鸽捻。
緩存鍵值
不同于以往的緩存鍵值僅以URL作為唯一標(biāo)識呼巴,Glide 針對不同緩存場景構(gòu)建了不同的key,活動資源和內(nèi)存緩存使用的鍵還和磁盤資源緩存略有不同御蒲,以適應(yīng)內(nèi)存 衣赶,選項(xiàng),比如影響 Bitmap 配置的選項(xiàng)或其他解碼時(shí)才會用到的參數(shù)厚满。
以內(nèi)存鍵值為例府瞄,加入請求的資源的圖片大小發(fā)生了變化,則無法命中緩存中的key碘箍,需要去磁盤中或者網(wǎng)絡(luò)中重新獲取后進(jìn)行變化遵馆。
我們看下相關(guān)Key的構(gòu)造方法;
EngineKey(
Object model,
Key signature,
int width,
int height,
Map<Class<?>, Transformation<?>> transformations,
Class<?> resourceClass,
Class<?> transcodeClass,
Options options)
ResourceCacheKey(
ArrayPool arrayPool,
Key sourceKey,
Key signature,
int width,
int height,
Transformation<?> appliedTransformation,
Class<?> decodedResourceClass,
Options options)
DataCacheKey(Key sourceKey, Key signature)
在 Glide v4 里敲街,所有緩存鍵都包含至少兩個(gè)元素:
- 請求加載的 model(File, Url, Url)团搞。如果你使用自定義的 model, 它需要正確地實(shí)現(xiàn)
hashCode()
和equals()
- 一個(gè)可選的
簽名
(Signature)
另外,步驟1-3(活動資源多艇,內(nèi)存緩存,資源磁盤緩存)的緩存鍵還包含一些其他數(shù)據(jù)像吻,包括: - 寬度和高度
- 可選的
變換(Transformation)
- 額外添加的任何
選項(xiàng)(Options)
- 請求的數(shù)據(jù)類型 (Bitmap, GIF, 或其他)
修改默認(rèn)緩存配置
當(dāng)然Glide也提供了修改默認(rèn)的緩存配置項(xiàng)的方式;
//更改內(nèi)存緩存配置大小(當(dāng)然邮屁,可以定制自己的緩存)
@GlideModule
public class YourAppGlideModule extends AppGlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
int memoryCacheSizeBytes = 1024 * 1024 * 20; // 20mb
builder.setMemoryCache(new LruResourceCache(memoryCacheSizeBytes));
}
}
以及使用加載某個(gè)圖片的時(shí)候配置緩存策略:
Glide.with(fragment)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(imageView);
Glide緩存源碼分析
正常加載一張圖片的常規(guī)操作如下:
RequestBuilder
我們進(jìn)入RequestBuilder看下into(imageView)方法
第一步做了一個(gè)requestOptions的重新賦值琐鲁,大概的邏輯是requestOptions會隨著ImageView設(shè)置的scaleType重新賦值。
然后進(jìn)入:
into(
glideContext.buildImageViewTarget(view, transcodeClass),
/*targetListener=*/ null,
requestOptions,
Executors.mainThreadExecutor())
然后通過buildRequest創(chuàng)建一個(gè)請求惭每,并進(jìn)行
private <Y extends Target<TranscodeType>> Y into(
@NonNull Y target,
@Nullable RequestListener<TranscodeType> targetListener,
BaseRequestOptions<?> options,
Executor callbackExecutor) {
Preconditions.checkNotNull(target);
if (!isModelSet) {
throw new IllegalArgumentException("You must call #load() before calling #into()");
}
//1. 創(chuàng)建一個(gè)請求
Request request = buildRequest(target, targetListener, options, callbackExecutor);
Request previous = target.getRequest();
if (request.isEquivalentTo(previous)
&& !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {
//如果兩次請求為相同請求骨饿,上一個(gè)請求如果失敗了亏栈,則重新去請求
// 如果上一次已經(jīng)正在運(yùn)行了,將繼續(xù)運(yùn)行上一次請求宏赘,不去打斷他绒北。
if (!Preconditions.checkNotNull(previous).isRunning()) {
//這里是判斷上一個(gè)相同的請求如果沒有運(yùn)行,就直接運(yùn)行開始上一次的請求任務(wù)察署。
// 這樣可以優(yōu)化一些像設(shè)置站維護(hù)闷游,記錄,獲取圖片信息等一次請求中需要的操作
previous.begin();
}
return target;
}
requestManager.clear(target);
target.setRequest(request);
// 開始執(zhí)行請求
requestManager.track(target, request);
return target;
}
SingleRequest
最終通過requestManager.track(target, request)調(diào)用RequestTracker.runRequest贴汪,通過SingleRequest.begin脐往,SingleRequest.onSizeReady方法進(jìn)入Engine.load方法;
我們重點(diǎn)研究一下Engine.load方法
public <R> LoadStatus load(
GlideContext glideContext,
Object model,
Key signature,
int width,
int height,
Class<?> resourceClass,
Class<R> transcodeClass,
Priority priority,
DiskCacheStrategy diskCacheStrategy,
Map<Class<?>, Transformation<?>> transformations,
boolean isTransformationRequired,
boolean isScaleOnlyOrNoTransform,
Options options,
boolean isMemoryCacheable,
boolean useUnlimitedSourceExecutorPool,
boolean useAnimationPool,
boolean onlyRetrieveFromCache,
ResourceCallback cb,
Executor callbackExecutor) {
long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;
//第一步扳埂,生成原始的緩存key
EngineKey key =
keyFactory.buildKey(
model,
signature,
width,
height,
transformations,
resourceClass,
transcodeClass,
options);
EngineResource<?> memoryResource;
synchronized (this) {
//第二步业簿,從內(nèi)存中讀取資源
memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
if (memoryResource == null) {
//第三步,開啟一個(gè)新的任務(wù):從磁盤中讀取或者從網(wǎng)絡(luò)中讀取需要的資源阳懂。
return waitForExistingOrStartNewJob(
glideContext,
model,
signature,
width,
height,
resourceClass,
transcodeClass,
priority,
diskCacheStrategy,
transformations,
isTransformationRequired,
isScaleOnlyOrNoTransform,
options,
isMemoryCacheable,
useUnlimitedSourceExecutorPool,
useAnimationPool,
onlyRetrieveFromCache,
cb,
callbackExecutor,
key,
startTime);
}
}
cb.onResourceReady(memoryResource, DataSource.MEMORY_CACHE);
return null;
}
第一步:生成緩存的key辖源,這里會根據(jù)上面所說的,模型希太,簽名克饶,長寬,變換以及原始資源誊辉,變換后的資源矾湃,以及選項(xiàng)創(chuàng)建一個(gè)key。這個(gè)是原始的key堕澄,后續(xù)如果執(zhí)行到磁盤緩存會根據(jù)原生key生成新的key邀跃。
第二步:從內(nèi)存中嘗試獲取資源文件,內(nèi)存中分為兩部分:活動資源以及內(nèi)存緩存資源蛙紫。
@Nullable
private EngineResource<?> loadFromMemory(
EngineKey key, boolean isMemoryCacheable, long startTime) {
if (!isMemoryCacheable) {
return null;
}
//活動資源
EngineResource<?> active = loadFromActiveResources(key);
if (active != null) {
if (VERBOSE_IS_LOGGABLE) {
logWithTimeAndKey("Loaded resource from active resources", startTime, key);
}
return active;
}
//內(nèi)存緩存資源
EngineResource<?> cached = loadFromCache(key);
if (cached != null) {
return cached;
}
return null;
}
活動資源
活動資源最終實(shí)現(xiàn)是通過ActiveResources拍屑。內(nèi)部維護(hù)了一個(gè)Map<Key, ResourceWeakReference> ,也就是一個(gè)弱引用的hashMap坑傅,將活動資源存儲在hashmap的弱引用中僵驰。
@VisibleForTesting final Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
synchronized void activate(Key key, EngineResource<?> resource) {
ResourceWeakReference toPut =
new ResourceWeakReference(
key, resource, resourceReferenceQueue, isActiveResourceRetentionAllowed);
ResourceWeakReference removed = activeEngineResources.put(key, toPut);
if (removed != null) {
removed.reset();
}
}
內(nèi)存緩存
private EngineResource<?> getEngineResourceFromCache(Key key) {
Resource<?> cached = cache.remove(key);
final EngineResource<?> result;
if (cached == null) {
result = null;
} else if (cached instanceof EngineResource) {
// Save an object allocation if we've cached an EngineResource (the typical case).
result = (EngineResource<?>) cached;
} else {
result =
new EngineResource<>(
cached, /*isMemoryCacheable=*/ true, /*isRecyclable=*/ true, key, /*listener=*/ this);
}
return result;
}
其中關(guān)鍵實(shí)現(xiàn)為MemoryCache 定義的cache,MemoryCache 的具體實(shí)現(xiàn)類為LruResourceCache唁毒。LruResourceCache 集成了LruCache蒜茴,就是實(shí)現(xiàn)了LRU算法來維護(hù)內(nèi)存緩存數(shù)據(jù)。
第三步浆西,開啟一個(gè)新的任務(wù):從磁盤中讀取或者從網(wǎng)絡(luò)中讀取需要的資源粉私。
通過waitForExistingOrStartNewJob 進(jìn)入 engineJob.start(decodeJob);
decodeJob 實(shí)現(xiàn)了runnable接口,engineJob實(shí)現(xiàn)內(nèi)部通過線程池去執(zhí)行decodeJob
看下DecodeJob 的run -> runWrapped -> runGenerators
private void runGenerators() {
currentThread = Thread.currentThread();
startFetchTime = LogTime.getLogTime();
boolean isStarted = false;
while (!isCancelled
&& currentGenerator != null
&& !(isStarted = currentGenerator.startNext())) {
stage = getNextStage(stage);
currentGenerator = getNextGenerator();
if (stage == Stage.SOURCE) {
reschedule();
return;
}
}
// We've run out of stages and generators, give up.
if ((stage == Stage.FINISHED || isCancelled) && !isStarted) {
notifyFailed();
}
}
其中currentGenerator.startNext() 方法為真正執(zhí)行加載近零,我們來看下currentGenerator 有哪些具體實(shí)現(xiàn)類诺核。
下面為如果根據(jù)緩存策略返回當(dāng)前的階段抄肖,后面會根據(jù)當(dāng)前階段生成對應(yīng)的生成器。
private Stage getNextStage(Stage current) {
switch (current) {
case INITIALIZE:
return diskCacheStrategy.decodeCachedResource()
? Stage.RESOURCE_CACHE
: getNextStage(Stage.RESOURCE_CACHE);
case RESOURCE_CACHE:
return diskCacheStrategy.decodeCachedData()
? Stage.DATA_CACHE
: getNextStage(Stage.DATA_CACHE);
case DATA_CACHE:
// Skip loading from source if the user opted to only retrieve the resource from cache.
return onlyRetrieveFromCache ? Stage.FINISHED : Stage.SOURCE;
case SOURCE:
case FINISHED:
return Stage.FINISHED;
default:
throw new IllegalArgumentException("Unrecognized stage: " + current);
}
}
三種生產(chǎn)器會根據(jù)當(dāng)前的階段進(jìn)行創(chuàng)建
- (變換后的)資源緩存文件對應(yīng)ResourceCacheGenerator
- 沒有被變換后的元數(shù)據(jù)緩存文件對應(yīng)DataCacheGenerator
- 源數(shù)據(jù)數(shù)據(jù)(網(wǎng)絡(luò)或者本地文件)對應(yīng)的SourceGenerator
大概邏輯就是窖杀,先從本地資源緩存文件查找有沒有需要的資源漓摩,如果沒有找到,則去查找元數(shù)據(jù)緩存陈瘦,如果還是沒有就去源數(shù)據(jù)去加載幌甘,并將當(dāng)前的數(shù)據(jù)寫入磁盤緩存。
private DataFetcherGenerator getNextGenerator() {
switch (stage) {
case RESOURCE_CACHE:
return new ResourceCacheGenerator(decodeHelper, this);
case DATA_CACHE:
return new DataCacheGenerator(decodeHelper, this);
case SOURCE:
return new SourceGenerator(decodeHelper, this);
case FINISHED:
return null;
default:
throw new IllegalStateException("Unrecognized stage: " + stage);
}
}
ResourceCacheGenerator
public boolean startNext() {
···省略代碼
currentKey =
new ResourceCacheKey( // NOPMD AvoidInstantiatingObjectsInLoops
helper.getArrayPool(),
sourceId,
helper.getSignature(),
helper.getWidth(),
helper.getHeight(),
transformation,
resourceClass,
helper.getOptions());
// 根據(jù)硬盤緩存判斷是否存在痊项,具體實(shí)現(xiàn)為DiskLruCache
cacheFile = helper.getDiskCache().get(currentKey);
if (cacheFile != null) {
sourceKey = sourceId;
modelLoaders = helper.getModelLoaders(cacheFile);
modelLoaderIndex = 0;
}
}
loadData = null;
boolean started = false;
while (!started && hasNextModelLoader()) {
ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
loadData =
modelLoader.buildLoadData(
cacheFile, helper.getWidth(), helper.getHeight(), helper.getOptions());
if (loadData != null && helper.hasLoadPath(loadData.fetcher.getDataClass())) {
started = true;
loadData.fetcher.loadData(helper.getPriority(), this);
}
}
return started;
}
這里會先從DiskCache嘗試獲取锅风,默認(rèn)的實(shí)現(xiàn)是DiskLruCache,在這里進(jìn)行讀取鞍泉,然后在資源編碼完成之后皱埠,存入磁盤。
DataCacheGenerator
DataCacheGenerator.startNext的邏輯和ResourceCacheGenerator基本相同咖驮。
第一步边器,創(chuàng)建DataCacheKey,這里和ResourceCacheGenerator生成Key方式不太一致托修。
第二步忘巧,嘗試通過DiskCache獲取元數(shù)據(jù)緩存
第三方,根據(jù)modelLoader加載數(shù)據(jù)
SourceGenerator
開始嘗試去加載源數(shù)據(jù)睦刃,主要通過DataFetcher實(shí)現(xiàn)砚嘴,DataFetcher會根據(jù)數(shù)據(jù)源類型進(jìn)行選擇合適的進(jìn)行調(diào)用。
private void startNextLoad(final LoadData<?> toStart) {
loadData.fetcher.loadData(
helper.getPriority(),
new DataCallback<Object>() {
@Override
public void onDataReady(@Nullable Object data) {
if (isCurrentRequest(toStart)) {
onDataReadyInternal(toStart, data);
}
}
@Override
public void onLoadFailed(@NonNull Exception e) {
if (isCurrentRequest(toStart)) {
onLoadFailedInternal(toStart, e);
}
}
});
}
DataFetcher 實(shí)現(xiàn)類如下圖所示涩拙。
我們進(jìn)入HttpUrlFetcher 看下具體實(shí)現(xiàn)际长,這里我們看到loadData方法內(nèi)部的loadDataWithRedirects方法進(jìn)入真正的文件網(wǎng)絡(luò)數(shù)據(jù)讀取。
private InputStream loadDataWithRedirects(
URL url, int redirects, URL lastUrl, Map<String, String> headers) throws IOException {
urlConnection = connectionFactory.build(url);
for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());
}
urlConnection.setConnectTimeout(timeout);
urlConnection.setReadTimeout(timeout);
urlConnection.setUseCaches(false);
urlConnection.setDoInput(true);
final int statusCode = urlConnection.getResponseCode();
if (isHttpOk(statusCode)) {
return getStreamForSuccessfulRequest(urlConnection);
} else if (isHttpRedirect(statusCode)) {
String redirectUrlString = urlConnection.getHeaderField("Location");
if (TextUtils.isEmpty(redirectUrlString)) {
throw new HttpException("Received empty or null redirect url");
}
URL redirectUrl = new URL(url, redirectUrlString);
// Closing the stream specifically is required to avoid leaking ResponseBodys in addition
// to disconnecting the url connection below. See #2352.
cleanup();
return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers);
} else if (statusCode == INVALID_STATUS_CODE) {
throw new HttpException(statusCode);
} else {
throw new HttpException(urlConnection.getResponseMessage(), statusCode);
}
}
最終通過onDataFetcherReady 回調(diào)DecodeJob里面兴泥,然后通過LoadPath.load方法進(jìn)行解碼工育,最終通過ResourceTranscoder 轉(zhuǎn)碼成我們需要的BitmapResource、BitmapDrawableResource搓彻、FileResource等如绸。