为什么用Glide
Glide:
多种图片格式的缓存,适用于更多的内容表现形式(如Gif、WebP、缩略图、Video)
生命周期集成(根据Activity或者Fragment的生命周期管理图片加载请求)
高效处理Bitmap(bitmap的复用和主动回收,减少系统回收压力)
高效的缓存策略,灵活(Picasso只会缓存原始尺寸的图片,Glide缓存的是多种规格),加载速度快且内存开销小(默认Bitmap格式的不同,使得内存开销是Picasso的一半)
Fresco:
最大的优势在于5.0以下(最低2.3)的bitmap加载。在5.0以下系统,Fresco将图片放到一个特别的内存区域(Ashmem区)
大大减少OOM(在更底层的Native层对OOM进行处理,图片将不再占用App的内存)
适用于需要高性能加载大量图片的场景
对于一般App来说,Glide完全够用,而对于图片需求比较大的App,为了防止加载大量图片导致OOM,Fresco 会更合适一些。并不是说用Glide会导致OOM,Glide默认用的内存缓存是LruCache,内存不会一直往上涨。
自己实现图片加载框架 需要考虑什么
- 异步加载:线程池
- 切换线程:Handler,没有争议吧
- 缓存:LruCache、DiskLruCache
- 防止OOM:软引用、LruCache、图片压缩、Bitmap像素存储位置
- 内存泄露:注意ImageView的正确引用,生命周期管理
- 列表滑动加载的问题:加载错乱、队满任务过多问题
当然,还有一些不是必要的需求,例如加载动画等。
1. 异步
线程池,多少个?
缓存一般有三级,内存缓存、硬盘、网络。
由于网络会阻塞,所以读内存和硬盘可以放在一个线程池,网络需要另外一个线程池,网络也可以采用Okhttp内置的线程池。
读硬盘和读网络需要放在不同的线程池中处理,所以用两个线程池比较合适。
Glide 必然也需要多个线程池,看下源码是不是这样
public final class GlideBuilder {
...
private GlideExecutor sourceExecutor; //加载源文件的线程池,包括网络加载
private GlideExecutor diskCacheExecutor; //加载硬盘缓存的线程池
...
private GlideExecutor animationExecutor; //动画线程池
Glide使用了三个线程池,不考虑动画的话就是两个。
2. 切换线程:
图片异步加载成功,需要在主线程去更新ImageView,
无论是RxJava、EventBus,还是Glide,只要是想从子线程切换到Android主线程,都离不开Handler。
3. 缓存
我们常说的图片三级缓存:内存缓存、硬盘缓存、网络。
内存缓存
一般都是用LruCache
Glide 默认内存缓存用的也是LruCache,只不过并没有用Android SDK中的LruCache,不过内部同样是基于LinkHashMap,所以原理是一样的。
// -> GlideBuilder#build
if (memoryCache == null) {
memoryCache = new LruResourceCache(memorySizeCalculator.getMemoryCacheSize());
}
为什么用LruCache?
LruCache 采用最近最少使用算法,设定一个缓存大小,当缓存达到这个大小之后,会将最老的数据移除,避免图片占用内存过大导致OOM。
LruCache小结:
LruCache 内部用LinkHashMap存取数据,在双向链表保证数据新旧顺序的前提下,设置一个最大内存,往里面put数据的时候,当数据达到最大内存的时候,将最老的数据移除掉,保证内存不超过设定的最大值。
LinkHashMap 继承HashMap,在 HashMap的基础上,新增了双向链表结构,每次访问数据的时候,会更新被访问的数据的链表指针,具体就是先在链表中删除该节点,然后添加到链表头header之前,这样就保证了链表头header节点之前的数据都是最近访问的(从链表中删除并不是真的删除数据,只是移动链表指针,数据本身在map中的位置是不变的)。
磁盘缓存 DiskLruCache
依赖:
implementation 'com.jakewharton:disklrucache:2.0.2'
DiskLruCache 跟 LruCache 实现思路是差不多的,一样是设置一个总大小,每次往硬盘写文件,总大小超过阈值,就会将旧的文件删除。简单看下remove操作:
防止OOM
加载图片非常重要的一点是需要防止OOM,上面的LruCache缓存大小设置,可以有效防止OOM,但是当图片需求比较大,可能需要设置一个比较大的缓存,这样的话发生OOM的概率就提高了,那应该探索其它防止OOM的方法。
方法1:软引用
回顾一下Java的四大引用:
- 强引用: 普通变量都属于强引用,比如 private Context context;
- 软应用: SoftReference,在发生OOM之前,垃圾回收器会回收SoftReference引用的对象。
- 弱引用: WeakReference,发生GC的时候,垃圾回收器会回收WeakReference中的对象。
- 虚引用: 随时会被回收,没有使用场景。
怎么理解强引用:
强引用对象的回收时机依赖垃圾回收算法,我们常说的可达性分析算法,当Activity销毁的时候,Activity会跟GCRoot断开,至于GCRoot是谁?这里可以大胆猜想,Activity对象的创建是在ActivityThread中,ActivityThread要回调Activity的各个生命周期,肯定是持有Activity引用的,那么这个GCRoot可以认为就是ActivityThread,当Activity 执行onDestroy的时候,ActivityThread 就会断开跟这个Activity的联系,Activity到GCRoot不可达,所以会被垃圾回收器标记为可回收对象。
软引用的设计就是应用于会发生OOM的场景,大内存对象如Bitmap,可以通过 SoftReference 修饰,防止大对象造成OOM,看下这段代码
private static LruCache> mLruCache = new LruCache>(10 * 1024){
@Override
protected int sizeOf(String key, SoftReference value) {
//默认返回1,这里应该返回Bitmap占用的内存大小,单位:K
//Bitmap被回收了,大小是0
if (value.get() == null){
return 0;
}
return value.get().getByteCount() /1024;
}
};
LruCache里存的是软引用对象,那么当内存不足的时候,Bitmap会被回收,也就是说通过SoftReference修饰的Bitmap就不会导致OOM。
当然,这段代码存在一些问题,Bitmap被回收的时候,LruCache剩余的大小应该重新计算,可以写个方法,当Bitmap取出来是空的时候,LruCache清理一下,重新计算剩余内存;
还有另一个问题,就是内存不足时软引用中的Bitmap被回收的时候,这个LruCache就形同虚设,相当于内存缓存失效了,必然出现效率问题。
方法2:onLowMemory
当内存不足的时候,Activity、Fragment会调用onLowMemory方法,可以在这个方法里去清除缓存,Glide使用的就是这一种方式来防止OOM。
方法3:从Bitmap 像素存储位置考虑
我们知道,系统为每个进程,也就是每个虚拟机分配的内存是有限的,早期的16M、32M,现在100+M,
虚拟机的内存划分主要有5部分:
- 虚拟机栈
- 本地方法栈
- 程序计数器
- 方法区
- 堆
而对象的分配一般都是在堆中,堆是JVM中最大的一块内存,OOM一般都是发生在堆中。
Bitmap 之所以占内存大不是因为对象本身大,而是因为Bitmap的像素数据, Bitmap的像素数据大小 = 宽 * 高 * 1像素占用的内存。
/**
* Bytes per pixel definitions
*/
public static final int ALPHA_8_BYTES_PER_PIXEL = 1;
public static final int ARGB_4444_BYTES_PER_PIXEL = 2;
public static final int ARGB_8888_BYTES_PER_PIXEL = 4;
public static final int RGB_565_BYTES_PER_PIXEL = 2;
public static final int RGBA_F16_BYTES_PER_PIXEL = 8;
如果Bitmap使用 RGB_565 格式,则1像素占用 2 byte,ARGB_8888 格式则占4 byte。
在选择图片加载框架的时候,可以将内存占用这一方面考虑进去,更少的内存占用意味着发生OOM的概率越低。 Glide内存开销是Picasso的一半,就是因为默认Bitmap格式不同。
ImageView 内存泄露
ImageView被回收,希望加载图片的任务可以取消,队未执行的任务可以移除。
Glide的做法是监听生命周期回调,看 RequestManager 这个类
public 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);
}
在Activity/fragment 销毁的时候,取消图片加载任务
列表加载问题
图片错乱
由于RecyclerView或者LIstView的复用机制,网络加载图片开始的时候ImageView是第一个item的,加载成功之后ImageView由于复用可能跑到第10个item去了,在第10个item显示第一个item的图片肯定是错的。
常规的做法是给ImageView设置tag,tag一般是图片地址,更新ImageView之前判断tag是否跟url一致。
当然,可以在item从列表消失的时候,取消对应的图片加载任务。要考虑放在图片加载框架做还是放在UI做比较合适。
线程池任务过多
列表滑动,会有很多图片请求,如果是第一次进入,没有缓存,那么队列会有很多任务在等待。所以在请求网络图片之前,需要判断队列中是否已经存在该任务,存在则不加到队列去。
问答
Glide 的特性:
- Activity 结束时自动停止加载
- 根据 ImageView 的宽高设置图片的宽高,也可以指定宽高
- 可以设置缩略图和占位图
- 实现了内存到文件的三级缓存
加载步骤
RequestOptions options = new RequestOptions()
.centerCrop()
.placeholder(R.drawable.placeholder);
Glide.with(context)
.load(url)
.apply(options)
.into(imageView);
一次图片加载主要分四步:
调用 with 方法根据 context 生成 RequestManager。实际上可以大致分为 Activity 和 Application 的 Manager 两种。前者可以在 Activity 销毁时自动停止加载;
调用 load 方法生成一个 RequestBuilder 用于构建 Request。所有必要的配置项和加载器都保存在 RequestBuilder 中。
调用配置方法,设置一些配置项;
调用 into 方法生成一个 Request,确定图片的大小,然后调用 Request 的 begin 方法开始加载图片并显示到 ImageView 中。
1. 如何在 Activtiy 结束时停止加载
这个在 with 方法中就实现了。原理如下:
-
- 给 Activity 中添加一个空的 Fragment;
-
- 这个 Fragment 在构造时会新建一个 ActivityFragmentLifecycle 对象,并在自己的生命周期中回调 Lifecycle 对应的方法;
-
- Lifecycle 对应的方法中又会调用监听器中相应方法;
-
- RequestManager 实现了这个监听器,并在构造时将自己添加进 Lifecycle 的监听器列表中;
-
- 这样 Activity 被销毁时,RequestManager 的 onDestroy 方法会被调用。在此方法中清除请求,做一些销毁工作。
2. 图片的宽高如何确定
在 SingleRequest 的 begin 方法中,会在加载之前进行图片大小的设置。
如果用户指定了宽高,使用用户指定的宽高
如果未指定,获取 View 的宽高
最后在加载前将宽高乘以 sizeMultiplier。这个数代表压缩比,由用户设置,必须介于 0 和 1 之间,默认为1。
乘完之后取整,这就是图片的最终宽高了。
那获取 ImageView 的宽高是如何实现的呢?
这个很简单。getSize 方法在 ViewTarget 类中实现。先获取 view 的宽高,判断是否有效,如果有效则回调。如果无效,说明 View 还未测量好,此时给 View 的 ViewTreeObserver 设置一个 Listener,在 Listener 的 onPreDraw 中重新获取宽高,再次判断。
3. 缩略图和占位图如何实现
占位图的实现
占位图的实现很简单,在加载图片前直接设置即可。
缩略图的原理
而如果设置了缩略图,缩略图和完整的图会同时加载,不保证加载完毕的顺序。如果缩略图先加载完就先显示,完整的图加载完毕后再覆盖。如果完整的图先加载完,则清除缩略图的请求。这就是我们看到的图片先模糊再变清晰的效果,这可以保证我们最快看到图片。
缩略图的原理用一句话概括就是使用两个图片请求同时开始加载,其具体实现是这样的:
如果设置了缩略图的 RequestBuilder 或者 thumbSizeMultiplier,那么原 RequestBuilder 构建出来的 Request 是 ThumbnailRequestCoordinator。其包装了两个 SingleRequest,分别代表缩略图的请求(thumbRequest)和完整的请求(fullRequest);
ThumbnailRequestCoordinator 启动时,同时启动 thumbRequest 和 fullRequest;
两个 Request 加载完后的回调方法并未特殊处理,都会显示在 ImageView 中;
如果 thumbRequest 先执行完了,就先显示。fullRequest 执行完后就会正常覆盖;
如果 fullRequest 先执行完,在 ThumbnailRequestCoordinator 的 onRequestSuccess 中会将 thumbRequest 取消掉。
4. 缓存如何实现
在介绍缓存前我们要先搞清楚几个概念。我们在显示图片时传入的是 url,file,资源 id 等,这些被统称为 Model。然后 DataFetcher 根据 Model 从网络或者磁盘中加载回资源,此时图片资源的大小和形状都未经过处理,这时候叫 Data。图片回来后还需要进行一些大小和圆角之类的处理,处理后的图片资源叫 Resource。
Glide 实现了 4 种缓存,内存和文件中各两种,查找缓存时按顺序查找,分别是:
活跃资源(Active Resources),代表内存正在被其他 View 显示的图片资源;
内存缓存(Memory cache),代表最近显示过并缓存在内存中的图片资源;
资源(Resource),代表文件缓存中经过转换的图片文件;
原始数据 (Data),代表文件缓存中未经转换的图片源文件;
下面来看看这四种缓存分别是如何实现的。
在内存缓存中,每个图片资源被包装成一个 EngineResource。里面实现了对资源的配置和释放。同时,还有一个引用计数器,每次被显示时计数器加一,被释放时计数器减一,只有当计数器归零了,图片资源才会被真正释放。
另外,内存中存储的都是经过转换后的 Resource,匹配时要图片的大小和转换方式都一样才能匹配。
活跃资源(Active Resources)
活跃资源代表有其他 View 也在展示这张图片。其将 EngineResource 放在一个弱引用中,然后保存在 HashMap 里。活跃资源占用内存的大小没有限制。每个最终被 View 显示的 EngineResource,都会存储到这个 HashMap 中。
内存缓存(Memory cache)
活跃资源中找不到时,会去内存缓存中查找。Glide 基于 LinkedHashMap 实现了一个 LruCache,其子类 LruResourceCache 就是内存缓存的容器,缓存的最大容量在 Glide 初始化时通过计算设置好。每次添加资源时检查资源总大小,如果超过了大小限制,就会回收最久没被使用的图片资源。
资源(Resource)和原始数据 (Data)
Glide 自己实现了 DiskLruCache 作为文件缓存。Resource
和 Data 的使用同一个 DickCache,缓存在同一片区域。唯一的区别是 Key 不同。同一张原图可以产生很多 Resource。前者的 key 需要宽高和转换方式都相同,只匹配转换后的图片资源。后者只比较原图的 url 和标签,只匹配原图。
在查找缓存时,两者都返回 File 对象。Glide 会在回调方法中设置一个标志,用于区分是 Resource 还是 Data。如果是 Data,还要对查询到的缓存再进行一次转换,如果是 Resource ,就可以省略这一步。
5. 缓存的 Key 如何生成
查找缓存时一共有三个地方会生成 Key:
在查找内存中的活跃资源和内存缓存时,生成 EngineKey
在文件缓存中查找转换过的图片时,生成 ResourceCacheKey
在文件缓存中查找未经转换的原图时,生成 DataCacheKey
DataCacheKey 只需要匹配图片的来源和标签,保证是同一张源图片即可。而 EngineKey 和 ResourceCacheKey 除此之外还要匹配宽高,转换器,各种配置以及 ResourceClass。保证经转换后的资源也一模一样。
6. 图片如何加载
图片资源的加载是交给 ModelLoader 来负责的。ModelLoader 会将具体的加载任务交给 DataFetcher 处理。Glide 会根据不同的 Model 使用不同的 DataFecther。比如我们最常见的加载图片 url,其实现类就是 HttpUrlFetcher,内部使用 HttpUrlConnection 来请求图片,最后输出一个 InputStream 获取图片数据。InputStream 经过 decoder 解码就能转换成我们需要的 Bitmap 或者 Drawable 了。
加载线程如何管理
Glide 中使用线程池管理加载线程,一共有四种:
1. SourceExecutor
默认使用这个线程池管理请求源图片的线程。它的核心线程数和最大线程数相等,队列采用优先级阻塞队列。也就是说,它只有固定个数的核心线程,且不会被回收。核心线程数的设置根据 Cpu 的核心数确定,且最多不超过4个。
2. DiskCacheExecutor
负责管理文件缓存的读写线程。单线程模型,只有一个核心线程,使用优先级阻塞队列。这种单线程模型可以避免文件的并发读写问题。
3. UnlimitedSourceExecutor
当请求配置中的 useUnlimitedSourceGeneratorPool 设置为 true 时,会使用这个线程池管理请求源图片的线程。这个线程池没有核心线程,最大线程数为 Integer.MAX_VALUE,这代表非核心线程的数量没有限制,每个线程的默认存活时间是10秒。另外,它采用 SynchronousQueue 队列,这个队列不存储任务,只要有新任务就会新建线程来处理。
4. AnimationExecutor
当请求配置中的 useAnimationPool 参数设置为 true 时,会使用 AnimationExecutor 管理请求源图片的线程。这个线程池没有核心线程,最大线程数取决于 Cpu 核心数,当核心数大于等于 4 时,最大线程数为2,否则为1。线程默认存活时间为10秒,使用优先级阻塞队列。
使用此线程可以避免在加载资源较大的图片时过多占用 Cpu,一般在加载 GIF 图片时使用。