写点什么

Glide 源码难看懂?用这个角度让你事半功倍!,移动端 h5 页面加载慢

用户头像
Android架构
关注
发布于: 刚刚

}


public synchronized <T> void put(String key, T value){


// 存储图片数据到磁盘中


}


}

3.3、网络缓存的设计

网络缓存的设计就很简单了,也就是直接访问获取,获取图片文件;


public class NetWorkLruCache {


public synchronized <T> T request(String url){


// 获取网络的数据


return null;


}


}


那么到这里,一个简单的缓存机制就设计完成了;

3.4、总结

那么一个简单的图片框架就这样实现了,相较于之前的框架,多了缓存机制,对于图片的利用有了很大的提升;



如果我告诉你,恭喜你,你已经成功掌握了 Glide 源码的实现,我想我可能会被一巴掌拍扁了;


但是我想要告诉你的是,上面的原理,就是一个 Glide 源码的简化,看懂了上面那个逻辑,基本上 Glide 源码的基本流程,你就已经搞懂了;


剩下的基本上就是更加细节的实现;


事实上,一个图片框架的实现基本上离不开这几步,更细节的实现,无非就是基于这几步来进行扩展,封装;


基础的原理搞明白了,再去深入研究源码,才会有意义;


4、Glide 版本实现




我们上面通过抽取实现了一个简单的图片框架,虽然功能都有了,但是总是感觉缺少了点什么!


缺了什么呢?


缺了更多的细节,比如设计模式的封装,怎样解耦以及更好的复用!


还有就是性能优化,上面我们这个框架,虽然引入了缓存机制,但是还有更多的性能优化的点待挖掘;


下面我将根据上面这个简单的图片框架,来讲一讲 Glide 框架的实现的细节,相较于我们这个框架,有了什么更进一步的优化点;

4.1、RequestManager

我们上面在实现 RequestManager 的时候,提到了 RequestManager 的实现思路,手动的在 Activity 或者 Fragment 里面进行回调;


而 Glide 的实现更加的巧妙,通过创建一个不可见的 Fragment 来实现生命周期的回调;


Glide 在调用 Glide.with(this)方法的时候,


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


就会返回一个创建好的 RequestManager,在创建 RequestManager 会创建一个不可见的 Fragment,并将其设置给 RequestManager,让其具有生命周期的监听;


创建 Fragment 的实现逻辑是在 RequestManagerRetriever 的 ragmentGet 这个方法里面;


看一下大概的实现;


private RequestManager fragmentGet(


@NonNull Context context,


@NonNull android.app.FragmentManager fm,


@Nullable android.app.Fragment parentHint,


boolean isParentVisible) {


// 通过传进来的 FragmentManager 来创建一个不可见的 RequestManagerFragment


RequestManagerFragment current = getRequestManagerFragment(fm, parentHint);


// 通过 RequestManagerFragment 获取 RequestManager


RequestManager requestManager = current.getRequestManager();


if (requestManager == null) {


Glide glide = Glide.get(context);


// 如果 RequestManager 为空,则通过抽象工厂来创建 RequestManger


requestManager =


factory.build(


glide, current.getGlideLifecycle(), current.getRequestManagerTreeNode(), context);


// 判断当前页面是否是可见的,是的话则回调开始方法;


if (isParentVisible) {


requestManager.onStart();


}


// 将 RequestManager 设置给了 fragment


current.setRequestManager(requestManager);


}


return requestManager;


}


这里面的实现没有很复杂,就做了两件事,创建不可见的 RequestManagerFragment,创建 RequestManager,并将 RequestManger 设置给 RequestManagerFragment,而 RequestManagerFragment 里面生命周期的回调,都会回调到 RequestManager 里;


这样就让 RequestManager 有了生命周期的监听;


这里的 Lifecycle 不是 Jectpack 的 Lifecycle 组件,而是自己定义的一个监听,用于回调生命周期;


这里有哪个优化细节值得探讨的呢?


