彻底掌握如何有效处理高清大图:Android-Universal-Image-Loader框架解析(一)基本使用
在UIL框架中,最重要的技术莫过于缓存技术的使用了,缓存技术的使用不仅仅体现在UIL中,其实在Gallery里面也有很多的使用;
- 首先我们从内存缓存MemoryCache 说起
在初始化ImageLoader的时候,我们可以通过如下代码配设置使用何种内存缓存,以下代码表示使用LruMemoryCache来进行内存缓存
config.memoryCache(new LruMemoryCache(10));
默认的UIL框架中,内存缓存是什么
在ImageLoaderConfiguration.java中,有如下代码
if (memoryCache == null) {
memoryCache = DefaultConfigurationFactory.createMemoryCache(context, memoryCacheSize);
}
public static MemoryCache createMemoryCache(Context context, int memoryCacheSize) {
if (memoryCacheSize == 0) {
ActivityManager am = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
int memoryClass = am.getMemoryClass();
if (hasHoneycomb() && isLargeHeap(context)) {
memoryClass = getLargeMemoryClass(am);
}
memoryCacheSize = 1024 * 1024 * memoryClass / 8;
}
return new LruMemoryCache(memoryCacheSize);
}
因此UIL框架默认采用的内存缓存为LruMemoryCache
LruMemoryCache的核心是采用了最近最少使用算法(Lru),其大小为app可用内存的1/8, 实现了MemoryCache接口,那这个Lru是如何实现的,深拨下源码便可知晓
package com.nostra13.universalimageloader.cache.memory.impl;
import android.graphics.Bitmap;
import com.nostra13.universalimageloader.cache.memory.MemoryCache;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* A cache that holds strong references to a limited number of Bitmaps. Each time a Bitmap is accessed, it is moved to
* the head of a queue. When a Bitmap is added to a full cache, the Bitmap at the end of that queue is evicted and may
* become eligible for garbage collection.<br />
* <br />
* <b>NOTE:</b> This cache uses only strong references for stored Bitmaps.
*
* @author Sergey Tarasevich (nostra13[at]gmail[dot]com)
* @since 1.8.1
*/
public class LruMemoryCache implements MemoryCache {
private final LinkedHashMap<String, Bitmap> map;
private final int maxSize;
/** Size of this cache in bytes */
private int size;
/** @param maxSize Maximum sum of the sizes of the Bitmaps in this cache */
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);
}
/**
* Returns the Bitmap for {@code key} if it exists in the cache. If a Bitmap was returned, it is moved to the head
* of the queue. This returns null if a Bitmap is not cached.
*/
@Override
public final Bitmap get(String key) {
if (key == null) {
throw new NullPointerException("key == null");
}
synchronized (this) {
return map.get(key);
}
}
/** Caches {@code Bitmap} for {@code key}. The Bitmap is moved to the head of the queue. */
@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;
}
/**
* Remove the eldest entries until the total of remaining entries is at or below the requested size.
*
* @param maxSize the maximum size of the cache before returning. May be -1 to evict even 0-sized elements.
*/
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);
}
}
}
/** Removes the entry for {@code key} if it exists. */
@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); // -1 will evict 0-sized elements
}
/**
* Returns the size {@code Bitmap} in bytes.
* <p/>
* An entry's size must not change while it is in the cache.
*/
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);
}
}
LruMemoryCache.java核心作用是:
LruMemoryCache保存了限量的bitmap,并且每一张bitmap都保持着强引用关系;当把一个bitmap添加到满了的缓存的时候,在缓存末尾的图片会被删除,并且将被垃圾回收;而每一次访问一个bitmap的时候,会将该bitmap添加到队首;
LruMemoryCache.java核心成员是:
. LinkedHashMap<String, Bitmap> map 实现Lru算法的关键
. int maxSize 缓存的最大大小
. int size 当前缓存的大小
LruMemoryCache.java代码分析
第36行,构造LinkedHashMap,在构造的时候,将LinkedHashMap的参数accessOrder设置为true,其是Lru的关键所在,其表示map里面元素的排序顺序,为true的时候表示按照访问顺序来排序,为false的时候表示按照插入的顺序来访问;(LinkedHashMap这里不做特别分析,后面会在集合/多线程章节中详细分解)
第44行,get方法通过传入的key,获得对应的bitmap
第56行,put方法就是传入key和bitmap,放入到map中,放入的时候会做如下操作
首先根据sizeOf方法获得该图片的具体的字节大小,计算当前缓存的当前总大小,即62行的方法的含义
然后将该bitmap存入到map中,我们知道map的put方法中,key可以相同,但是value会覆盖前一个值,map的put方法返回的当前key的前一个对象,即63行的方法的含义
如果通过map的put方法返回的对象已经存在,则我们需要重新计算当前缓存大小,即减去该图片的字节,即64,65行的方法的含义
即将你存入一个对象,那么就得考虑是否超过了缓存的最大大小,顾通过trimToSize方法,删除最旧的对象,直到剩余总数等于或低于请求的大小。因为LinkedHashMap里面的数据是有序的,而且创建LinkedHashMap的时候,数据排序是以访问顺序来定的,即你只要使用了map的里面的元素(通过get方法)那么这个元素就会被排至末尾(Lru算法),顾在这个map中,排在对头的元素就是使用次数就是最少的;所以trimToSize方法使用了迭代器的方式,从头一个元素开始删除,直到大小小于等于maxSize
第105行,根据key值移除map中的对象,并重新计算当前缓存大小;
- 我们在看下硬盘缓存DiskCache
在初始化ImageLoader的时候,我们可以通过如下代码配设置使用何种硬盘缓存
config.diskCache(... ...)
默认的UIL框架中,硬盘缓存使用的是什么类?硬盘缓存是如何保存图片的?
在ImageLoaderConfiguration.java中,有如下代码
if (diskCache == null) {
if (diskCacheFileNameGenerator == null) {
diskCacheFileNameGenerator = DefaultConfigurationFactory.createFileNameGenerator();
}
}
在这段代码中,我们创建了FileNameGenerator的实例diskCacheFileNameGenerator,该类的作用是命名保存的图片,那在硬盘中一张图片的命名形式是怎么样的
public static FileNameGenerator createFileNameGenerator() {
return new HashCodeFileNameGenerator();
}
public class HashCodeFileNameGenerator implements FileNameGenerator {
@Override
public String generate(String imageUri) {
return String.valueOf(imageUri.hashCode());
}
}
这个类比较简单,是根据图片的url的哈希值来进行命名的。有了文件命名方式,我们自然就可以很好的创建硬盘缓存了,接着看ImageLoaderConfiguration.java中的一段代码
diskCache = DefaultConfigurationFactory
.createDiskCache(context, diskCacheFileNameGenerator, diskCacheSize, diskCacheFileCount);
public static DiskCache createDiskCache(Context context, FileNameGenerator diskCacheFileNameGenerator,
long diskCacheSize, int diskCacheFileCount) {
File reserveCacheDir = createReserveDiskCacheDir(context);
if (diskCacheSize > 0 || diskCacheFileCount > 0) {
File individualCacheDir = StorageUtils.getIndividualCacheDirectory(context);
try {
return new LruDiskCache(individualCacheDir, reserveCacheDir, diskCacheFileNameGenerator, diskCacheSize,
diskCacheFileCount);
} catch (IOException e) {
L.e(e);
// continue and create unlimited cache
}
}
File cacheDir = StorageUtils.getCacheDirectory(context);
return new UnlimitedDiskCache(cacheDir, reserveCacheDir, diskCacheFileNameGenerator);
}
可见我们会首先尝试创建LruDiskCache硬盘缓存,如果LruDiskCache创建失败,我们会创建UnlimitedDiskCache硬盘缓存;
LruDiskCache实现了DiskCache接口,DiskCache是硬盘缓存的统一接口,LruDiskCache也是基于Lru算法实现的一个硬盘缓存,同时他的缓存是有最大限度的,在彻底掌握如何有效处理高清大图:Android-Universal-Image-Loader框架解析(一)基本使用中,我们通过如下方法设置了最大硬盘存储空间为50M
config.diskCacheSize(50 * 1024 * 1024);
UnlimitedDiskCache继承自BaseDiskCache类,BaseDiskCache实现了DiskCache接口,他没有采用Lru算法,也没有限制最大缓存大小;针对这个类我们就不做单独分析了,有兴趣的自己看下源码
总结:在硬盘缓存中
. 默认缓存到硬盘缓存的实现类是LruDiskCache.java,他采用了Lru算法保证按访问顺序排列,并限制了最大存储大小
. 默认缓存到硬盘的图片的命名方式是以图片URL的哈希值来命名的
我们来看下LruDiskCache.java
LruDiskCache.java核心成员是:
. DiskLruCache cache,真正的硬盘缓存类,LruDiskCache通过操作DiskLruCache来实现对象的增删改查操作
. FileNameGenerator fileNameGenerator,如何命名缓存到硬盘中的图片,默认为图片URL的哈希值
. Bitmap.CompressFormat compressFormat,图片的压缩格式,默认为png,因为png是无损压缩
. int compressQuality ,图片的压缩质量,默认为100,compressQuality和compressFormat会被bitmap的compress方法所使用;bitmap的compress的圆形是
compress(CompressFormat format, int quality, OutputStream stream)
其含义是按照format指定的图片格式以及quality指定的画质,将图片转换为stream输出流
举个例子,如果compressFormat为png,compressQuality的值为60,则这张图片会按照png的图片格式,保留60%的图像品质写入到输出流中,当然这种方式会使得图片失真;
compress方法压缩的是存储大小,即你放到disk上的大小,他并没有改变图片占用内存的大小,不过我们可以通过BitmapFactory.Options options = new BitmapFactory.Options()的方式改变图片占用内存的大小
LruDiskCache.java核心方法:
. 初始化
private void initCache(File cacheDir, File reserveCacheDir, long cacheMaxSize, int cacheMaxFileCount)
throws IOException {
try {
cache = DiskLruCache.open(cacheDir, 1, 1, cacheMaxSize, cacheMaxFileCount);
} catch (IOException e) {
L.e(e);
if (reserveCacheDir != null) {
initCache(reserveCacheDir, null, cacheMaxSize, cacheMaxFileCount);
}
if (cache == null) {
throw e; //new RuntimeException("Can't initialize disk cache", e);
}
}
}
通过调用DiskLruCache的全局静态方法完成DiskLruCache实例的创建cache
. 根据图片URL获得存储的文件
@Override
public File get(String imageUri) {
DiskLruCache.Snapshot snapshot = null;
try {
snapshot = cache.get(getKey(imageUri));
return snapshot == null ? null : snapshot.getFile(0);
} catch (IOException e) {
L.e(e);
return null;
} finally {
if (snapshot != null) {
snapshot.close();
}
}
}
可见硬盘缓存中在得到DiskLruCache实例之后,LruDiskCache变可以拿到DiskLruCache的快照类,通过快照类就可以找到对应的保存的文件
. 根据图片URL将图片缓存起来
@Override
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
if (editor == null) {
return false;
}
OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
boolean copied = false;
try {
copied = IoUtils.copyStream(imageStream, os, listener, bufferSize);
} finally {
IoUtils.closeSilently(os);
if (copied) {
editor.commit();
} else {
editor.abort();
}
}
return copied;
}
可见硬盘缓存中在得到DiskLruCache实例之后,LruDiskCache变可以拿到DiskLruCache的Editor类,通过Editor类就可以将对应的输入流写到DiskLruCache的Entry中
总结:
. LruDiskCache是硬盘缓存对外暴露的实体类,其具体实现的硬盘缓存是DiskLruCache;
. LruDiskCache默认压缩图片格式是PNG,默认压缩比率是100,即保持原画质
. LruDiskCache默认硬盘缓存的文件名是图片URL的哈希值
. LruDiskCache在初始化的时候会获得DiskLruCache实例
. LruDiskCache通过DiskLruCache的快照类完成获取缓存的文件
. LruDiskCache通过DiskLruCache的编辑类完成将流或者图片写入到DiskLruCache的Entry中,从而完成添加的动作
. LruDiskCache通过DiskLruCache的remove方法根据传入的图片URL完成对一个缓存的删除操作
- 既然硬盘缓存的真正实现是DiskLruCache,那就让我们细看一下他的实现
DiskLruCache.java概览
DiskLruCache缓存在文件系统中创建了一个有限的空间,在这个空间当中存储了一条条的数据,每一条的数据是由key,value来对应的;key需要满足[a-z0-9_-]{1,64}的正则表达,value是由字节组成的数据,可被流或者文件获取;每一条数据我们可以称呼为一个Entry
DiskLruCache.java展示
在默认情况下,被缓存的文件的目录地址是/data/packagename/cache/uil-images目录,下图展示的是demo当中的被缓存的内容
一个DiskLruCache最重要的部分有两个
. journal文件,例如上图中的journal
. 每一条cache entry,例如上图中的6cdg7wdkrtb8dmd05gdj8x87n0
打开journal文件你可以发现
其中红色方框依次表示:libcore.io.DiskLruCache,DiskLruCache的版本,应用程序的版本,每个条目中保存值的个数
其中绿色方框表示被缓存的内容的操作记录
这里我们只要知道journal文件里面有哪些内容即可,就不详细了解journal文件的生成过程了,其过程在DiskLruCache.java的rebuildJournal方法中
我们在打开6cdg7wdkrtb8dmd05gdj8x87n0文件看下
可以发现里面多是一些字节信息
顾我们可以简单的猜测下DiskLruCache的增删改查的过程
. 增加一个缓存:新建一个根据图片url的哈希值来命名的文件,将缓存以字节的形式存入并更新journal文件
. 删除一个缓存:在一个map,根据缓存图片的url来进行查找,从而删除缓存
. 查询一个缓存:在map中,科举图片url来查找具体的文件, 并读取文件保存的字节信息
DiskLruCache.java核心成员
.LinkedHashMap<String, Entry> lruEntries:核心成员,使用Lru算法存储缓存的内容
.static final String JOURNAL_FILE = “journal”;:硬盘缓存内容操作镜像文件,通过这个文件可以知道缓存被进行了怎样的操作
.Callable cleanupCallable:硬盘缓存是有大小限制的,顾当缓存大小超过总大小的时候,就会启动cleanupCallable来执行清空任务,直到小于总缓存大小;这点和LruMemoryCache没有什么差别;这里我们注意到这是一个callable接口,他和runable接口一样,都是实现线程的接口,只不过callable接口实现了call方法,runable接口实现了run方法;callable接口可以有返回值,而runable接口没有返回值;
注意点:
Callable接口支持返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取‘将来’结果;当不调用此方法时,主线程不会阻塞!
DiskLruCache.java核心方法
. 打开DiskLruCache
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount)
throws IOException {
/*省略部分代码*/
// If a bkp file exists, use it instead.
File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
if (backupFile.exists()) {
File journalFile = new File(directory, JOURNAL_FILE);
// If journal file also exists just delete backup file.
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile, journalFile, false);
}
}
/*以上都是对journal文件的操作*/
// Prefer to pick up where we left off.
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize, maxFileCount);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
cache.journalWriter = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
return cache;
/*以上都是对journal文件的操作*/
} catch (IOException journalIsCorrupt) {
System.out
.println("DiskLruCache "
+ directory
+ " is corrupt: "
+ journalIsCorrupt.getMessage()
+ ", removing");
cache.delete();
}
}
// Create a new empty cache.
directory.mkdirs();
/*创建空的DiskLruCache*/
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize, maxFileCount);
/*在journal文件中写入固定内容*/
cache.rebuildJournal();
return cache;
}
. 存储一个缓存,主要有两个步骤
第一步是获得DiskLruCache类的Editor类,对应edit方法
第二步是将Edit类中的保存的字节信息写到到Entry中,对应commit方法
private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
|| entry.sequenceNumber != expectedSequenceNumber)) {
return null; // Snapshot is stale.
}
if (entry == null) {
entry = new Entry(key);
lruEntries.put(key, entry);
} else if (entry.currentEditor != null) {
return null; // Another edit is in progress.
}
Editor editor = new Editor(entry);
entry.currentEditor = editor;
// Flush the journal before creating files to prevent file leaks.
journalWriter.write(DIRTY + ' ' + key + '\n');
journalWriter.flush();
return editor;
}
第4行,根据传入key,在lruEntriesmap中查找一个Entry entry,一个entry表示一个缓存的字节内容
第11行,如果这个entry为空,则新建一个entry,并保存到lruEntries中
第16行,创建DiskLruCache的Edit类
当创建完Edit类之后,LruDiskCache.java在save方法中
@Override
public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
if (editor == null) {
return false;
}
OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
boolean copied = false;
try {
copied = IoUtils.copyStream(imageStream, os, listener, bufferSize);
} finally {
IoUtils.closeSilently(os);
if (copied) {
editor.commit();
} else {
editor.abort();
}
}
return copied;
}
通过editor.newOutputStream(0)的方法来新建一个输出流,然后将需要被缓存的内容的流写入到输出流中,然后通过调用editor.commit()完成最后的刷新操作
. 获得一个存储的缓存,这里也分为两个步骤
第一步:根据key查找对应的快照,对应get方法
第二步:根据Snapshot的snapshot.getFile(0)方法返回这个缓存的文件
public synchronized Snapshot get(String key) throws IOException {
checkNotClosed();
validateKey(key);
Entry entry = lruEntries.get(key);
if (entry == null) {
return null;
}
if (!entry.readable) {
return null;
}
// Open all streams eagerly to guarantee that we see a single published
// snapshot. If we opened streams lazily then the streams could come
// from different edits.
File[] files = new File[valueCount];
InputStream[] ins = new InputStream[valueCount];
try {
File file;
for (int i = 0; i < valueCount; i++) {
file = entry.getCleanFile(i);
files[i] = file;
ins[i] = new FileInputStream(file);
}
} catch (FileNotFoundException e) {
// A file must have been deleted manually!
for (int i = 0; i < valueCount; i++) {
if (ins[i] != null) {
Util.closeQuietly(ins[i]);
} else {
break;
}
}
return null;
}
redundantOpCount++;
journalWriter.append(READ + ' ' + key + '\n');
if (journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
return new Snapshot(key, entry.sequenceNumber, files, ins, entry.lengths);
}
这里删除和关闭方法也不做分析,比较简单
总结:
. DiskLruCache.java通过LinkedHashMap的方式来实现Lru算法
. DiskLruCache.java 硬盘缓存是有大小限制的,顾当缓存大小超过总大小的时候,就会启动 cleanupCallable来执行清空任务,知道小于总缓存大小
. LinkedHashMap<String, Entry> lruEntries里面的key对应图片URL的哈希值,其Entry保存了该key,
那么当新增一个缓存的时候,我们会创建一个Entry,这个Entry练保存了缓存的key(为以后查找做准备),然后创建Editor类,将缓存的字节信息输出到输出流中,即目标缓存文件中;
当查找一个缓存的时候,我们会根据key在lruEntries查找这样Entry,找到对应的Entry之后,创建snapshot类,通过Entry找到对应的文件并获取文件的输入流,获取里面的字节信息,在调用snapshot的getfile方法返回这个缓存
当清空一个缓存的时候,删除掉这个key对应的所有缓存文件,然后重新计算当前缓存的大小,然后从lruEntries中删除对应的key
- 最后看下硬盘缓存UnlimitedDiskCache
.UnlimitedDiskCache.java概述
UnlimitedDiskCache也是硬盘缓存的一种,在创建LruDiskCache失败的时候,就会创建UnlimitedDiskCache,他缓存大小没有限制,也不采用Lru来控制缓存;UnlimitedDiskCache继承自BaseDiskCache类,BaseDiskCache实现了DiskCache接口;而LruDiskCache只实现了DiskCache接口;
UnlimitedDiskCache针对缓存的增删改查比较简单,就是通过缓存的url地址,查找到对应的文件,然后通过对流的操作来实现增删改查的操作
似乎这样UnlimitedDiskCache的实现方式比较简单,那为什么LruDiskCache不按照这样的方式去做了?那还不是因为LruDiskCache支持Lru算法,并且缓存大小有限制