在App中,图片的加载显示是很常见的需求。一般都选择一个star多的图片加载框架直接使用,没有知其所以然。现在来考虑一下,一个合格的图片加载框架应该考虑到哪些方面?
1. 线程池多线程下载,UI线程展示
2. 图片缓存,缓存空间占用完成后,采用哪种策略决定要删除哪些图片
3. OOM,大图处理,图片压缩
4. 列表快速滑动的情况下,图片展示问题,生命周期问题,防止内存泄漏
5. 图片的裁剪等高级效果,动图加载等
源码学习
Glide
最基本的用法:Glide.with(context).load(imgurl) .into(iv);
一. Glide.with(context)
1. context: Activity、Fragment、Application有什么区别
和对应的生命周期有关,使用Activity、Fragment可以连接生命周期智能地停止、启动和重新启动请求。Application则不会基于页面的生命周期事件启动或停止。加载应该在结果将用于的级别(Activity、Fragment、Application)开始。
2. Glide.with(context)返回RequestManager对象,是一个管理和启动Glide请求的类。
3. Glide.with(context)根据context类型的不同,有多种重载方式,其进行的操作如下:
@NonNull
public static RequestManager with(@NonNull Context context) {
return getRetriever(context).get(context);
}
@NonNull
private static RequestManagerRetriever getRetriever(@Nullable Context context) {
// Context could be null for other reasons (ie the user passes in null), but in practice it will
// only occur due to errors with the Fragment lifecycle.
Preconditions.checkNotNull(context,
"You cannot start a load on a not yet attached View or a Fragment where getActivity() "
+ "returns null (which usually occurs when getActivity() is called before the Fragment "
+ "is attached or after the Fragment is destroyed).");
return Glide.get(context).getRequestManagerRetriever();
}
-
getRetriever(context)返回一个RequestManagerRetriever:一组静态方法,用于创建新的RequestManager 或者 从Activity、Fragment检索出现有的
-
Glide.get(context)是一个单例模式,生成Glide的实例并进行一系列初始化操作,包括RequestManagerRetriever
-
Glide初始化的时候,先 new GlideBuilder(), 再通过GlideBuilder.build()获取到Glide对象,在build的过程中,会设置各种需要初始化的值,比如:线程池、Engine对象、RequestManagerRetriever等
-
Glide 的线程池用的GlideExecutor,内部有一个ThreadPoolExecutor,不能直接设置一个线程池进去,只能设置一下核心线程数、线程池名字、异常策略,其内部会根据传入的参数生成一个ThreadPoolExecutor
-
RequestManagerRetriever.get(context)根据context获取对应的RequestManager。context分为两类:
-
Application类:返回mApplicationManager。一个全局变量,null的话初始化,否则直接返回
-
Activity/Fragment类:非主线程的情况同Application类一样。主线程的情况下,利用getSupportFragmentManager/getChildFragmentManager获取tag为FRAGMENT_TAG = "com.bumptech.glide.manager"的fragment,如果不存在,则添加一个带tag的空的fragment,接着生成一个RequestManager,这个时候就会绑定空fragment的生命周期,生成后设置给刚添加的fragment;存在的话直接返回fragment的RequestManager。
-
绑定空fragment的生命周期 : 空fragment本身继承于Fragment,内部有一个全局变量ActivityFragmentLifecycle,会在fragment本身的生命周期回调方法中调用ActivityFragmentLifecycle的对应的方法。生成RequestManager的时候,入参之一就是空fragment的ActivityFragmentLifecycle,这个样子来进行生命周期绑定操作的。
-
所以,为什么都要以一个空的fragment做中介,而不是直接设置给对应的Activity/Fragment? 为了生命周期的绑定么?假如直接绑定的话要怎么操作?
-
我猜:直接设置有tag重复的风险?毕竟开发者也可以设置;而且直接设置的话,开发者需要在每个页面手动进行绑定,入侵性大?写个基类呢?不需要的页面也进行了相应的绑定操作,浪费内存?
-
综上所述,RequestManagerRetriever的作用可以理解为一个map,用于保存context及其对应的RequestManager。当然,它不是一个map,它是将RequestManager设置给context对应FragmentManager中的tag为FRAGMENT_TAG的空Fragment中。
Glide.get(context)是一个单例模式,不会随着context的不同就创建不同的对象,不论context的值,只会初始化一次
二. RequestManager.load(imgurl)
Glide.with(context).load(imgurl)返回RequestBuilder对象,一个可以处理设置选项的泛型类,并对泛型资源启动负载。必须注意,此处只是设置了imgurl,并没有开始加载
三. RequestBuilder.into(iv)
Glide.with(context) .load(imgurl) .into(iv); 所以真正的下载是在这个里面进行的
1. 必须在主线程且iv不能为空
2. RequestManager.track(ViewTarget, Request)
3. Request(SingleRequest).begin
4. Engine.load(EngineJob, DecodeJob ...),先直接在内存中找,有的话直接用,没有的话走下一步
5. EngineJob.start(DecodeJob)
6. 指定线程池中执行 DecodeJob.run,DecodeJob实现了Runnable接口
7. DataFetcherGenerator.startNext
8. DataFetcher.loadData
9. HttpUrlConnection
10. 获取的result通过接口回调到DataFetcherGenerator,再传递给DecodeJob解码,生成的resource传递给EngineJob,再传递给SingleRequest中展示图片
这个过程中用到的主要的类:
-
Engine类(发动机):负责启动加载,管理活动资源和缓存资源。
-
EngineJob:通过为加载添加和删除回调并在加载完成时通知回调来管理加载的类。
-
DecodeJob:从 磁盘缓存/原始数据源 解码资源,并进行转换和转码
-
DataFetcherGenerator:使用注册的ModelLoader和一个Model生成一系列DataFetcher
-
DataFetcher:惰性地检索可用于加载资源的数据。
大概流程图如下:
线程池GlideExecutor:
1. 内部有一个ThreadPoolExecutor,不能直接设置一个线程池进去,只能设置一下核心线程数、线程池名字、异常策略,其内部会根据传入的参数生成一个ThreadPoolExecutor。
2. Glide中设置的核心线程数和最大线程数一样,且keepAliveTime为0(参数含义为 当线程数大于内核时,这是多余空闲线程在终止之前等待新任务的最大时间。Glide中因为核心线程数和最大线程数一样,所以这个其实是没啥意义的吧)
缓存(内存缓存、磁盘缓存共用一个线程池)线程池 disk-cache:默认线程数 1
远程加载线程池 source:默认线程数 Math.min(4,RuntimeCompat.availableProcessors())
缓存策略:
LruCache:
-
内部有一个LinkedHashMap用来存放数据,根据最近最少使用原则删除超出范围的数据。
-
LinkedHashMap 实现原理学习
-
线程安全,因为它的方法都是同步方法,方法加了synchronized关键字。
-
每次增删数据都要重新计算当前的size,增加数据的时候需要判断新增的是否超出范围,超出的话不能增加,会回调删除数据的方法;没有的话新增后判断整体是否超出范围,超出的话根据最近最少使用原则删除超出范围的数据;删除数据会有对应的回调
onItemEvicted(@NonNull T key, @Nullable Y item)
Glide缓存:
缓存密钥key:
-
内存缓存 EngineKey:仅用于内存缓存的密钥,包含宽高签名等参数,确定唯一性
-
磁盘缓存分为两种
-
ResourceCacheKey:采样和转换后的资源数据 + 任何请求签名 的缓存密钥
-
DataCacheKey:原始源数据 + 任何请求签名 的缓存密钥
缓存密钥都包含宽高等信息,所以需要不同宽高的图片的时候,缓存找不到,会重新下载,不会只下载一个图片然后进行拉伸等操作。
缓存实现:
-
内存缓存, 有两种,直接在主线程中进行的
-
弱引用的缓存:内部有一个 key-资源弱引用 的map,正在显示的图片
-
Map<Key, ResourceWeakReference> activeEngineResources = new HashMap<>();
-
强引用的缓存 LruResourceCache:最近最少使用原则,不是正在显示的图片
-
获取图片的时候:优先弱引用查找,有的话直接返回,没有的话LruResourceCache中查找,找到的话从LruResourceCache中删除,添加到弱引用中
-
为什么要加到弱引用中?弱引用存在的意义?
-
我猜:图片很大的时候,内存不足,直接引起gc,太大就直接回收掉,不会引起OOM。
-
图片资源释放的时候,从弱引用中删除,添加到 LruResourceCache 中
-
每次取到资源的时候,都先计数器加一 EngineResource.acquire();
-
EngineResource类:对Resource进行了一次包装,可以对其进行计数。
-
磁盘缓存,也有两种类型,在缓存线程池 disk-cache 中执行
-
转码后的
-
转码前的原始的
-
网络获取,通过HttpUrlConnection进行的,在远程加载线程池 source 中执行
-
获取完添加到磁盘和弱引用中
未完待续...