Android | Glide细枝篇

《看完不忘系列》之Glide (树干篇)一文对Glide加载图片的核心流程作了介绍,细枝篇做为补充,将对一些具体实现细节进行深刻。本文篇幅略大,你们能够根据目录索引到感兴趣的章节阅读~html

源码基于最新版本4.11.0,先上一张职责图预览下,一家人就要整整齐齐~java

本文约3200字,阅读大约10分钟。如个别大图模糊(官方会压缩),可前往我的站点阅读git

Generated API

经过建立一些类,继承相关接口,而后打上注解,由apt来处理这些类,从而实现接口扩展。github

全局配置

注解@GlideModule用来配置全局参数和注册定制的能力,在application里使用AppGlideModule,在library里使用LibraryGlideModuleweb

@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来直接使用这一组打包好的行为了,app

//要用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方法,从DiskLruCacheDiskLruCacheWrapperget,而后再追,发现有两个类调了get,分别是DataCacheGeneratorResourceCacheGenerator,前者是原始图片的缓存,后者是通过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有以下优点:

  1. 空Fragment感知页面生命周期,避免无效请求
  2. 高度可配置,详见 配置
  3. 三级缓存(网络层缓存如okhttp就不考虑了):内存活跃资源 ActiveResources、内存非活跃资源 LruResourceCache、磁盘缓存 DiskLruCache
  4. 可定制,引入apt处理注解,打包行为,扩展接口。(哈迪没怎么用,感受有点鸡肋,可能之后会真香)
  5. 可扩展,能够替换网络层、定制本身的图片来源 ModelLoader,详见 编写定制的ModelLoader
  6. 无侵入,into能够传入最简单的ImageView
  7. 优秀的设计模式运用、应用层优雅的链式调用

至于缺点吧,暂时还没想到。本文只列出了哈迪以为比较精彩的细节,可能还有遗漏的一些点,你们有补充的能够留下评论,后续我会更新进本文。

参考资料


本文使用 mdnice 排版

相关文章
相关标签/搜索