上篇博客说道,一个优秀的ImageLoader应该具有内存缓存能力和磁盘缓存能力,而缓存能力该怎么实现?
这里就要引出LruCache与DiskLruCache,先说LruCache
LruCache是Android的一个缓存类,通常用于实现内存缓存
public class LruCache<K, V>
LruCache有一个LinkedHashMap用于存储我们的缓存
private final LinkedHashMap<K, V> map;
当我们创建新的LruCache时
mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
protected int sizeOf(String key, Bitmap value) {
return value.getRowBytes() * value.getHeight() / 1024;
};
};
需要传递一个参数以及重写里面的sizeof方法
public LruCache(int maxSize) {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
this.maxSize = maxSize;
this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
}
这里我们可以看到LruCache初始化时创建了LinkedHashMap
而sizeof方法在LruCache的代码里是默认返回1
protected int sizeOf(K key, V value) {
return 1;
}
sizeof必须要重写,因为系统是通过sizeof来确定储存的Value的大小,得到了Value的大小系统才可以给Value分配空间,不然无法储存,内部逻辑我们等会再看。
我们传入的Value是一个Bitmap对象,Bitmap的大小等于value.getRowBytes() * value.getHeight(),因为分配内存时的单位是KB,所以我们还需要除以1024统一单位
现在我们已经拿到了一个LruCache对象,接下来我们就要往里添加数据了,调用它的put方法
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
我来梳理一下put方法的逻辑
1.先判断key还有value是不是空,空就报错
2.增加size,其中safeSizeof调用我们重写的sizeof方法
3.向map里加入数据
4.判断previous是不是为空,这里我说一下,因为LinkedHashmap继承Hashmap,HashMap一个很重要的性质就是key不能重复,当key重复时HashMap会覆盖原来的key,并把先前的Value返回,如果key没有重复,那么则返回null,具体源码可以看这个博客http://www.cnblogs.com/children/archive/2012/10/02/2710624.html
5.当我们key重复时,size要把原来加上去的长度减回来,再调用entryRemoved(false, key, previous, value);方法,这个方法是一个空实现,主要是用于一些资源回收的工作,如果有必要的话我们要重写这个方法
6.调用trimToSize方法,这个方法是LRU(Least Recently Used)算法的具体实现。设想一下,当我们缓存空间满了的时候,我们还想往里加入缓存怎么办,我们是不是得删除一些不那么重要的缓存(要是删除了重要的缓存,用户体验会不好),而这些个缓存的重要度则通过使用次数来反应。下面是trimTosize的源码
public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
if (size <= maxSize) {
break;
}
Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
我们可以看出,trimTosize会先判断我们的size是不是超过maxsize了,如果超过了就删除最少使用的缓存,删除完再判断,如果空间不够还要再删,直到满足为止。
以上就是加入缓存的逻辑,而get方法的逻辑没有什么好说了,跟HashMap差不多。
接下来我们看看DiskLruCache(长文慎入)
DiskLruCache是用于磁盘缓存的一个类,如果我们需要把缓存缓存到sd卡里,那么就要用到DiskLruCache。有趣的是DiskLruCache并不属于Android源码的一部分,但是得到了Android官方文档的推荐,不知道是哪位大神造的轮子。鉴于Google已经被墙了,这个类可以从我刚上传的资源下载http://download.csdn.net/detail/yuwang_00/9480250
回到主题,DiskLruCache和LruCache不一样,它并不能通过构造方法来创建,它提供了open方法用于创建自身,有点类似于数据库。
当我们使用时,我们需要这样写
File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
if (!diskCacheDir.exists()) {
diskCacheDir.mkdirs();
}
if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
try {
mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,
DISK_CACHE_SIZE);
mIsDiskLruCacheCreated = true;
} catch (IOException e) {
// TODO: handle exception
e.printStackTrace();
}
}
拿到外部储存的路径File对象(一般是sd卡下的路径),如果外部储存不存在则调用手机的缓存空间,跟LruCache一样
private File getDiskCacheDir(Context context, String uniqueName) {
boolean exter = Environment.getExternalStorageState().equals(
Environment.MEDIA_MOUNTED);
final String cachePath;
if (exter) {
cachePath = context.getExternalCacheDir().getPath();
} else {
cachePath = context.getCacheDir().getPath();
}
return new File(cachePath + File.separator + uniqueName);
}
判断SD的可用空间还能不能够我所需的缓存空间,这里返回的单位是MB,所以我们定义DISK_CACHE_SIZE的时候要1024*1024*想要的大小
private long getUsableSpace(File path) {
if(Build.VERSION.SDK_INT>=VERSION_CODES.GINGERBREAD){
return path.getUsableSpace();
}
final StatFs stats=new StatFs(path.getPath());
return (long)stats.getBlockSize()*(long)stats.getAvailableBlocks();
}
前期工作做完了,我们来看看open方法的四个参数,第一个参数我觉得猜都猜到了,是缓存的存储目录,如果像上面这么写的话,缓存会存储在/sdcard/Android/data/你的包名/data,当然你也可以自己指定一个目录,看你选择了。如果是前者,那么app卸载的时候缓存就会随app一起删除,如果自己指定的话,就不会删除(流氓软件就是这样来的,都卸载了还留一大堆缓存)
第二个参数是appversion,一般设置为1,如果版本号变了的话会清空之前的缓存文件,不过很多时候我们升级版本的话还是要保留之前版本缓存的数据的,所以一般不要改这个参数
第三个参数是valueCount,表示单个节点所对应的数据的个数,一般设为1
第四个参数是缓存的大小,注意单位应该是MB,LruCache则是KB
最后我们来看一下open方法的具体实现
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
if (maxSize <= 0) {
throw new IllegalArgumentException("maxSize <= 0");
}
if (valueCount <= 0) {
throw new IllegalArgumentException("valueCount <= 0");
}
// prefer to pick up where we left off
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
IO_BUFFER_SIZE);
return cache;
} catch (IOException journalIsCorrupt) {
// System.logW("DiskLruCache " + directory + " is corrupt: "
// + journalIsCorrupt.getMessage() + ", removing");
cache.delete();
}
}
// create a new empty cache
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
先看构造方法
private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
this.directory = directory;
this.appVersion = appVersion;
this.journalFile = new File(directory, JOURNAL_FILE);
this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
this.valueCount = valueCount;
this.maxSize = maxSize;
}
方法里真没啥可讲的,传值,创建日志文件,创建Tmp文件(
让我们回到open方法的逻辑,检查参数合不合法,然后检查cache的journalFile是否存在(以防万一),如果有了,就把它改成我们的。如果没有,就新建一个。rebuildJournal方法初始化了journalwriter,有了缓存文件writer对象,我们就能轻松的进行写入操作。
下面就到了DiskLruCache的写入操作,跟LruCache的LinkedHashmap不一样,DiskLruCache只是通过LinkedHashmap进行中转的,真正存储在journalFileTmp中。估计是大神为了安全性?稳定性?考虑,下面是写入操作
DiskLruCache.Editor editor = mDiskLruCache.edit(key);
if (editor != null) {
OutputStream outputStream = editor
.newOutputStream(DISK_CACHE_INDEX);
if (downloadUrlToStream(url, outputStream)) {
editor.commit();
} else {
editor.abort();
}
mDiskLruCache.flush();
}
可以看出,DiskLruCache是采用Editor的方式读写数据,其中editor源码如下
public Editor edit(String key) throws IOException {
return edit(key, ANY_SEQUENCE_NUMBER);
}
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;
}
我们来解析一下写入流程
1.checkNotClosed();检查我们的journalWrite是否关闭,如果关闭了就报错,当我们关闭journalWrite的时候会把它置null
public boolean isClosed() {
return journalWriter == null;
}
private void checkNotClosed() {
if (journalWriter == null) {
throw new IllegalStateException("cache is closed");
}
}
2.检查key是否合法,主要是看有没有特殊字符或者长度太长等
private void validateKey(String key) {
if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
throw new IllegalArgumentException(
"keys must not contain spaces or newlines: \"" + key + "\"");
}
}
3.然后从一个LinkedHashmap对象中取出Entry对象,Entry entry = lruEntries.get(key);
4.第一个if是判断tmp文件是否正在被读取,毕竟文件不能被同时读取,同时读取会出错的
5.要是在LinkedHashmap对象中没找到Entry则新建一个然后加入,LinkedHashmap实际上起到一个目录的作用,这样做得好处显而易见,重复输入的时候就不必打开文件去一个一个比对数据,毕竟打开输入流对内存的占用还是挺高的,一不留神就容易OOM。
6.第三个else if判断tmp文件是不是正在被其他程序写入,并发会出问题
7.创建一个新的Editor,通过刚才的Entry对象
7.声明当前正在编辑文件entry.currentEditor = editor;
8.返回这个Editor
好了,现在我们拿到了Editor,我们接着往下看newOutputStream方法
public OutputStream newOutputStream(int index) throws IOException {
synchronized (DiskLruCache.this) {
if (entry.currentEditor != this) {
throw new IllegalStateException();
}
return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
}
}
而Entry类里的getDirtyFile
public File getDirtyFile(int i) {
return new File(directory, key + "." + i + ".tmp");
}
一切都清晰了,这步就是完成dirty缓存文件.tmp的创建工作,并且拿到它的FilterOutputStream对象,FaultHidingOutputStream是继承于FilterOutputStream,构造方法并没有重写。
至于downloadUrlToStream方法是自己定义的一个方法,代码在下一篇博客实现三级缓存的时候再贴,它的作用是收到传入的FilterOutputStream后,将通过url下载好的图片buffer流写入.tmp文件
最后一步,editor.commit();提交
public void commit() throws IOException {
if (hasErrors) {
completeEdit(this, false);
remove(entry.key); // the previous entry is stale
} else {
completeEdit(this, true);
}
}
要是有错的话就会回滚,没错的话继续
private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
Entry entry = editor.entry;
if (entry.currentEditor != editor) {
throw new IllegalStateException();
}
// if this edit is creating the entry for the first time, every index must have a value
if (success && !entry.readable) {
for (int i = 0; i < valueCount; i++) {
if (!entry.getDirtyFile(i).exists()) {
editor.abort();
throw new IllegalStateException("edit didn't create file " + i);
}
}
}
for (int i = 0; i < valueCount; i++) {
File dirty = entry.getDirtyFile(i);
if (success) {
if (dirty.exists()) {
File clean = entry.getCleanFile(i);
dirty.renameTo(clean);
long oldLength = entry.lengths[i];
long newLength = clean.length();
entry.lengths[i] = newLength;
size = size - oldLength + newLength;
}
} else {
deleteIfExists(dirty);
}
}
redundantOpCount++;
entry.currentEditor = null;
if (entry.readable | success) {
entry.readable = true;
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
if (success) {
entry.sequenceNumber = nextSequenceNumber++;
}
} else {
lruEntries.remove(entry.key);
journalWriter.write(REMOVE + ' ' + entry.key + '\n');
}
if (size > maxSize || journalRebuildRequired()) {
executorService.submit(cleanupCallable);
}
}
这里主要是把一些变量置null,比如entry.currentEditor,然后把Dirty缓存文件.tmp改名为clean缓存文件,另外无论是dirty文件或者clean文件在journal中有记录,格式为
journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
写入说完了,下面就是读取
DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
if (snapshot != null) {
FileInputStream fileInputStream = (FileInputStream) snapshot
.getInputStream(DISK_CACHE_INDEX);
FileDescriptor fileDescriptor = fileInputStream.getFD();
bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(
fileDescriptor, reqWidth, reqHeight);
get方法内部逻辑是从上文我们提到的LinkedHashmap取出Entry,拥有了这个Entry我们就相当于拥有了缓存文件,因为Entry中的getCleanFile方法可以拿到Clean文件
对象,有了Clean文件对象,我们就可以创建FileInputStream,有了FileInputStream就可以把图片显示出来,这里我是拿到了FileDescriptor,通过FileDescriptor实现图片压缩功能,这是上一篇博客的事情了。
到这里缓存分析就全部结束了,下面就是源码地址http://download.csdn.net/detail/yuwang_00/9480256