这个细节就在于 RequestManagerFragment 的创建;


RequestManagerFragment 的创建,并不是每次获取都会重新创建的;


总共有三步逻辑,请看下面的源码


private RequestManagerFragment getRequestManagerFragment(


@NonNull final android.app.FragmentManager fm, @Nullable android.app.Fragment parentHint) {


// 先通过 FragmentManager 来获取对应的 fragment


RequestManagerFragment current = (RequestManagerFragment) fm.findFragmentByTag(FRAGMENT_TAG);


// 如果获取为空,则从一个 HashMap 集合中获取;


if (current == null) {


current = pendingRequestManagerFragments.get(fm);


// 如果从集合中获取为空,那么就新建一个 Fragment 并添加到页面去,然后再将其放到 HashMap 中,并发送消息,将该 Fragment 从 HashMap 中移除掉;


if (current == null) {


current = new RequestManagerFragment();


current.setParentFragmentHint(parentHint);


...


fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();


...


}


}


return current;


}


这里主要做了两步操作:


第一步:通过 FragmentManager 来查找 Fragment,如果获取到了则返回;


第二步:第一步没有获取到 Fragment,则新建一个 Fragment,将其添加到页面;


在 RequestManagerFragment 的生命周期方法里,通过 lifecycle 进行回调,而 RequestManger 注册了这个监听,那么就可以获取到生命周期;



最终在 RequestManger 的生命周期里,开启了图片的加载和停止的操作;


4.2、RequestBuilder

RequestBuilder 的职责很明确,用于创建获取图片的请求;


这个类使用了建造者模式来构建参数,这样有一个好处就是,可以很方便的添加各种各样复杂的参数;


这个 RequestBuilder 没有 build 方法,但是有 into 方法,原理其实一样,没有说一定要写成 build;


最终加载图片的逻辑,就是在 into 方法里面实现的;


private <Y extends Target<TranscodeType>> Y into(


@NonNull Y target,


@Nullable RequestListener<TranscodeType> targetListener,


BaseRequestOptions<?> options,


Executor callbackExecutor) {


...


// 创建图片请求


Request request = buildRequest(target, targetListener, options, callbackExecutor);


// 获取当前图片控件是否已经有设置图片请求了,如果有且还没有加载完成,


// 或者加载完成但是加载失败了,那么就将这个请求再重新调用 begin,再一次进行请求;


Request previous = target.getRequest();


if (request.isEquivalentTo(previous)


&& !isSkipMemoryCacheWithCompletePreviousRequest(options, previous)) {


...


if (!Preconditions.checkNotNull(previous).isRunning()) {


...


previous.begin();


}


return target;


}


//清除当前图片控件的图片请求


requestManager.clear(target);


// 设置请求给控件


target.setRequest(request);


// 再将请求添加到 requestManager 中


requestManager.track(target, request);


return target;


}


这里有两个细节:


第一个细节:


在开始请求之前,会先获取之前的请求,如果之前的请求还没有加载完成,或者加载完成了但是加载失败了,那么则会再次重试请求;


第二个细节:


将请求设置给了封装了图片控件的 target,这样做有什么好处呢?


我们的页面大多数都是列表页,那么基本上会使用 RecycleView 这种列表控件来加载数据,而这种列表在加载图片的时候,快速滑动时会出现加载错乱的问题,其原因是 RecycleView 的 Item 复用的问题;


而 Glide 就是在这里通过这样的操作来避免这样的问题;


在调用 setRequest 的时候,将当前的 Request 作为 tag 设置给了 View,那么在获取 Request 进行加载的时候,就不会出现错乱的问题;


private void setTag(@Nullable Object tag) {


...


view.setTag(tagId, tag);


}

4.3、Engine

从上面我们知道,一切的请求都是在 Request 的 begin 里开始的,而其实现是在 SingleRequest 的 begin 里面,最终会走到 SingleRequest 的 onSizeReady 里,通过 Engine 的 load 来加载图片数据;


