Android 图片加载之浅谈 Universal Image Loader

文章目录
  1. 1. 简介 Universal Image Loader
  2. 2. Demo 用法
  3. 3. OOM 优化方案
  4. 4. 参考文献

坚持做自己懒得做但正确的事情,就能得到别人想得到却得不到的东西。

Android 开发中,会遇到加载大批量图片或异步加载图片的场景,这其中,或许会遇到一些问题,如图片错乱,甚至是著名的 OOM(Out Of Memory) 问题。谈及图片加载,本期先谈谈老牌的图片加载框架,即 Universal Image Loader,距今都有着五六年的光景了。

简介 Universal Image Loader

Universal Image Loader 下面简称 UIL,迄今为止在 gitrep 上 Android library 中排行第一。UIL 旨在作为加载、缓存和展示图片的的工具,其强大、灵活且高度定制化。此外,其提供了很多配置选项,在图片的加载和缓存过程中表现良好。重要特点如下:

  • 异步或同步地多线程加载图片,其可来源于网络、文件系统、项目文件夹 assets 或 drawable 中等;
  • 可任意配置 ImageLoader 的参数,如线程执行器、下载器、解码器、内存和磁盘缓存、图片展示选项等;
  • 支持许多图片展示调用选项的配置,如 stub images、缓存切换、解码选项和位图的处理与展示等;
  • 可将图片缓存进内存和磁盘,如设备的文件系统或 SD 卡;
  • 支持加载过程包括下载过程的监听。

除此之外,根据开发中的实际情况,可以由控件的如 ImageView 的大小对 Bitmap 进行裁剪,减少其所占的内存;较好地控制图片的加载过程,如暂停加载图片、重新开始加载图片,应用场景像:在 ListView 或 RecyclerView 中使用,滑动时暂停加载图片,停止滑动时加载图片;可在较慢的网络下加载图片,等等。

Demo 用法

Demo 地址:UniversalImageLoaderDemo

引入 Gradle 依赖

1
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'

Android Manifest:

1
2
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

MyApplication 类如下:

1
2
3
4
5
6
7
8
9
10
11
12
public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();

// 配置 ImageLoader 的默认参数
ImageLoaderConfiguration configuration = ImageLoaderConfiguration.createDefault(this);
ImageLoader.getInstance().init(configuration);
}

}

ImageLoaderConfiguration 是指定的 ImageLoader 的配置参数,看其源码,运用了建造者模式 (Builder Pattern)。其中,调用 createDefault() 方法配置默认的参数。此外,选择性配置的全部项参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
File cacheDir = StorageUtils.getCacheDirectory(context);
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
.memoryCacheExtraOptions(480, 800) // 默认的设备屏幕尺寸
.diskCacheExtraOptions(480, 800, null)
.taskExecutor(...)
.taskExecutorForCachedImages(...)
.threadPoolSize(3) // 默认的线程池尺寸
.threadPriority(Thread.NORM_PRIORITY - 2) // 默认的线程优先级为 3
.tasksProcessingOrder(QueueProcessingType.FIFO) // 默认的任务处理顺序为先进先出
.denyCacheImageMultipleSizesInMemory()
.memoryCache(new LruMemoryCache(2 * 1024 * 1024))
.memoryCacheSize(2 * 1024 * 1024)
.memoryCacheSizePercentage(13) // 默认的内存缓存大小占比为 13
.diskCache(new UnlimitedDiskCache(cacheDir)) // 默认的磁盘缓存
.diskCacheSize(50 * 1024 * 1024)
.diskCacheFileCount(100)
.diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) // 默认的磁盘缓存文件名生成器
.imageDownloader(new BaseImageDownloader(context)) // 默认的图像下载器
.imageDecoder(new BaseImageDecoder()) // 默认的图像解码器
.defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // 默认的展示图像选项
.writeDebugLogs()
.build();

布局文件很简单,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">


<ImageView
android:id="@+id/iv_picture"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@mipmap/ic_launcher"/>

</FrameLayout>

只放置了一个 ImageView,UIL 主要提供的图片加载方法为 displayImage()、loadImage() 和 loadImageSync()。注意,loadImageSync() 要求是同步的,涉及不能在主线程里的网络操作时,不使用。

loadImage()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
final ImageView imageView = (ImageView) findViewById(R.id.iv_picture);
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037194_2965.jpg";

