引言
在前面的安卓面试系列–OOM异常(二)中我们已经给大家简单分析了一下Universal-Image-Loader,并且还在文末给大家提供了一个已经封装好的工具类,不知道大家觉得好不好用呢?今天我们就来分析一下安卓中几大主流的图片加载框架的优缺点。
Universal-Image-Loader
我们先来回顾一下这个图片加载框架,主要是要进行两个配置,一个是图片下载前的配置,还有一个是图片显示配置,我们一个一个的来说。
1、导包
方式一:
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.3'
方式二:
导入jar包到libs目录下,jar包自行百度,这里就不提供了
2、权限配置
<!-- 如果想要加载网络图片就需要下面这个权限 -->
<uses-permission android:name="android.permission.INTERNET" />
<!-- 如果想要加载SD卡上的图片就需要下面这个权限 -->
<uses-permissionandroid:name="android.permission.WRITE_EXTERNAL_STORAGE" />
3、图片下载配置ImageLoaderConfiguration
a、使用默认配置:
ImageLoaderConfiguration configuration = ImageLoaderConfiguration.createDefault(this);
b、自己配置参数:
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
.memoryCacheExtraOptions(480, 800) // default = device screen dimensions 内存缓存文件的最大长宽
.diskCacheExtraOptions(480, 800, null) // 本地缓存的详细信息(缓存的最大长宽),最好不要设置这个
.taskExecutor(...)
.taskExecutorForCachedImages(...)
.threadPoolSize(3) // default 线程池内加载的数量
.threadPriority(Thread.NORM_PRIORITY - 2) // default 设置当前线程的优先级
.tasksProcessingOrder(QueueProcessingType.FIFO) // default
.denyCacheImageMultipleSizesInMemory()
.memoryCache(new LruMemoryCache(2 * 1024 * 1024)) //可以通过自己的内存缓存实现
.memoryCacheSize(2 * 1024 * 1024) // 内存缓存的最大值
.memoryCacheSizePercentage(13) // default
.diskCache(new UnlimitedDiscCache(cacheDir)) // default 可以自定义缓存路径
.diskCacheSize(50 * 1024 * 1024) // 50 Mb sd卡(本地)缓存的最大值
.diskCacheFileCount(100) // 可以缓存的文件数量
// default为使用HASHCODE对UIL进行加密命名, 还可以用MD5(new Md5FileNameGenerator())加密
.diskCacheFileNameGenerator(new HashCodeFileNameGenerator())
.imageDownloader(new BaseImageDownloader(context)) // default
.imageDecoder(new BaseImageDecoder()) // default
.defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default
.writeDebugLogs() // 打印debug log
.build(); //开始构建
配置好ImageLoaderConfiguration,一定不要忘记进行初始化操作(一般在application中进行初始化)
ImageLoader.getInstance().init(config);
注:上面的配置请根据自己的需要进行配置,不是所有的都要进行配置的
4、图片显示配置
a、首先要得到ImageLoader的实例(使用的单例模式)
ImageLoader imageLoader = ImageLoader.getInstance();
b、相关显示参数设置
DisplayImageOptions options = new DisplayImageOptions.Builder()
.showImageOnLoading(R.drawable.ic_stub) // 设置图片下载期间显示的图片
.showImageForEmptyUri(R.drawable.ic_empty) // 设置图片Uri为空或是错误的时候显示的图片
.showImageOnFail(R.drawable.ic_error) // 设置图片加载或解码过程中发生错误显示的图片
.resetViewBeforeLoading(false) // default 设置图片在加载前是否重置、复位
.delayBeforeLoading(1000) // 下载前的延迟时间
.cacheInMemory(false) // default 设置下载的图片是否缓存在内存中
.cacheOnDisk(false) // default 设置下载的图片是否缓存在SD卡中
.preProcessor(...)
.postProcessor(...)
.extraForDownloader(...)
.considerExifParams(false) // default
.imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2) // default 设置图片缩放方式
.bitmapConfig(Bitmap.Config.ARGB_8888) // default 设置图片的解码类型
.decodingOptions(...) // 图片的解码设置
.displayer(new SimpleBitmapDisplayer()) // default 还可以设置圆角图片new RoundedBitmapDisplayer(20)
.handler(new Handler()) // default
.build();
注:如果DisplayImageOption没有传递给ImageLoader.displayImage(…)方法,那么默认配置显示选项
(ImageLoaderConfiguration.defaultDisplayImageOptions(…))将被使用。
1).imageScaleType(ImageScaleType imageScaleType) //设置图片的缩放方式
缩放类型mageScaleType:
EXACTLY :图像将完全按比例缩小的目标大小
EXACTLY_STRETCHED:图片会缩放到目标大小完全
IN_SAMPLE_INT:图像将被二次采样的整数倍
IN_SAMPLE_POWER_OF_2:图片将降低2倍,直到下一减少步骤,使图像更小的目标大小
NONE:图片不会调整
2).displayer(BitmapDisplayer displayer) //设置图片的显示方式
显示方式displayer:
RoundedBitmapDisplayer(int roundPixels)设置圆角图片
FakeBitmapDisplayer()这个类什么都没做
FadeInBitmapDisplayer(int durationMillis)设置图片渐显的时间
SimpleBitmapDisplayer()正常显示一张图片
c、显示图片:
1、 ImageLoader.getInstance().displayImage(uri, imageView);
2、 ImageLoader.getInstance().displayImage(uri, imageView, options);
3、 ImageLoader.getInstance().displayImage(uri, imageView, listener);
4、 ImageLoader.getInstance().displayImage(uri, imageView, options, listener);
5、 ImageLoader.getInstance().displayImage(uri, imageView, options, listener, progressListener);
其中:
imageUrl 图片的URL地址
imageView 显示图片的ImageView控件
options DisplayImageOptions配置信息
listener 图片下载情况的监听
progressListener 图片下载进度的监听
- 方法1:最简单的方式,我们只需要定义要显示的图片的URL和要显示图片的ImageView。这种情况下,图片的显示选项会使用默认的配置
- 方法2:加载自定义配置的一个图片
- 方法3:加载带监听的一个图片
- 方法4:加载自定义配置且带监听的一个图片
参数最全的方法:
ImageLoader.getInstance().displayImage(uri, imageView, options,
new ImageLoadingListener() {
@Override
public void onLoadingStarted(String arg0, View arg1) {
//开始加载
}
@Override
public void onLoadingFailed(String arg0, View arg1,
FailReason arg2) {
//加载失败
}
@Override
public void onLoadingComplete(String arg0, View arg1,
Bitmap arg2) {
//加载成功
}
@Override
public void onLoadingCancelled(String arg0, View arg1) {
//加载取消
}
}, new ImageLoadingProgressListener() {
@Override
public void onProgressUpdate(String imageUri, View view, int current, int total) {
//加载进度
}
});
5、注意事项:
a、如果你的程序经常出现OOM,你可以尝试以下设置:
- 禁用在内存中缓存cacheInMemory(false);
- 减少配置的线程池的大小(.threadPoolSize(…)),建议1~5;
- 在显示选项中使用 .bitmapConfig(Bitmap.Config.RGB_565) . RGB_565模式消耗的内存比ARGB_8888模式少两倍;
- 配置中使用.diskCacheExtraOptions(480, 320, null);
- 配置中使用 .memoryCache(newWeakMemoryCache()) 或者完全禁用在内存中缓存(don’t call .cacheInMemory());
- 在显示选项中使用.imageScaleType(ImageScaleType.EXACTLY) 或 .imageScaleType(ImageScaleType.IN_SAMPLE_INT);
b、一定要对ImageLoaderConfiguration进行初始化,否则会报错;
c、缓存到Sd卡需要在AndroidManifest.xml文件中进行如下配置:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
d、内存缓存可以使用以下已经已实现的方法(ImageLoaderConfiguration.memoryCache(…)):
- 只使用强引用:
LruMemoryCache (缓存大小超过指定值时,删除最近最少使用的bitmap) --默认情况下使用
- 缓存使用弱引用和强引用:
UsingFreqLimitedMemoryCache (缓存大小超过指定值时,删除最少使的bitmap)
LRULimitedMemoryCache (缓存大小超过指定值时,删除最近最少使用的<span style="font-family: 'Helvetica Neue', Helvetica, 'Segoe UI', Arial, freesans, sans-serif;">bitmap) --默认值</span>
FIFOLimitedMemoryCache (缓存大小超过指定值时,按先进先出规则删除的<span style="font-family: 'Helvetica Neue', Helvetica, 'Segoe UI', Arial, freesans, sans-serif;">bitmap)</span>
LargestLimitedMemoryCache (缓存大小超过指定值时,删除最大的bitmap)
LimitedAgeMemoryCache (缓存对象超过定义的时间后删除)
- 只使用弱引用:
WeakMemoryCache(没有限制缓存)
e、硬盘缓存可以使用以下已经实现的方式(ImageLoaderConfiguration.diskCache(…))
UnlimitedDiskCache 不限制缓存大小(默认)
TotalSizeLimitedDiskCache (设置总缓存大小,超过时删除最久之前的缓存)
FileCountLimitedDiskCache (设置总缓存文件数量,当到达警戒值时,删除最久之前的缓存。如果文件的大小都一样的时候,可以使用该模式)
LimitedAgeDiskCache (不限制缓存大小,但是设置缓存时间,到期后删除)
6、总结
- 好了,到这里我们已经对UIL这个框架的基本使用的介绍已经结束了,简单总结一下,使用这个框架首先需要导包,配置权限,配置图片下载前的各项参数,配置图片显示的各项参数,然后就是通过ImageLoader实例对象的displayImage()方法展示图片。
- 最简单的就是传入一个url和一个ImageView控件,当然也可以添加自定义图片显示配置options,图片下载情况监听listener,主要监听图片下载开始、完成、失败、取消四种状态,图片下载进度监听progressListener。
- 然后我们还提供了几种内存缓存和硬盘缓存策略,先说内存缓存,默认的就是LruMemoryCache(强引用),当内存满了以后,删除最近最少使用的图片,还有一些其他的删除规则,比如时间先后,文件大小等等。再说硬盘缓存,跟内存缓存类似,也可以按照时间先后来删除,还可以设定缓存时间,到期删除。
你以为UIL到这里就结束了?作为一个有追求的程序员,怎么可以不看源码。
UIL图片框架缓存策略
关于UIL的三级缓存大家可以去看看我的这篇博客安卓面试系列–OOM异常(二),里面介绍了内存缓存、硬盘缓存以及网络下载三种缓存方式的调用顺序,这里就不多赘述了。在这里我想给大家讲一讲UIL框架中最著名的LruCache(最近最少使用)算法是怎么实现的。
点开UIL框架的源码,我们可以看到这样的一个目录结构:
其中:memory表示内存缓存目录,disk表示硬盘缓存目录
Lru缓存策略
我们先来讲一下内存缓存,首先分析一下LruMemoryCache这个类,这个类实现了一个接口MemoryCache,所以我们先来看下这个接口里面有什么方法:
/**
* 内存缓存的接口
*/
public interface MemoryCache {
/**
* 根据键把图片加入内存
*/
boolean put(String key, Bitmap value);
/**
* 根据键从内存中取出图片
*/
Bitmap get(String key);
/**
* 根据键从内存中移除图片
*/
Bitmap remove(String key);
/**
* 返回所有的键
*/
Collection<String> keys();
/**
* 清空所有数据
*/
void clear();
}
/**
* LRU 缓存策略 Least Recentlty Use 最近最少使用
*/
public class LruMemoryCache implements MemoryCache{
/**
* 链表数据结构存储
*/
private final LinkedHashMap<String, Bitmap> map;
/**
* 最大缓存大小
*/
private final int maxSize;
/**
* 当前使用的缓存大小
*/
private int size;
/**
* 构造方法初始化缓存类
*/
public LruMemoryCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);//ture,表示它是按照访问顺序进行排序的
}
/**
* 取出当前key对应的图片,并把这张图片放在list的尾部
*/
@Override
public final Bitmap get(String key) {
if (key == null) {
throw new NullPointerException("key == null");
}
synchronized (this) {
return map.get(key);
}
}
/**
* 添加图片,添加至末尾
*/
@Override
public final boolean put(String key, Bitmap value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
synchronized (this) {
//加入新图片的大小
size += sizeOf(key, value);
// 图片已经存在 ,替换原来的图片,
Bitmap previous = map.put(key, value);
if (previous != null) {
//移除旧图片大小
size -= sizeOf(key, previous);
}
}
trimToSize(maxSize);
return true;
}
/**
* 超过maxSize,不停的移除图片,直到小于maxSize
*/
private void trimToSize(int maxSize) {
// 循环判断当前缓存大小
while (true) {
String key;
Bitmap value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
}
if (size <= maxSize || map.isEmpty()) {
break;
}
// 链表结构最少使用的数据项
Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= sizeOf(key, value);
}
}
}
/**
* 移除图片
*/
@Override
public final Bitmap remove(String key) {
if (key == null) {
throw new NullPointerException("key == null");
}
synchronized (this) {
Bitmap previous = map.remove(key);
if (previous != null) {
size -= sizeOf(key, previous);
}
return previous;
}
}
@Override
public Collection<String> keys() {
synchronized (this) {
return new HashSet<String>(map.keySet());
}
}
/**
* 清空数据
*/
@Override
public void clear() {
trimToSize(-1);
}
/**
* 计算Bitmap的大小
*/
private int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
@Override
public synchronized final String toString() {
return String.format("LruCache[maxSize=%d]", maxSize);
}
}
总结
- 以上就是内存缓存最近最少使用算法的实现,总结一下,把一张图片加入到内存缓存中首先要判断键和值是否为空,如果为空就抛出异常,都不为空的情况下,我们使用一个同步块,先计算一下新加入内存的图片有多大,然后把它加入内存,再判断一下这张图片是否已经存在于内存,如果存在就把这张图片删除,更新一下内存。假如我们新添加的图片非常大,超过了我们的内存大小,这个时候就有必要开始尝试移除图片的工作。移除图片的工作主要在trimToSize(maxSize)这个方法中进行,主要通过一个while循环,判断,如果内存没有超过设定的最大值,那么就不需要删除任何对象,直接break。如果超过了,我们就通过map集合的迭代器取出第一个数据项,这个数据项就是最近最少使用的那一项,然后把它移除,再更新一下内存,再次判断,知道内存小于设定的最大值为止。这样我们就实现了最近最少使用算法。
先进先出策略
再给大家将一个先进先出的策略吧,直接上源码:
public class FIFOLimitedMemoryCache extends LimitedMemoryCache {
private final List<Bitmap> queue = Collections.synchronizedList(new LinkedList<Bitmap>());
public FIFOLimitedMemoryCache(int sizeLimit) {
super(sizeLimit);
}
@Override
public boolean put(String key, Bitmap value) {
if (super.put(key, value)) {
queue.add(value); //添加到队列的最后
return true;
} else {
return false;
}
}
@Override
public Bitmap remove(String key) {
Bitmap value = super.get(key);
if (value != null) {
queue.remove(value);
}
return super.remove(key);
}
@Override
public void clear() {
queue.clear();
super.clear();
}
@Override
protected int getSize(Bitmap value) {
return value.getRowBytes() * value.getHeight();
}
@Override
protected Bitmap removeNext() {
return queue.remove(0); //移除第一个
}
@Override
protected Reference<Bitmap> createReference(Bitmap value) {
return new WeakReference<Bitmap>(value);
}
}
- 可以看到,我们添加的时候是添加到queue的末尾,而移除的时候是移除第一个,这样就实现了FIFO(先进先出)算法。