先看下Glide官方文档对图片加载性能优化的两个方面:
- 图片解码速度
- 解码图片带来的资源压力
主要采用的步骤如下:
- 自动、智能地下采样(downsampling)和缓存(caching),以最小化存储开销和解码次数;
- 积极的资源重用,例如字节数组和Bitmap,以最小化昂贵的垃圾回收和堆碎片影响;
- 深度的生命周期集成,以确保仅优先处理活跃的Fragment和Activity的请求,并有利于应用在必要时释放资源以避免在后台时被杀掉。
Glide缓存机制说明
官方文档已经有了详细的使用说明
https://muyangmin.github.io/glide-docs-cn/doc/caching.html
多级缓存逻辑
多级缓存
- 活动资源 (Active Resources) - 现在是否有另一个 View 正在展示这张图片?
- 内存缓存 (Memory cache) - 该图片是否最近被加载过并仍存在于内存中?
- 资源类型(Resource) - 该图片是否之前曾被解码、转换并写入过磁盘缓存?
- 数据来源 (Data) - 构建这个图片的资源是否之前曾被写入过文件缓存?
前两步检查图片是否在内存中,如果是则直接返回图片。后两步则检查图片是否在磁盘上,以便快速但异步地返回图片。如果四个步骤都未能找到图片,则Glide会返回到原始资源以取回数据(原始文件,Uri, Url等)。
缓存策略
内存缓存策略
内存中会缓存上述的活动资源 和 内存缓存资源;
活动资源采用采取了HashMap进行弱引用进行存储;
内存缓存资源采用LRU缓存进行存储;
硬盘缓存策略类型(见DiskCacheStrategy):
- DiskCacheStrategy.DATA:磁盘写入数据为未被加载过程修改过原始数据;
- DiskCacheStrategy.RESOURCE: 将解码,变换后的资源写入磁盘;
- DiskCacheStrategy.ALL:远程的资源(URL资源)会写入原始资源和变换后的资源;本地文件资源只会写入解码变换后的资源;
- DiskCacheStrategy.NONE: 不写入任何数据到硬盘;
- DiskCacheStrategy.AUTOMATIC:它会尝试对本地和远程图片使用最佳的策略。当你加载远程数据(比如,从URL下载)时,
AUTOMATIC
策略仅会存储未被你的加载过程修改过(比如,变换)的原始数据,因为下载远程数据相比调整磁盘上已经存在的数据要昂贵得多。对于本地数据,AUTOMATIC
策略则会仅存储变换过的缩略图,因为即使你需要再次生成另一个尺寸或类型的图片,取回原始数据也很容易。
缓存键值
不同于以往的缓存键值仅以URL作为唯一标识,Glide 针对不同缓存场景构建了不同的key,活动资源和内存缓存使用的键还和磁盘资源缓存略有不同,以适应内存 ,选项,比如影响 Bitmap 配置的选项或其他解码时才会用到的参数。
以内存键值为例,加入请求的资源的图片大小发生了变化,则无法命中缓存中的key,需要去磁盘中或者网络中重新获取后进行变化。
我们看下相关Key的构造方法;
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 里,所有缓存键都包含至少两个元素:
- 请求加载的 model(File, Url, Url)。如果你使用自定义的 model, 它需要正确地实现
hashCode()
和equals()
- 一个可选的
签名
(Signature)
另外,步骤1-3(活动资源,内存缓存,资源磁盘缓存)的缓存键还包含一些其他数据,包括: - 宽度和高度
- 可选的
变换(Transformation)
- 额外添加的任何
选项(Options)
- 请求的数据类型 (Bitmap, GIF, 或其他)
修改默认缓存配置
当然Glide也提供了修改默认的缓存配置项的方式;
//更改内存缓存配置大小(当然,可以定制自己的缓存)
@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));
}
}
以及使用加载某个图片的时候配置缓存策略:
Glide.with(fragment)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.ALL)
.into(imageView);
Glide缓存源码分析
正常加载一张图片的常规操作如下:
RequestBuilder
我们进入RequestBuilder看下into(imageView)方法
第一步做了一个requestOptions的重新赋值,大概的逻辑是requestOptions会随着ImageView设置的scaleType重新赋值。
然后进入:
into(
glideContext.buildImageViewTarget(view, transcodeClass),
/*targetListener=*/ null,
requestOptions,
Executors.mainThreadExecutor())
然后通过buildRequest创建一个请求,并进行
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. 创建一个请求
Request request = buildRequest(target, targetListener, options, callbackExecutor);
Request previous = target.getRequest();
if (request.isEquivalentTo(previous)
&& !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {
//如果两次请求为相同请求,上一个请求如果失败了,则重新去请求
// 如果上一次已经正在运行了,将继续运行上一次请求,不去打断他。
if (!Preconditions.checkNotNull(previous).isRunning()) {
//这里是判断上一个相同的请求如果没有运行,就直接运行开始上一次的请求任务。
// 这样可以优化一些像设置站维护,记录,获取图片信息等一次请求中需要的操作
previous.begin();
}
return target;
}
requestManager.clear(target);
target.setRequest(request);
// 开始执行请求
requestManager.track(target, request);
return target;
}
SingleRequest
最终通过requestManager.track(target, request)调用RequestTracker.runRequest,通过SingleRequest.begin,SingleRequest.onSizeReady方法进入Engine.load方法;
我们重点研究一下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) {
//第二步,从内存中读取资源
memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
if (memoryResource == null) {
//第三步,开启一个新的任务:从磁盘中读取或者从网络中读取需要的资源。
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,这里会根据上面所说的,模型,签名,长宽,变换以及原始资源,变换后的资源,以及选项创建一个key。这个是原始的key,后续如果执行到磁盘缓存会根据原生key生成新的key。
第二步:从内存中尝试获取资源文件,内存中分为两部分:活动资源以及内存缓存资源。
@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;
}
//内存缓存资源
EngineResource<?> cached = loadFromCache(key);
if (cached != null) {
return cached;
}
return null;
}
活动资源
活动资源最终实现是通过ActiveResources。内部维护了一个Map<Key, ResourceWeakReference> ,也就是一个弱引用的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();
}
}
内存缓存
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;
}
其中关键实现为MemoryCache 定义的cache,MemoryCache 的具体实现类为LruResourceCache。LruResourceCache 集成了LruCache,就是实现了LRU算法来维护内存缓存数据。
第三步,开启一个新的任务:从磁盘中读取或者从网络中读取需要的资源。
通过waitForExistingOrStartNewJob 进入 engineJob.start(decodeJob);
decodeJob 实现了runnable接口,engineJob实现内部通过线程池去执行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() 方法为真正执行加载,我们来看下currentGenerator 有哪些具体实现类。
下面为如果根据缓存策略返回当前的阶段,后面会根据当前阶段生成对应的生成器。
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);
}
}
三种生产器会根据当前的阶段进行创建
- (变换后的)资源缓存文件对应ResourceCacheGenerator
- 没有被变换后的元数据缓存文件对应DataCacheGenerator
- 源数据数据(网络或者本地文件)对应的SourceGenerator
大概逻辑就是,先从本地资源缓存文件查找有没有需要的资源,如果没有找到,则去查找元数据缓存,如果还是没有就去源数据去加载,并将当前的数据写入磁盘缓存。
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());
// 根据硬盘缓存判断是否存在,具体实现为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尝试获取,默认的实现是DiskLruCache,在这里进行读取,然后在资源编码完成之后,存入磁盘。
DataCacheGenerator
DataCacheGenerator.startNext的逻辑和ResourceCacheGenerator基本相同。
第一步,创建DataCacheKey,这里和ResourceCacheGenerator生成Key方式不太一致。
第二步,尝试通过DiskCache获取元数据缓存
第三方,根据modelLoader加载数据
SourceGenerator
开始尝试去加载源数据,主要通过DataFetcher实现,DataFetcher会根据数据源类型进行选择合适的进行调用。
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 实现类如下图所示。
我们进入HttpUrlFetcher 看下具体实现,这里我们看到loadData方法内部的loadDataWithRedirects方法进入真正的文件网络数据读取。
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 回调DecodeJob里面,然后通过LoadPath.load方法进行解码,最终通过ResourceTranscoder 转码成我们需要的BitmapResource、BitmapDrawableResource、FileResource等。