这个 load 方法主要分为两步:


第一步:从内存加载数据


第二步:从磁盘或者网络加载数据


public <R> LoadStatus load(...) {


...


synchronized (this) {


// 第一步


memoryResource = loadFromMemory(key, isMemoryCacheable, startTime);


// 第二步


if (memoryResource == null) {


return waitForExistingOrStartNewJob(


glideContext,


model,


signature,


width,


height,


resourceClass,


transcodeClass,


priority,


diskCacheStrategy,


transformations,


isTransformationRequired,


isScaleOnlyOrNoTransform,


options,


isMemoryCacheable,


useUnlimitedSourceExecutorPool,


useAnimationPool,


onlyRetrieveFromCache,


cb,


callbackExecutor,


key,


startTime);


}


}


...


return null;


}


简单回顾一些,我上面设计了一个 LruResourceCache 的类来做内存缓存,里面使用了 LRU 算法;


相较于我们上面设计的从内存加载数据的逻辑,Glide 这里的细节体现在哪里呢?


private EngineResource<?> loadFromMemory(


EngineKey key, boolean isMemoryCacheable, long startTime) {


...


// 从弱引用里获取图片资源


EngineResource<?> active = loadFromActiveResources(key);


if (active != null) {


...


return active;


}


// 从 LRU 缓存里面获取图片资源


EngineResource<?> cached = loadFromCache(key);


if (cached != null) {


...


return cached;


}


return null;


}


首先第一个细节:


Glide 设计了一个弱引用缓存,当从内存里面加载时,会先从弱引用里面获取图片资源;


为什么要多设计一层弱引用的缓存呢?


这里要说一下弱引用在内存里面的机制,弱引用对象在 Java 虚拟机触发 GC 时,会回收弱引用对象,不管此时内存是不是够用!


那么设计了一个弱引用缓存的好处在于,没有触发 GC 的这段时间,可以重复的利用图片资源,减少从 LruCache 里的操作;


看一下源码最终调用的地方:


synchronized EngineResource<?> get(Key key) {


// 根据 Key 获取到了一个弱引用对象


ResourceWeakReference activeRef = activeEngineResources.get(key);


if (activeRef == null) {


return null;


}


EngineResource<?> active = activeRef.get();


...


return active;


}


第二个细节:


从 LruCache 里获取图片资源后,并将其存入到弱引用缓存里;


private EngineResource<?> loadFromCache(Key key) {


//


EngineResource<?> cached = getEngineResourceFromCache(key);


if (cached != null) {


cached.acquire();


activeResources.activate(key, cached);


}


return cached;


}


还有一个操作就是在图片加载完成之后,会将该图片资源存入到弱引用缓存里,以便后续复用;


其源码位置在这里调用:Engine 的 onEngineJobComplete;


而这个方法是在图片加载的回调里调用的,也就是 EngineJob 的 onResourceReady


public synchronized void onEngineJobComplete(


EngineJob<?> engineJob, Key key, EngineResource<?> resource) {


...


if (resource != null && resource.isMemoryCacheable()) {


// 存入到弱引用缓存里


activeResources.activate(key, resource);


}


...


}


其实这两个细节,都是由弱引用缓存来引入的,正是因为多了一个弱引用缓存,所以才能把图片的加载性能压榨到了极致;


有人会说,加了这个有啥用呢? 又不能快多少。


俗话说的好:性能都是一步步压榨出来的,在能优化的地方优化一点点,渐渐的积累,也就成河流了!


如果上面都获取不到图片资源,那么接下来就会从磁盘或者网络加载数据了;

4.4、DecodeJob

从磁盘或者网络读取,必然是一个耗时的任务,那么肯定是要放在子线程里面执行,而 Glide 里也正是这样实现的;


来看下大致的源码:


最终会创建一个叫 DecodeJob 的异步任务,来看下这个 DecordJob 的 run 方法做了什么操作?


