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方法,从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. 优秀的设计模式运用、应用层优雅的链式调用

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

参考资料


全部评论

相关推荐

10-09 09:39
门头沟学院 C++
HHHHaos:这也太虚了,工资就一半是真的
点赞 评论 收藏
分享
评论
点赞
收藏
分享
牛客网
牛客企业服务