《看完不忘系列》之Glide (树干篇)一文对Glide
加载图片的核心流程做了介绍,细枝篇作为补充,将对一些具体实现细节进行深入。本文篇幅略大,大家可以根据目录索引到感兴趣的章节阅读~
源码基于最新版本4.11.0
,先上一张职责图预览下,一家人就要整整齐齐~
本文约3200字,阅读大约10分钟。如个别大图模糊(官方会压缩),可前往个人站点阅读
Generated API
通过创建一些类,继承相关接口,然后打上注解,由apt来处理这些类,从而实现接口扩展。
全局配置
注解@GlideModule
用来配置全局参数和注册定制的能力,在application里使用AppGlideModule
,在library里使用LibraryGlideModule
,
@GlideModule
public class MyAppGlideModule extends AppGlideModule {
@Override
public boolean isManifestParsingEnabled() {
return false;//新版本不需要解析manifest里的元数据(没用过老版本,不太懂,按文档返回false即可)
}
@Override
public void applyOptions(Context context, GlideBuilder builder) {
super.applyOptions(context, builder);
//全局配置
//builder.setBitmapPool(xxx);
//builder.setDefaultRequestOptions(xxx);
//...
}
@Override
public void registerComponents(Context context, Glide glide, Registry registry) {
super.registerComponents(context, glide, registry);
//注册一些定制的能力,比如扩展新的图片来源ModelLoader
//registry.register(xxx);
}
}
比如现在的Glide
的Bitmap默认配置是ARGB_8888
,如果项目图片类型比较单一,不需要透明度通道和高色域,可以配置全局的RGB_565
减少一半内存。见默认请求选项,
@GlideModule
public class MyAppGlideModule extends AppGlideModule {
@Override
public void applyOptions(@NonNull Context context, @NonNull GlideBuilder builder) {
super.applyOptions(context, builder);
builder.setDefaultRequestOptions(new RequestOptions()
.format(DecodeFormat.PREFER_RGB_565));
//注:由于png需要透明度通道,这类图依旧会采用8888
}
}
或者可以根据设备评分来衡量,普通机型配置RGB_565
(在需要透明度通道的场景局部使用ARGB_8888
),高端机型则可以直接配置ARGB_8888
,纵享奢华体验。
行为打包
注解@GlideExtension
可以将一些通用行为打包起来,扩展一个接口方便业务层调用。比如电商App很多页面都有商品列表,这些商品图片的宽高如果是固定的,就可以包装起来,
@GlideExtension
public class MyAppExtension {
private static final int GOODS_W = 300; //商品图宽度
private static final int GOODS_H = 400; //商品图高度
private MyAppExtension() { //私有化构造方法
}
@GlideOption
public static BaseRequestOptions<?> goods(BaseRequestOptions<?> options) {
return options
.fitCenter()
.override(GOODS_W, GOODS_H) //宽高
.placeholder(R.mipmap.ic_launcher) //商品占位图
.error(R.mipmap.ic_launcher); //商品图加载失败时
}
}
rebuild一下项目,生成类build/generated/ap_generated_sources/debug/out/com/holiday/srccodestudy/glide/GlideOptions.java
,里面会多出一个方法,
class GlideOptions extends RequestOptions implements Cloneable {
public GlideOptions goods() {
return (GlideOptions) MyAppExtension.goods(this);
}
}
这时,就可以用goods来直接使用这一组打包好的行为了,
//要用GlideApp
GlideApp.with(this).load(url).goods().into(img);
Generated API
比较适合短周期/小型项目,中大型项目往往不会直接裸使用Glide
,会包一个中间层来进行隔离(禁止业务层用到Glide
的任何类),以便随时可以升级替换,这个中间层就可以根据需要来自行扩展。
空Fragment取消请求
Glide.with(context),当context是Activity时,每个页面都会被添加一个空fragment,由空fragment持有页面级别RequestManager
来管理请求,那退出页面时是如何取消请求的呢?
with通过RequestManagerRetriever
获取SupportRequestManagerFragment
,
//SupportRequestManagerFragment.java
//创建SupportRequestManagerFragment
public SupportRequestManagerFragment() {
//创建Lifecycle
this(new ActivityFragmentLifecycle());
}
//RequestManager.java
//创建RequestManager,传入Lifecycle
RequestManager(
Glide glide,
Lifecycle lifecycle,
//...
Context context) {
//lifecycle添加RequestManager为观察者
lifecycle.addListener(this);
}
//ActivityFragmentLifecycle.java
public void addListener(LifecycleListener listener) {
//记录观察者们
lifecycleListeners.add(listener);
}
退出页面时,
//SupportRequestManagerFragment.java
public void onDestroy() {
lifecycle.onDestroy();
}
//ActivityFragmentLifecycle.java
void onDestroy() {
for (LifecycleListener lifecycleListener : Util.getSnapshot(lifecycleListeners)) {
lifecycleListener.onDestroy();
}
}
//RequestManager.java
public synchronized void onDestroy() {
//各种取消、注销操作
targetTracker.onDestroy();
for (Target<?> target : targetTracker.getAll()) {
clear(target);
}
targetTracker.clear();
requestTracker.clearRequests();
lifecycle.removeListener(this);
lifecycle.removeListener(connectivityMonitor);
mainHandler.removeCallbacks(addSelfToLifecycle);
glide.unregisterRequestManager(this);
}
代码看起来有点绕,大致如下图,
Cache缓存
内存
内存缓存有两级,一是处于活跃状态,正被view使用着的缓存,称活跃资源
;二是没被view使用的,就叫他非活跃资源
吧,
读取内存:
//Engine.java
public <R> LoadStatus load(...){
//获取内存缓存
memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);
}
private EngineResource<?> loadFromMemory(
EngineKey key, boolean isMemoryCacheable, long startTime) {
//活跃资源,从ActiveResources的Map中获取
//Map<Key, ResourceWeakReference> activeEngineResources,值是弱引用,会手动计数
EngineResource<?> active = loadFromActiveResources(key);
if (active != null) {
return active;
}
//非活跃资源,从LruResourceCache获取,也有手动计数
//返回后,说明这个缓存被view给用上了,非活跃资源则变成活跃
EngineResource<?> cached = loadFromCache(key);
if (cached != null) {
return cached;
}
//内存没有缓存,load就会去请求
return null;
}
写入内存:
//Engine.java
public synchronized void onEngineJobComplete(
EngineJob<?> engineJob, Key key, EngineResource<?> resource) {
if (resource != null && resource.isMemoryCacheable()) {
//简单理解,就是图片加载完成,这时写入活跃资源的
activeResources.activate(key, resource);
}
}
public void onResourceReleased(Key cacheKey, EngineResource<?> resource) {
//活跃资源已经没有被引用了,就移出
activeResources.deactivate(cacheKey);
if (resource.isMemoryCacheable()) {
//转入非活跃资源
cache.put(cacheKey, resource);
}
}
如下图:
磁盘
看看缓存目录/data/data/com.holiday.srccodestudy/cache/image_manager_disk_cache/
,
先看日志文件journal
,
libcore.io.DiskLruCache //头部名字
1 //磁盘缓存版本
1 //App版本
1 //每个entry(日志条目)存放的文件数,默认为1,即一个entry对应一个图片文件,比如下面就有4个entry,即4张图片
DIRTY 64d4b00d8ce8b0942d53b3048d5cf6aaa7173acd321e17420891bbc35b98629f
CLEAN 64d4b00d8ce8b0942d53b3048d5cf6aaa7173acd321e17420891bbc35b98629f 5246
DIRTY 2c23e32bd9b208092b3cbee8db6f1aff5bc11cb0d4ebd092604ee53099beff37
CLEAN 2c23e32bd9b208092b3cbee8db6f1aff5bc11cb0d4ebd092604ee53099beff37 404730
READ 64d4b00d8ce8b0942d53b3048d5cf6aaa7173acd321e17420891bbc35b98629f
READ 2c23e32bd9b208092b3cbee8db6f1aff5bc11cb0d4ebd092604ee53099beff37
DIRTY b566e62aa0e2fb8cb219ad3aa7a0ade9a96521526501ccd775d70aa4f6489272
CLEAN b566e62aa0e2fb8cb219ad3aa7a0ade9a96521526501ccd775d70aa4f6489272 9878
READ 2c23e32bd9b208092b3cbee8db6f1aff5bc11cb0d4ebd092604ee53099beff37
READ b566e62aa0e2fb8cb219ad3aa7a0ade9a96521526501ccd775d70aa4f6489272
DIRTY 55f4af9c1020e3272ce8063c351aff3518f3a1c9508f38345eab27686e263a4c
CLEAN 55f4af9c1020e3272ce8063c351aff3518f3a1c9508f38345eab27686e263a4c 69284
下半部分是操作记录,行开头指操作行为,DIRTY
表示在编辑(处于脏数据状态,别读),CLEAN
(干净状态)表示写好了,可以读了,READ
表示被读入了,REMOVE
则表示被删除,中间很长的一串字符就是缓存键或文件名字,最后的数字是文件大小,如404730 B=395.2 KB,只有处于CLEAN
状态才会写大小。那么图中的文件名是什么意思,为啥key的后面还有.0
后缀?因为一个entry
(日志条目)可以对应多个图片,.0
代表entry
的第一张图片,如果有配置1对多,那就会有.1
、.2
这样的后缀。选一个.0
文件点击右键,Save as
保存到电脑,改个jpg后缀,就能看图了。
来到DiskLruCache
类(看名字知道还是最近最少使用算法
),
//DiskLruCache.java
//有序Map,实现最近最少使用算法
private final LinkedHashMap<String, Entry> lruEntries =
new LinkedHashMap<String, Entry>(0, 0.75f, true);
//读取磁盘缓存
public synchronized Value get(String key) throws IOException {
//根据key找到entry
Entry entry = lruEntries.get(key);
if (entry == null) {
return null;
}
//还不可以读,返回null
if (!entry.readable) {
return null;
}
//追加一行日志:READ
journalWriter.append(READ);
journalWriter.append(' ');
journalWriter.append(key);
journalWriter.append('\n');
//Value就是用来封装的实体
return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);
}
//写入磁盘缓存(这里只是存进内存的Map,真正的写入在DiskLruCacheWrapper)
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
Entry entry = lruEntries.get(key);
if (entry == null) {
entry = new Entry(key);
//存进LinkedHashMap
lruEntries.put(key, entry);
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
//追加一行日志:DIRTY
journalWriter.append(DIRTY);
return editor;
}
//删除磁盘缓存
public synchronized boolean remove(String key) throws IOException {
Entry entry = lruEntries.get(key);
if (entry == null || entry.currentEditor != null) {
return false;
}
//删除entry对应的图片文件
for (int i = 0; i < valueCount; i++) {
File file = entry.getCleanFile(i);
size -= entry.lengths[i];
entry.lengths[i] = 0;
}
//追加一行日志:REMOVE
journalWriter.append(REMOVE);
//从内存Map中移除
lruEntries.remove(key);
return true;
}
//当日志操作数和entry数都达到2000,就清空日志重写
private boolean journalRebuildRequired() {
final int redundantOpCompactThreshold = 2000;
return redundantOpCount >= redundantOpCompactThreshold //
&& redundantOpCount >= lruEntries.size();
}
那么读取和写入时机在哪呢?我们反向追踪一波get
方法,从DiskLruCache
到DiskLruCacheWrapper
的get
,然后再追,发现有两个类调了get
,分别是DataCacheGenerator
和ResourceCacheGenerator
,前者是原始图片的缓存,后者是经过downsampled
向下采样或transformed
转换过的图片,在磁盘缓存策略中提到:
目前支持的策略允许你阻止加载过程使用或写入磁盘缓存,选择性地仅缓存无修改的原生数据,或仅缓存变换过的缩略图,或是兼而有之。
默认情况下,网络图片缓存的是原始数据,那我们继续跟DataCacheGenerator
,
//DataCacheGenerator.java
public boolean startNext() {
while (modelLoaders == null || !hasNextModelLoader()) {
sourceIdIndex++;
if (sourceIdIndex >= cacheKeys.size()) {
return false;
}
Key sourceId = cacheKeys.get(sourceIdIndex);
Key originalKey = new DataCacheKey(sourceId, helper.getSignature());
//获取磁盘缓存的图片文件
cacheFile = helper.getDiskCache().get(originalKey);
if (cacheFile != null) {
this.sourceKey = sourceId;
//获取能够处理File类型的modelLoaders集合,
//modelLoader就是图片加载类型,比如网络url、本地Uri、文件File都有各自的loader
modelLoaders = helper.getModelLoaders(cacheFile);
modelLoaderIndex = 0;
}
}
loadData = null;
boolean started = false;
while (!started && hasNextModelLoader()) {
//成功找到ByteBufferFileLoader,可以处理File
ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);
//传入磁盘缓存的图片文件cacheFile
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;
}
继续跟modelLoader.buildLoadData
,后边就是把图片文件cacheFile封装成ByteBufferFetcher
,然后调用上边的loadData.fetcher.loadData
进行回调,就不继续跟了,startNext
方法在DecodeJob
里会被调用,树干篇中可知他就是图片加载过程用到的一个Runnable,好了,下面看看缓存写入时机,反向追踪edit
方法,
//DiskLruCacheWrapper.java
public void put(Key key, Writer writer) {
String safeKey = safeKeyGenerator.getSafeKey(key);
writeLocker.acquire(safeKey);
try {
try {
DiskLruCache diskCache = getDiskCache();
Value current = diskCache.get(safeKey);
//已经有缓存,结束
if (current != null) {
return;
}
//获取Editor
DiskLruCache.Editor editor = diskCache.edit(safeKey);
try {
File file = editor.getFile(0);
if (writer.write(file)) {//编码写入文件
//提交“事务”,追加一行日志:CLEAN,表示该条目对应的缓存文件已经干净可以使用了
editor.commit();
}
} finally {
editor.abortUnlessCommitted();
}
} catch (IOException e) {
}
} finally {
writeLocker.release(safeKey);
}
}
同样,put
方法也会在DecodeJob
里被调用,就不往上跟了。
合并内存缓存和磁盘缓存,
BitmapPool令人诟病
Glide
有将Bitmap进行池化,默认是LruBitmapPool
,他会决定怎么复用Bitmap、何时回收Bitmap、池子上限时清理,也就是说,他全盘接管了Bitmap的处理,如果项目中有在回调方法外持有Bitmap
、手动回收Bitmap
的场景,会发生意料外的crash,详见资源重用错误的征兆。即,我们要有这样的意识,既然使用了Glide
,就不要再关心Bitmap的事情了,全盘交由BitmapPool
管理即可。
发散:所谓池化,就是设计模式中的享元模式,即维护一个有限个数的对象池来实现对象复用,从而避免频繁的创建销毁对象。比如Handler消息机制中的
Message.obtain
,就是从消息池(链表)里取出对象来复用,池子的消息总数被限制在MAX_POOL_SIZE=50。Android内的很多实现都是基于Handler(消息驱动)的,池化能减少很大部分的创建销毁。
Decoder解码
链路有点长,直接看调用栈,
可见最终走的是native层的nativeDecodeStream
,哈迪就不跟了,对inputstream转成bitmap感兴趣的读者自行研究啦~
总结
Glide
有如下优势:
- 空Fragment感知页面生命周期,避免无效请求
- 高度可配置,详见配置
- 三级缓存(网络层缓存如okhttp就不考虑了):内存活跃资源
ActiveResources
、内存非活跃资源LruResourceCache
、磁盘缓存DiskLruCache
- 可定制,引入apt处理注解,打包行为,扩展接口。(哈迪没怎么用,感觉有点鸡肋,可能以后会真香)
- 可扩展,可以替换网络层、定制自己的图片来源
ModelLoader
,详见编写定制的ModelLoader - 无侵入,into可以传入最简单的ImageView
- 优秀的设计模式运用、应用层优雅的链式调用
至于缺点吧,暂时还没想到。本文只列出了哈迪觉得比较精彩的细节,可能还有遗漏的一些点,大家有补充的可以留下评论,后续我会更新进本文。