ImageLoader
.getInstance()
.loadImage(imageUrl, new ImageLoadingListener() {
@Override
public void onLoadingStarted(String imageUri, View view) {

}

@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {

}

@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
imageView.setImageBitmap(loadedImage);
}

@Override
public void onLoadingCancelled(String imageUri, View view) {

}
});

回调方法 onLoadingComplete() 中,将 loadedImage 设置到指定的 ImageView 上。开发中,在只关注图片加载事件的情况下,也可以用 ImageLoadingListener 的实现类 SimpleImageLoadingListener 代替 ImageLoadingListener 如下:

1
2
3
4
5
6
7
8
9
10
11
final ImageView imageView = (ImageView) findViewById(R.id.iv_picture);
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037194_2965.jpg";

ImageLoader.getInstance().loadImage(imageUrl, new SimpleImageLoadingListener() {

@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
imageView.setImageBitmap(loadedImage);
}
});

进一步,想指定图片大小的做法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
final ImageView imageView = (ImageView) findViewById(R.id.iv_picture);
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037194_2965.jpg";

ImageSize imageSize = new ImageSize(100, 100);

ImageLoader.getInstance().loadImage(imageUrl, imageSize, new SimpleImageLoadingListener() {

@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
imageView.setImageBitmap(loadedImage);
}
});

当然,这里仅为简单的示例。实际开发中有着复杂的场合应用,会用到 DisplayImageOptions 类,其配置参考如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
DisplayImageOptions options = new DisplayImageOptions.Builder()
.showImageOnLoading(R.drawable.ic_stub) // resource or drawable
.showImageForEmptyUri(R.drawable.ic_empty) // resource or drawable
.showImageOnFail(R.drawable.ic_error) // resource or drawable
.resetViewBeforeLoading(false) // 默认 Loading 之前不重置 View
.delayBeforeLoading(1000)
.cacheInMemory(false) // 默认不内存缓存
.cacheOnDisk(false) // 默认不磁盘缓存
.preProcessor(...)
.postProcessor(...)
.extraForDownloader(...)
.considerExifParams(false) // 默认不考虑可交换图像文件参数
.imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2) // 默认的图像比例类型
.bitmapConfig(Bitmap.Config.ARGB_8888) // 默认的位图配置
.decodingOptions(...)
.displayer(new SimpleBitmapDisplayer()) // 默认的位图展示
.handler(new Handler()) // 默认
.build();

根据需要,上面的代码可以稍作重构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
final ImageView imageView = (ImageView) findViewById(R.id.iv_picture);
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037194_2965.jpg";

ImageSize imageSize = new ImageSize(100, 100);

// 显示图像的配置
DisplayImageOptions options = new DisplayImageOptions.Builder()
.cacheInMemory(true) // 设置内存缓存
.cacheOnDisk(true) // 设置磁盘缓存
.bitmapConfig(Bitmap.Config.RGB_565) // 设置位图显示质量低于 ARGB_8888
.build();

ImageLoader.getInstance().loadImage(imageUrl, imageSize, options, new SimpleImageLoadingListener() {

@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
super.onLoadingComplete(imageUri, view, loadedImage);
imageView.setImageBitmap(loadedImage);
}
});

对图像显示选项作了一些定制配置,如设置内存缓存和磁盘缓存,则不需要每次从网络中加载图片;设置位图显示质量为 RGB_565 而不是 ARGB_8888,在图像显示要求不是特别高的情况下,可以少消耗 2 倍的内存。

displayImage()

1
2
3
4
5
6
7
8
9
10
11
12
final ImageView imageView = (ImageView) findViewById(R.id.iv_picture);
String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037194_2965.jpg";

DisplayImageOptions options = new DisplayImageOptions.Builder()
.showImageOnLoading(R.drawable.ic_load)
.showImageOnFail(R.drawable.ic_load_fail)
.cacheInMemory(true)
.cacheOnDisk(true)
.bitmapConfig(Bitmap.Config.RGB_565)
.build();

ImageLoader.getInstance().displayImage(imageUrl, imageView, options);

使用 displayImage() 无需添加 ImageLoadingListener 接口。此外,注意到添加了 showImageOnLoading() 方法,一开始显示 ic_load 图片;也添加了 showImageOnFail() 方法,加载错误则显示 ic_load_fail 图片。值得注意的是,该方法 displayImage() 同时会根据控件的大小和 imageScaleType() 自动裁剪图片。重构 MyApplication 类,加入 Log 打印如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();

ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(this)
.writeDebugLogs() // 打印 Log
.build();