public void run() {


...


runWrapped();


...


}


run 方法里面主要有一个 runWrapped 方法,这个方法才是最终执行的地方;


在这个 com.bumptech.glide.load.engine.DecodeJob#getNextGenerator 方法里面,会获取内存生产者 Generator,这几个内容生产者分别对应着不同的缓存数据;


private DataFetcherGenerator getNextGenerator() {


switch (stage) {


case RESOURCE_CACHE:


return new ResourceCacheGenerator(decodeHelper, this);


case DATA_CACHE:


return new DataCacheGenerator(decodeHelper, this);


case SOURCE:


return new SourceGenerator(decodeHelper, this);


case FINISHED:


return null;


...


}


}

4.5、DataCacheGenerator

ResourceCacheGenerator:对应转化后的图片资源生产者;


DataCacheGenerator:对应没有转化的原生图片资源生产者;


SourceGenerator:对应着网络资源内容生产者;


这里我们只关心从磁盘获取的数据,那么则对应着这个 ResourceCacheGenerator 和这个 DataCacheGenerator 的生产者;


这两个方法的实现差不多,都是通过获取一个 File 对象,然后再根据 File 对象来加载对应的图片数据;


从上面我们可以知道,Glide 的磁盘缓存,是从 DiskLruCache 里面获取的;


下面我们来看一下这个 DataCacheGenerator 的 startNext 方法;


public boolean startNext() {


while (modelLoaders == null || !hasNextModelLoader()) {


sourceIdIndex++;


...


Key originalKey = new DataCacheKey(sourceId, helper.getSignature());


// 通过生成的 key 从 DiskLruCache 里面获取 File 对象


cacheFile = helper.getDiskCache().get(originalKey);


if (cacheFile != null) {


this.sourceKey = sourceId;


modelLoaders = helper.getModelLoaders(cacheFile);


modelLoaderIndex = 0;


}


}


...


while (!started && hasNextModelLoader()) {


ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);


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;


}


这里主要分为两步:


第一步是通过生成的 key 从 DiskLruCache 里面获取 File 对象;


第二步是将 File 对象,通过 LoadData 将 File 对象转化为 Bitmap 对象;


第一步:获取 File 对象


Glide 在加载 DiskLruCache 的时候,会将所有图片对应的路径信息加载到内存中,当调用 DiskLruCache 的 get 方法时,其实是从 DiskLruCache 里面维护的一个 Lru 内存缓存里直接获取的;


所以第一步的 get 方法,其实是从 LruCache 内存缓存里面获取 File 对象的;


这个处理逻辑是在 DiskLruCache 的 open 方法里面操作的,里面会触发一个读取本地文件的操作,也就是 DiskLruCache 的 readJournal 方法;


最终走到了 readJournalLine 方法;



这个文件主要存放了图片资源的 key,用于获取本地图片路径的文件;


最终是在 DiskLruCache 的 readJournalLine 方法里面,会创建一个 Entry 对象,在 Entry 对象的构造方法里面创建了 File 对象;


private Entry(String key) {


...


for (int i = 0; i < valueCount; i++) {


// 创建图片文件 File 对象


cleanFiles[i] = new File(directory, fileBuilder.toString());


fileBuilder.append(".tmp");


dirtyFiles[i] = new File(directory, fileBuilder.toString());


...


}


}


到这里你是否会有疑问了,如果我是在 DiskLruCache 初始化之后下载的图片呢? 这时候 DiskLruCache 的 Lru 内存里面肯定没有这个数据,那这个数据是哪来的?


相信聪明的你已经猜到了,就是图片文件在存入本地的时候也会将其加入到 DiskLruCache 的 Lru 内存里;


其实现是在 DiskLruCache 的 edit()方法;


private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {


...


Entry entry = lruEntries.get(key);


...


if (entry == null) {


entry = new Entry(key);


lruEntries.put(key, entry);


}


...


Editor editor = new Editor(entry);


entry.currentEditor = editor;


...


return editor;


}


