一、图片加载框架对比
首先,当下流行的图片加载框架有那么几个,可以拿 Glide 跟Fresco对比,例如这些:
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,内存不会一直往上涨。
LruCache初始化是需要指定一个内存大小的,在大量图片的需求下,可能会指定大一点的内存,保证效率,LruCache内存越大,发生OOM的几率就越高,特别是在老设备上,内存本来就小。
二、假如让你自己写个图片加载框架,你会考虑哪些问题?
首先,梳理一下必要的图片加载框架的需求:
- 异步加载:线程池
- 切换线程:Handler
- 缓存:LruCache、DiskLruCache
- 防止OOM:软引用、LruCache、图片压缩、Bitmap像素存储位置
- 防止内存泄露:注意ImageView的正确引用,生命周期管理
- 列表滑动加载的问题:加载错乱、队满任务过多问题
2.1、异步加载
缓存一般有三级,内存、硬盘、网络。
加载流程:先从内存加载,内存中有,就直接加载;内存中没有,就从硬盘加载,若硬盘中有,加载的同时缓存到内存;若硬盘也没有,从网络加载,同时缓存到内存和硬盘。
由于网络会阻塞,所以读内存和硬盘可以放在一个线程池,网络需要另外一个线程池。所以用两个线程池比较合适。
Glide 必然也需要多个线程池,看下源码是不是这样:
public final class GlideBuilder {
...
private GlideExecutor sourceExecutor; //加载源文件的线程池,包括网络加载
private GlideExecutor diskCacheExecutor; //加载硬盘缓存的线程池
...
private GlideExecutor animationExecutor; //动画线程池
Glide使用了三个线程池,不考虑动画的话就是两个。
2.2、切换线程
图片异步加载成功,需要在主线程去更新ImageView,
无论是RxJava、EventBus,还是Glide,只要是想从子线程切换到Android主线程,都离不开Handler。
看下Glide 相关源码:
class EngineJob<R> implements DecodeJob.Callback<R>,Poolable {
private static final EngineResourceFactory DEFAULT_FACTORY = new EngineResourceFactory();
//创建Handler
private static final Handler MAIN_THREAD_HANDLER =
new Handler(Looper.getMainLooper(), new MainThreadCallback());
问:RxJava是完全用Java语言写的,那怎么实现从子线程切换到Android主线程的?
答:引入了rxandroid,利用handler进行主线程切换。
2.3、缓存
2.3.1、内存缓存-LruCache
目的:防止应用重复将图片读入到内存,造成内存资源浪费。
一般都是用LruCache
,LruCache 采用最近最少使用算法,设定一个缓存大小,当缓存达到这个大小之后,会将最老的数据移除,避免图片占用内存过大导致OOM。
2.3.2、 磁盘缓存-DiskLruCache
目的:防止应用重复的从网络或者其他地方下载和读取数据。
DiskLruCache 跟 LruCache 实现思路是差不多的,一样是设置一个总大小,每次往硬盘写文件,总大小超过阈值,就会将旧的文件删除。
2.4、防止OOM
加载图片非常重要的一点是需要防止OOM,上面的LruCache初始化是需要指定一个内存大小的,在大量图片的需求下,可能会指定大一点的内存,保证效率,LruCache内存越大,发生OOM的几率就越高,特别是在老设备上,内存本来就小。那应该探索其它防止OOM的方法。
2.4.1、软引用
//强引用:
//直接new出来的对象
String str = new String(“aaa”);
//特点:对象任何时候都不会对系统回收,JVM宁愿抛出OOM异常,也不会回收强引用所指向的对象。
//软引用:SoftReference
String str = new String(“aaa”);
SoftReference s = new SoftReference(str);
if(s.get() != null){ // 一定要判空
String softStr = s.get();
}
//特点:内存空间足,不回收;内存空间不足,才会回收
//弱引用:WeakReference
String str = new String(“aaa”);
WeakReferences = new WeakReference(str);
if(s.get() != null){ // 一定要判空
String weakStr = s.get();
}
//特点:无论内存空间是否足够,只要发现,就会回收
//虚引用:PhantomReference
//特点:可以在任何时候被回收,无法通过get()方法来获取对象实例。仅仅只能在对象被回收时收到一个通知。
到底什么时候用软引用,什么时候用弱引用?
- 如果只是想避免OOM,则可以使用软引用;如果对应用的性能更在意,则可以使用弱引用。
- 根据对象是否经常被使用。如果对象经常被使用,则尽量用软引用,否则使用弱引用。
2.4.2、onLowMemory
当内存不足的时候,Activity、Fragment会调用onLowMemory
方法,可以在这个方法里去清除缓存,Glide使用的就是这一种方式来防止OOM。
//Glide
public void onLowMemory() {
clearMemory();
}
public void clearMemory() {
// Engine asserts this anyway when removing resources, fail faster and consistently
Util.assertMainThread();
// memory cache needs to be cleared before bitmap pool to clear re-pooled Bitmaps too. See #687.
memoryCache.clearMemory();
bitmapPool.clearMemory();
arrayPool.clearMemory();
}
2.4.3、从Bitmap像素存储位置考虑
各个解码格式所占byte大小(1byte = 8bit):
/** * 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,16位;- 如果Bitmap使用
ARGB_8888
格式则占4 byte,32位;
Glide4.0
之前,Glide
默认使用RGB565
格式,比较省内存;内存开销是Picasso的一半。Glide4.0
之后,默认格式已经变成了ARGB_8888
格式了,这一优势也就不存在了。
2.5、防止内存泄露
Glide的做法是通过启动一个无UI空Fragment监听生命周期回调,看 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 销毁的时候,取消图片加载任务。
2.6、列表滑动加载的问题
2.6.1、加载错乱
由于RecyclerView或者LIstView的复用机制,网络加载图片开始的时候ImageView是第一个item的,加载成功之后ImageView由于复用可能跑到第10个item去了,在第10个item显示第一个item的图片肯定是错的。
常规的做法是:
- 给ImageView设置tag,tag一般是图片地址,更新ImageView之前判断tag是否跟url一致。
- 在item从列表消失的时候,取消对应的图片加载任务。。
2.6.2、队满任务过多
列表滑动,会有很多图片请求,如果是第一次进入,没有缓存,那么队列会有很多任务在等待。所以在请求网络图片之前,需要判断队列中是否已经存在该任务,存在则不加到队列去。
三、Glide缓存机制
3.1、缓存key
不管是内存缓存还是磁盘缓存,存储的时候肯定需要一个唯一 key 值。Glide生成缓存key的地方其实就在Engine的load方法中,通过重写equals和hashCode方法,来确保只有key对象的唯一性。
通过 URL + ignature + width + height 等等10个参数一起传入到EngineKeyFactory的buildKey()方法当中,从而构建出了一个EngineKey对象,这个EngineKey也就是Glide中的缓存Key了。
因此,
如果你图片的width或者height发生改变,也会生成一个完全不同的缓存Key。
- 4.4以前必须长宽相等Bitmap才可以复用;
- 而4.4及以后是Size >= 所需就可以复用。
//Engine
public synchronized <R> LoadStatus load(
GlideContext glideContext,
Object model,
Key signature,
int width,
int height,
Class<?> resourceClass,
Class<R> transcodeClass,
Priority priority,
DiskCacheStrategy diskCacheStrategy,
Map<Class<?>, Transformation<?>> transformations,
boolean isTransformationRequired,
boolean isScaleOnlyOrNoTransform,
Options options,
boolean isMemoryCacheable,
boolean useUnlimitedSourceExecutorPool,
boolean useAnimationPool,
boolean onlyRetrieveFromCache,
ResourceCallback cb,
Executor callbackExecutor) {
long startTime = VERBOSE_IS_LOGGABLE ? LogTime.getLogTime() : 0;
//创建EngineKey对象
EngineKey key = keyFactory.buildKey(model, signature, width, height, transformations,
resourceClass, transcodeClass, options);
......
return new LoadStatus(cb, engineJob);
}
//EngineKey
class EngineKey implements Key {
private final Object model;
private final int width;
private final int height;
private final Class<?> resourceClass;
private final Class<?> transcodeClass;
private final Key signature;
private final Map<Class<?>, Transformation<?>> transformations;
private final Options options;
private int hashCode;
EngineKey(
Object model,
Key signature,
int width,
int height,
Map<Class<?>, Transformation<?>> transformations,
Class<?> resourceClass,
Class<?> transcodeClass,
Options options) {
this.model = Preconditions.checkNotNull(model);
this.signature = Preconditions.checkNotNull(signature, "Signature must not be null");
this.width = width;
this.height = height;
this.transformations = Preconditions.checkNotNull(transformations);
this.resourceClass =
Preconditions.checkNotNull(resourceClass, "Resource class must not be null");
this.transcodeClass =
Preconditions.checkNotNull(transcodeClass, "Transcode class must not be null");
this.options = Preconditions.checkNotNull(options);
}
@Override
public boolean equals(Object o) {
if (o instanceof EngineKey) {
EngineKey other = (EngineKey) o;
return model.equals(other.model)
&& signature.equals(other.signature)
&& height == other.height
&& width == other.width
&& transformations.equals(other.transformations)
&& resourceClass.equals(other.resourceClass)
&& transcodeClass.equals(other.transcodeClass)
&& options.equals(other.options);
}
return false;
}
@Override
public int hashCode() {
if (hashCode == 0) {
hashCode = model.hashCode();
hashCode = 31 * hashCode + signature.hashCode();
hashCode = 31 * hashCode + width;
hashCode = 31 * hashCode + height;
hashCode = 31 * hashCode + transformations.hashCode();
hashCode = 31 * hashCode + resourceClass.hashCode();
hashCode = 31 * hashCode + transcodeClass.hashCode();
hashCode = 31 * hashCode + options.hashCode();
}
return hashCode;
}
....
}
3.1.1、缓存key所引发的问题
缓存key中一个重要参数信息就是图片的URL,这会造成两个问题:
-
同一张图片,动态URL的时候会发生重复加载的问题;
原因:
- 如七牛云上的图片会在原url后面添加一个token参数,而token又不是一成不变的,导致图片url变了,缓存key也变了,结果就造成了,明明是同一张图片,因为token不断在变,导致Glide缓存失效。
解决方案:
-
新建一个继承于GlideUrl的子类——MyGlideUrl类,在MyGlideUrl里重写getCacheKey方法,最后在Glide请求里面采用MyGlideUrl,如下:
Glide.with(this) .load(new MyGlideUrl(url)) .into(imageView);
-
同一个URL,返回不同的图片,前端显示的还是之前旧的图片。
原因:
- 因为虽然服务器返回的图片变了,但是图片 url 还是以前那个,其决定缓存 Key 的参数也不会变,Glide 就认为有该缓存,就会直接从缓存中获取,而不是重新下载,所以显示的还是以前的图片。
解决方案:
- 图片 url 不要固定。也就是说如果某个图片改变了,那么该图片的 url 也要跟着改变。(推荐)
- 使用 signature() 更改缓存 Key。我们刚刚知道了决定缓存 Key 的参数包括 signature,刚好 Glide 提供了 signature() 方法来更改该参数。(不推荐,增加前后端工作)具体如下:
Glide.with(this).load(url).signature(new ObjectKey(timeModified)).into(imageView);
其中 timeModified 可以是任意数据,这里用图片的更改时间。例如图片改变了,那么服务器应该改变该字段的值,然后随图片 url 一起返回给前端,这样前端加载的时候就知道图片改变了,需要重新下载。
- 禁用缓存。前端加载图片的时候设置禁用内存与磁盘缓存,这样每次加载都会重新下载最新的。(不推荐)
3.2、缓存类型
Glide 缓存机制可以说是设计的非常完美,考虑的非常周全,下面就以一张表格来说明下 Glide 缓存。
缓存类型 | 缓存代表 | 说明 |
---|---|---|
活动缓存 | ActiveResources | 如果当前对应的图片资源是从内存缓存中获取的,那么会将这个图片存储到活动资源中。 |
内存缓存 | LruResourceCache | 图片解析完成并最近被加载过,则放入内存中 |
磁盘缓存-资源类型 | DiskLruCacheWrapper | 被解码后的图片写入磁盘文件中 |
磁盘缓存-原始数据 | DiskLruCacheWrapper | 网络请求成功后将原始数据在磁盘中缓存 |
3.2.1、活动缓存
-
原理:弱引用 + HashMap,用来缓存正在使用中的图片。
-
读取:优先读取活动缓存。
-
写入:
- 从内存缓存读取并移出内存缓存,再写入活动缓存。
- 从磁盘或原始数据加载完成。
设计原理:保护不想被回收掉的图片不被 LruCache 算法回收掉。
比如我们 Lru 内存缓存 size 设置装 99 张图片,在滑动 RecycleView 的时候,如果刚刚滑动到 100 张,那么就会回收掉我们已经加载出来的第一张,这个时候如果返回滑动到第一张,会重新判断是否有内存缓存,如果没有就会重新开一个 Request 请求,很明显这里如果清理掉了第一张图片并不是我们要的效果。所以在从内存缓存中拿到资源数据的时候就主动添加到活动资源中,并且清理掉内存缓存中的资源。这么做很显然好处是
保护不想被回收掉的图片不被 LruCache 算法回收掉,充分利用了资源。
3.2.2、内存缓存
- 原理:LruCache最近最少原则,用来缓存最近被加载过,并且当前没有使用的图片。
- 读取:活动缓存读取不到,则从内存缓存读取。
- 写入:从活动缓存移出后。
3.2.3、磁盘缓存-资源类型
- 原理:DiskLruCache,被解码后的图片写入磁盘文件中。
- 读取:内存缓存读取不到,则从资源类型读取。
- 写入:原图资源解码后。
3.2.4、磁盘缓存-原始数据
- 原理:图片原始数据在磁盘中的缓存(从网络、文件中直接获得的原始数据)。
- 读取:当以上缓存都没读取到,则将原始数据解码转换读取出来。
- 写入:从网络、文件中获取到就将其写入磁盘。
3.2.5、流程总结
以开启内存缓存和磁盘缓存为例
- 先从活动缓存获取,获取到则直接显示;
- 获取不到,则从内存缓存获取,获取到则将图片移出内存缓存,写入活动缓存并显示;
- 获取不到,则从磁盘缓存-资源类型获取,获取到则将图片写入活动缓存并显示;
- 获取不到,则从磁盘缓存-原始数据获取并转换,获取到则将图片写入活动缓存和磁盘缓存-资源类型并显示;
- 获取不到,则从网络、文件获取,获取到则将图片写入磁盘缓存-原始数据,转换后,将图片写入活动缓存和磁盘缓存-资源类型并显示。
- 不再使用的图片则将移出活动缓存,写入内存缓存。
四、Glide加载GIF原理
- 首先需要区分加载的图片类型,即网络请求拿到输入流后,获取输入流的前三个字节,若为 GIF 文件头,则返回图片类型为 GIF。
- 确认为 GIF 动图后,会构建一个 GIF 的解码器(StandardGifDecoder),它可以从 GIF 动图中读取每一帧的数据并转换成 Bitmap,然后使用 Canvas 将 Bitmap 绘制到 ImageView 上,下一帧则利用 Handler 发送一个延迟消息实现连续播放,所有 Bitmap 绘制完成后又会重新循环,所以就实现了加载 GIF 动图的效果。