前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Android | Glide细枝篇

Android | Glide细枝篇

作者头像
Holiday
发布2020-08-10 15:17:25
7440
发布2020-08-10 15:17:25
举报
文章被收录于专栏:哈利迪ei哈利迪ei

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

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

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

Generated API

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

全局配置

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

代码语言:javascript
复制
@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减少一半内存。见默认请求选项,

代码语言:javascript
复制
@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很多页面都有商品列表,这些商品图片的宽高如果是固定的,就可以包装起来,

代码语言:javascript
复制
@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,里面会多出一个方法,

代码语言:javascript
复制
class GlideOptions extends RequestOptions implements Cloneable {
    public GlideOptions goods() {
        return (GlideOptions) MyAppExtension.goods(this);
    }
}

这时,就可以用goods来直接使用这一组打包好的行为了,

代码语言:javascript
复制
//要用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

代码语言:javascript
复制
//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);
}

退出页面时,

代码语言:javascript
复制
//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使用的,就叫他非活跃资源吧,

读取内存:

代码语言:javascript
复制
//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;
}

写入内存:

代码语言:javascript
复制
//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

代码语言:javascript
复制
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类(看名字知道还是最近最少使用算法),

代码语言:javascript
复制
//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

代码语言:javascript
复制
//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方法,

代码语言:javascript
复制
//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. 优秀的设计模式运用、应用层优雅的链式调用

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

参考资料

  • 官方文档 & GitHub
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-07-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 哈利迪ei 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • Generated API
    • 全局配置
      • 行为打包
      • 空Fragment取消请求
      • Cache缓存
        • 内存
          • 磁盘
          • BitmapPool令人诟病
          • Decoder解码
          • 总结
          • 参考资料
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档