这里先把生成的 Entry 对象加入到内存中,然后再通过 Editor 将图片文件写入到本地,通过 IO 操作,这里就不多赘述了;


存的方法有了,来看一下 DiskLruCache 的 get 方法吧;


public synchronized Value get(String key) throws IOException {


...


// 直接通过内存获取 Entry 对象;


Entry entry = lruEntries.get(key);


...


return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);


}



第二步:File 对象转化


当根据 key 拿到 File 对象时,那么接下来就是将 File 文件转化为 bitmap 数据了;


这一段代码的设计非常有意思,下面我们来看一下具体是怎么实现的!


DataCacheGenerator 的 startNext 方法:


public boolean startNext() {


while (modelLoaders == null || !hasNextModelLoader()) {


...


modelLoaders = helper.getModelLoaders(cacheFile);


...


}


while (!started && hasNextModelLoader()) {


// 遍历获取 ModelLoader


ModelLoader<File, ?> modelLoader = modelLoaders.get(modelLoaderIndex++);


// 获取 LoadData


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;


}

4.6、ModelLoader

这里通过设计了 ModelLoader 类,来用于加载数据逻辑;


这一段代码都是基于接口来实现的,没有依赖具体实现,好处就是非常的解耦与灵活,坏处就是代码阅读性降低,因为你很一时半会很难找到这里的实现类到底是哪个!


这个 ModelLoader 类的职责具体是做什么的呢? 我们可以先来看看注释;



翻译过来的意思就是:将任意复杂的数据模型转换为具体数据类型的工厂接口;


怎么理解这句话呢? 可以理解为适配器模式,将某一类数据转化为另外一类数据;


public interface ModelLoader<Model, Data> {


...


class LoadData<Data> {


public final Key sourceKey;


public final List<Key> alternateKeys;


public final DataFetcher<Data> fetcher;


public LoadData(@NonNull Key sourceKey, @NonNull DataFetcher<Data> fetcher) {


this(sourceKey, Collections.<Key>emptyList(), fetcher);


}


public LoadData(


@NonNull Key sourceKey,


@NonNull List<Key> alternateKeys,


@NonNull DataFetcher<Data> fetcher) {


this.sourceKey = Preconditions.checkNotNull(sourceKey);


this.alternateKeys = Preconditions.checkNotNull(alternateKeys);


this.fetcher = Preconditions.checkNotNull(fetcher);


}


}


...


@Nullable


LoadData<Data> buildLoadData(


@NonNull Model model, int width, int height, @NonNull Options options);


...


boolean handles(@NonNull Model model);


这个类方法很少,里面持有内部类 LoadData,LoadData 里面又持有接口 DataFetcher,DataFetcher 的职责就是用于加载数据,里面持有接口方法 loadData;


这是一个典型的组合模式,可以在不改变类结构的情况下,让这个类具有更多的功能;


ModelLoader 还有一个接口方法 buildLoadData,用于构建 LoadData 对象;



看到这里,你是否感觉懂了,但是又没完全懂!


是的,尽管我们知道了这里的实现逻辑,但是具体的实现我们并不知道在哪里!


这里使用了两个接口,一个是 ModelLoader,一个是 DataFetcher,找完 ModelLoader 的实现,还得找 DataFetcher 的实现,看起来就非常的绕!


而这一切还是得从 ModelLoader 讲起;


实现了 ModelLoader,也就实现了构建 LoadData 的方法,在构建 LoadData 的时候也就构建了 DataFetcher 对象,而这里的 LoadData 对象,其实只负责维护 DataFetcher 等相关变量,最终加载数据的地方还是 DataFetcher;


也就是说我们找到了 ModelLoader,也就找到了相应的 DataFetcher,也就知道了对应的加载逻辑;


这个在设计上叫做啥? 高内聚!同一类相关的实现放在一起,实现了高内聚的封装;


短短几行代码,大牛诠释了什么叫做高内聚,低耦合,先膜拜一下!



接下来我们来看看 ModelLoader 是怎么获取的;


从上面的源码我们可以看到,ModelLoader 是通过 DecodeHelper 的 getModelLoaders 方法来获取的,通过传进去的 File 对象;


最终的调用是通过 Registry 的 getModelLoaders 方法来获取 ModelLoader 列表;


这里有必要来讲一下这个 Registry 类;

4.7、Registry

这个类的主要职责就是用于保存各种各样的数据,提供给外部使用,你这里可以理解为一个 Map 集合,存放着各种各样的数据,通过 Key 值来获取;


只是这个 Registry 封装的更强大,里面能保存的内容更丰富;



看一下这个类的构造方法,创建了一堆的注册类,用于存放对应的数据;


而这个类的初始化是在 Glide 的构造方法,注册对应的数据也是在 Glide 的构造方法;



大致瞄一眼,不必太过深入去了解,只需要知道这个类是用于存放注册的数据,提供给外部使用;


当我们通过 Registry 获取到 ModelLoader 列表后,就会进行遍历,判断当前的 LoadData 是否有 LoadPath,这个 LoadPath 我们下面再讲;


这里只需要了解这个类是用来做资源转换的,比如把资源文件转化为 Bitmap 或者 Drawable 等等;



当有一个符合的时候,就只会执行一次;


而我们这里最终执行的类是 ByteBufferFileLoader;


这个类在 loadData 执行方法的时候将 File 对象转化为字节数据 ByteBuffer;



看一下大致的实现,不必太过深入了解;



上面我们获取到了文件的流数据,那么接下来怎么将其转化为能展示的 bitmap 数据呢?


这里就到了解码的阶段了;


上面获取到的字节数据 ByteBuffer,最终会回调到 DecodeJob 这个类,在这里面实现了解码的逻辑;


看一下 DecodeJob 的 onDataFetcherReady 方法:


public void onDataFetcherReady(


Key sourceKey, Object data, DataFetcher<?> fetcher, DataSource dataSource, Key attemptedKey) {


...


decodeFromRetrievedData();


...


}


里面调用了 decodeFromRetrievedData 方法来解码图片流数据;


这里通过 LoadPath 类来实现解码的功能,

4.8、LoadPath

这个 LoadPath 类里面实现其实并不复杂,里面持有各种抽象的变量,比如 Pool,DecodePath,这个类也是用的组合的模式,里面不持有具体的实现;


而是通过组合的类来进行调用,将实现抽象出来,让这个类可以更灵活的使用;



解码的地方是在这个类的 LoadPath 的 load 方法,最终是通过 DecodePath 的 decode 方法,具体实现并不在这个类中;


这个 DecodePath 的设计和 LoadPath 其实差不多,都是不涉及具体实现,而是通过里面持有的接口来进行调用,进而将具体实现抽象到外部,由外部传入来控制;


来大概看一下这个类的成员变量和构造方法:



在 DecodePath 的 decode 方法里面进行解码,最终调用的地方是通过 ResourceDecoder 的 decode 方法 来进行解码;


瞄一下大致的实现;



而这个 ResourceDecoder 是一个接口,具体实现有很多个类,具体看下面的截图:



最终的实现是在这些类里面;


看一下大致流程图:



这些类是在哪里创建的呢?


我们通过上面的构造方法可以知道,这个类是由外部一步步传进来的;


首先创建 LoadPath 的时候会将 DecodePath 传进来,创建 DecodePath 的时候,又会把 ResourceDecoder 传进来,那么我们可以先从创建 LoadPath 的地方找起;


首先是在 DecodeJob 的 decodeFromFetcher 方法里,通过 DecodeHelper 的 getLoadPath 方法;


private <Data> Resource<R> decodeFromFetcher(Data data, DataSource dataSource)

用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Glide源码难看懂?用这个角度让你事半功倍!,移动端h5页面加载慢