ImageLoader.getInstance().init(configuration);
}

}

关注下面的 Log:

1
2
3
4
5
09-29 21:03:30.622 6243-6279/com.iamasoldier6.universalimageloaderdemo D/ImageLoader: Start display image task [http://img.my.csdn.net/uploads/201309/01/1378037194_2965.jpg_768x1184]
09-29 21:03:30.622 6243-6279/com.iamasoldier6.universalimageloaderdemo D/ImageLoader: Load image from network [http://img.my.csdn.net/uploads/201309/01/1378037194_2965.jpg_768x1184]
09-29 21:03:30.622 6243-6279/com.iamasoldier6.universalimageloaderdemo D/ImageLoader: Cache image on disk [http://img.my.csdn.net/uploads/201309/01/1378037194_2965.jpg_768x1184]
09-29 21:03:37.292 6243-6279/com.iamasoldier6.universalimageloaderdemo D/ImageLoader: Cache image in memory [http://img.my.csdn.net/uploads/201309/01/1378037194_2965.jpg_768x1184]
09-29 21:03:37.292 6243-6243/com.iamasoldier6.universalimageloaderdemo D/ImageLoader: Display image in ImageAware (loaded from NETWORK) [http://img.my.csdn.net/uploads/201309/01/1378037194_2965.jpg_768x1184]

读 Log 信息,

第一条:打印出图片的 url 和图片的最大宽度和高度,默认是设备的宽高;

第二条:显示加载的图片来自网络;

第三条:缓存图片到磁盘;

第四条:缓存图片到内存;

第五条:加载自网络,在 ImageAware 中展示图片。

引人兴趣的是,displayImage() 方法中传入 ImageLoadingProgressListener 接口,当加载网络图片时,可以显示图片下载的进度,关键代码如下:

1
2
3
4
5
6
7
8
ImageLoader.getInstance().displayImage(imageUrl, imageView, options, new SimpleImageLoadingListener(), new ImageLoadingProgressListener() {

@Override
public void onProgressUpdate(String imageUri, View view, int current,
int total)
{


}
});

回调方法 onProgressUpdate() 中,可以设置图片的加载进度。

以上,加载的网络图片显示如下:

除此之外,加载来自 drawable 和 assets 的 imageUrl 示例如下:

1
2
String imageUrl = "drawable://" + R.drawable.image_loader_flower;
String imageUrl = "assets://image_loader_flower.png";

更有甚者,加载来自 SD 卡和 Content Provider 的示例如下,不常用:

1
2
3
4
"file:///mnt/sdcard/image.png" // 来自 SD 卡
"file:///mnt/sdcard/video.mp4" // 来自 SD 卡,极短小的视频
"content://media/external/images/media/13" // 来自 Content Provider
"content://media/external/video/media/13" // 来自 Content Provider,极短小的视频

OOM 优化方案

OOM 是加载图片时绕不开的痛,针对 UIL 的优化 OOM 的方案如下:

  • 禁用内存缓存,即设置 cacheInMemory(false),可使用 MemoryAnalyzer 来配合检测,查看代码中是否有内存泄漏;
  • 减少配置时的线程池大小,其配置选项中通过调用 threadPoolSize() 方法,推荐 1-5 ;
  • 如上面提到的,显示选项中调用 bitmapConfig(Bitmap.Config.RGB_565),其消耗内存比之 ARGB_8888 减少 2 倍;
  • 显示选项中调用 imageScaleType(ImageScaleType.EXACTLY);
  • 配置选项中调用 diskCacheExtraOptions(480, 320, null);
  • 指定加载图像 ImageView 的宽高。

更多细节使用及注意事项参见 DocumentationIssues

至此,UIL 的基本了解、使用和注意事项介绍完毕,下一期将从源码角度探究这一经典的图片加载框架。

本人才疏学浅,如有疏漏错误之处,望读者中有识之士不吝赐教,谢谢。

1
Email: [email protected] / WeChat: Wolverine623

您也可以关注我个人的微信公众号码农六哥第一时间获得博客的更新通知,或后台留言与我交流

参考文献

1.https://github.com/nostra13/Android-Universal-Image-Loader/wiki

2.http://blog.csdn.net/xiaanming/article/details/26810303