Android | Glide细枝篇
《看完不忘系列》之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
- 优秀的设计模式运用、应用层优雅的链式调用
至于缺点吧,暂时还没想到。本文只列出了哈迪觉得比较精彩的细节,可能还有遗漏的一些点,大家有补充的可以留下评论,后续我会更新进本文。