05 ImageLoader 常见问题

01 基础框架
02 请求队列
03 三级缓存
04 图片加载
05 常见问题
06 项目源码

内存泄漏

当一个对象已经不需要再使用本该被回收时,另外一个正在使用的对象持有它的引用从而导致它不能被回收,这导致本该被回收的对象不能被回收而停留在堆内存中,这就产生了内存泄漏。

内存泄漏是造成应用程序 OOM 的主要原因之一。我们知道 Android 系统为每个应用程序分配的内存是有限的,而当一个应用中产生的内存泄漏比较多时,这就难免会导致应用所需要的内存超过系统分配的内存限额,这就造成了内存溢出从而导致应用 Crash。

由于单例的静态特性使得其生命周期和应用的生命周期一样长,如果一个对象已经不再需要使用了,而单例对象还持有该对象的引用,就会使得该对象不能被正常回收,从而导致了内存泄漏。我们的 ImageLoader 中就有几个单例对象,如 ImageLoader、LoaderManager,此时我们就要注意内存泄漏的可能性。

在 ImageLoader 中,持有的对象有 IBitmapCache、RequestQueue、ImageLoaderConfig,从业务上来讲它们的生命周期都是与 ImageLoader 一致的,不满足内存泄漏的定义,但这不代表不存在内存泄漏的可能。我们不但要检查直接的引用关系,还要检查引用链,即引用的引用所持有的对象引用。ImageLoader 持有的三个引用所指向的引用都直接或间接持有了 BitmapRequest,而 BitmapRequest 又持有了 ImageView 对象。也就是说当请求队列还在异步加载图片时,我们即使退出某个界面,但依然持有了(可能是大量的)ImageView 的对象引用,即发生了内存泄漏。

所以我们不能持有 ImageView 的强引用(StrongReference),它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误。使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收;只有在内存空间不足时,软引用才会被垃圾回收器回收。而具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。如果一个对象仅持有虚引用,那么它相当于没有引用,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列关联使用。

此处,我们选择使用弱引用,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class BitmapRequest implements Comparable<BitmapRequest> {
// public ImageView imageView;
private Reference<ImageView> mImageViewRef;
public BitmapRequest(ImageView imageView, String uri) {
this.mImageViewRef = new WeakReference<>(imageView);
this.imageUri = uri;
imageView.setTag(uri);
}
public ImageView getImageView() {
return mImageViewRef.get();
}
// 代码省略...
}

其他类中 request.imageView 代码都要改成 request.getImageView()。

通过检查代码来找出内存泄漏的原因,是有一定的难度的,而且很是繁琐。我们可以使用 Android LeakCanary 工具,自动检测出内存泄漏。

直接集成:

1
2
3
4
5
6
dependencies {
debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'
releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3'
// Optional, if you use support library fragments:
debugImplementation 'com.squareup.leakcanary:leakcanary-support-fragment:1.6.3'
}

直接在Application中使用,然后运行APP就会自动检测,检测到会在另一个APP上通知,显示详情。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TestApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (LeakCanary.isInAnalyzerProcess(this)) {
// This process is dedicated to LeakCanary for heap analysis.
// You should not init your app in this process.
return;
}
LeakCanary.install(this);
// Normal app init code...
}
}

然后在 AndroidManifest.xml 中配置自定义的 Application。

1
2
3
4
5
6
7
8
<application
android:name=".TestApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher"
android:supportsRtl="true"
android:theme="@style/AppTheme">

我们在 RequestDispatcher 的 run 方法中加入 Thread.sleep(100000); 然后退出当前界面。此时 LeakCanary 检测到内存泄漏,如下图所示。

内存溢出

我们在 Demo 中使用 ImageLoader 加载大量图片时,发生 OOM 异常。如下图所示:

在 Demo 中,使用了双缓存技术。由于内存不够,就去从 SD 卡中加载图片,而 DiskCache 中获取本地图片时没有使用图片解码器,这就容易导致加载一张大图就会发生 OOM 异常。

1
2
3
4
5
6
7
8
9
10
public class DiskCache implements IBitmapCache {
// 从 SD 卡缓存中获取图片
@Override
public Bitmap get(BitmapRequest request) {
final InputStream inputStream = getInputStream(Md5Helper.toMD5(request.imageUri));
return BitmapFactory.decodeStream(inputStream);
}
...
}

使用图片解码器修改上述代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public synchronized Bitmap get(final BitmapRequest request) {
// 图片解析器
BitmapDecoder decoder = new BitmapDecoder() {
@Override
public Bitmap decodeBitmapWithOption(BitmapFactory.Options options) {
final InputStream inputStream = getInputStream(Md5Helper.toMD5(request.imageUri));
Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
IOUtil.closeQuietly(inputStream);
return bitmap;
}
};
return decoder.decodeBitmap(request.getImageViewWidth(),
request.getImageViewHeight());
}

项目总结


ImageLoader 系列到这里就算结束了,我们从基本架构、具体实现、设计上面详细的阐述了一个简单、可扩展性较好的 ImageLoader 实现过程。

我们一步一步地实现 ImageLoader 项目的过程中,多次出现代码重构,每次修改一处代码都会导致其他代码跟着修改。有能力有经验的架构师,几乎都是采用第一种思路,抽象出整体架构,画出架构图,如下所示。

参考上图,我们再来捋一遍整体流程。

ImageLoader 类是用户的入口,用户在通过配置类初始化ImageLoader 之后就可以向 ImageLoader 提交加载图片的请求了。ImageLoader 内部维护了一个请求队列,用户提交的加载图片的请求会在内部被封装成 BitmapRequest 对象,然后将这些对象放到请求队列中。在创建队列时会创建用户指定数量(默认为CPU个数 + 1)的线程来加载图片,这些线程在内部命名为 RequestDispatcher,它们在 run 函数中不断地获取队列中的加载请求,然后交给对应的 Loader 加载图片。

为了方便用户的扩展,我们引入了 Loader 这个抽象,因为在 ImageLoader 中只支持两种图片 uri 的加载,即网络图片 uri 和本地文件的 uri。网络图片一般以 “http://“ 或者 “https://“ 开头,而本地图片的 uri 格式却是 “file://“ 开头,ImageLoader 内部通过图片 uri 的格式的不同使用不同的 Loader 来加载图片,这样后续用户就可以注册 Loader 来实现其他格式的加载,例如 “drawable:// + 图片名” 来加载 res/drawable 中的图片等。这样保证了 ImageLoader 可加载图片 uri 格式的可扩展性。Loader 会通过 LoaderManager 来进行管理,如果需要注册自己的 Loader 实现,则调用 LoaderManager 的 register 函数即可。如果你传递进去的图片 uri 是无效,例如格式错误,那么 LoaderManager 会返回一个默认的 Loader,这个默认的 Loader 名为 NullLoader,它其实什么也不做,只是为了防止在外部进行判空而已,这种模式成为 Null Object 设计模式。当然,在加载图片之前我们会从缓存中读取,如果有缓存我们则不加载。

Loader 加载完图片之后会先更新 UI,即将图片显示到对应的 ImageView 上,在构造 BitmapRequest 时内部已经将图片的 uri 设置为 ImageView 的 tag 了。图片加载完成后判断 ImageView 的 tag 和 uri 是否相等,如果相等则将图片显示到 ImageView 上,否则不更新 ImageView。这一步很重要,很多朋友在使用 ImageLoader 时出现问题基本上就是由于没有设置 ImageView 的 tag。

加载图片的先后顺序是由加载策略决定的,策略相关的内容没有在架构图中给出。加载策略决定了请求在队列中的排序,在将请求添加到队列中时会给每个请求设置一个序号,队列将根据这个序号对请求进行排序。这样我们就可以知道哪个请求是先添加进来的,也可以很方便的实现自己的策略类来定制自己的加载策略,比如最后加载到队列中的请求最先加载。比如我们在 ListView 滚动时,最后添加到队列中的图片请求应该是我们最急需显示的,我们它们就在手机的当前屏幕,而前面的请求对应的 ImageView 已经被复用,即使它们加载完成它们也不会被显示,因为 ImageView 的 tag 已经变化了。因此,策略的灵活性依然很重要。

在加载完图片并且更新UI之后,我们会将图片缓存起来。内置的缓存类型有四种,无缓存、内存缓存、sd卡缓存、内存和sd卡的双缓存,这四种缓存都实现了 Cache 接口,如果你这四种缓存类型还不能满足你的需求,那么你可以实现 Cache 接口,然后实现自己的缓存逻辑,然后在配置 ImageLoader 时设置需要的缓存类型(具体配置后续说明),如果不配置则默认使用的是内存缓存。这里我们又看到了一个面向接口编程的例子,即 ImageLoader 只依赖于 Cache 接口的抽象,而不是说依赖于某个具体的缓存类,这样用户就可以很方面的实现自己的缓存逻辑,并且将缓存实现注入到sdk中。当然,上述的 Loader、加载策略实现也是基于同样的理论基础,就是说过很多遍的“面向接口编程”。

用户调用 displayImage 请求加载图片,ImageLoader 将这个加载图片请求封装成一个 Request,然后加入到队列中。几个色眯眯的调度子线程不断地从队列中获取请求,然后根据 uri 的格式获取到对应的 Loader 来加载图片。在加载图片之前首先会查看缓存中是否含有目标图片,如果有缓存则使用缓存,否则加载目标图片。获取到图片之后,我们会将图片投递给 ImageView 进行更新,如果该 ImageView 的 tag 与图片的 uri 是一样的,那么则更新 ImageView,否则不处理。使用 ImageView 的 tag 与图片的 uri 进行对比是为了防止图片错位显示的问题,这在 ImageLoader 中是很重要的一步。如果目标图片没有缓存,第一次从 uri 中加载后会加入缓存中,当然从 sdcard 中加载的图片我们只会缓存到内存中,而不会再缓存一份到 sd 卡的另一个目录中。这样,整个加载过程也就完成了。

参考链接:
https://blog.csdn.net/bboyfeiyu/article/details/43195413
https://blog.csdn.net/bboyfeiyu/article/details/43195705
https://blog.csdn.net/bboyfeiyu/article/details/44155857
https://blog.csdn.net/bboyfeiyu/article/details/44172273