实现自己的ImageLoader(2)-----LruCache与DiskLruCache缓存详解
来源:互联网 发布:科比2016数据 编辑:程序博客网 时间:2024/04/28 06:02
上篇博客说道,一个优秀的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 exceptione.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
- 实现自己的ImageLoader(2)-----LruCache与DiskLruCache缓存详解
- LruCache和DiskLruCache实现二级缓存的自定义ImageLoader
- Bitmap的缓存(LruCache,DiskLruCache)
- Andoird LruCache和DiskLruCache缓存详解
- 图片的LruCache缓存和DiskLruCache缓存
- 基与LruCache(内存缓存)和 DiskLruCache(硬盘缓存)的图片三级缓存工具类
- Android的缓存技术:LruCache和DiskLruCache
- Android的缓存策略:LruCache和DiskLruCache
- 打造完美的ImageLoader——LruCache+DiskLruCache
- 打造完美的ImageLoader——LruCache+DiskLruCache
- Android缓存源码分析(DiskLruCache,LruCache)
- 图片缓存-LruCache、DiskLruCache
- 安卓缓存LruCache、DiskLruCache、SharedPreference、SqlLite-2DiskLruCache
- ImageLoader使用的DiskLruCache硬盘缓存算法
- 通过DiskLruCache以及LruCache来构建自己项目的图片缓存框架
- LruCache内存缓存与DiskLruCache硬盘缓存完美集合
- LruCache,DiskLruCache实现分析
- 使用LruCache和DiskLruCache实现内存磁盘二级图片缓存
- 进程间通信
- opencv 初学者常见问题
- 毕业设计博客(一)
- python爬虫(爬游民星空壁纸)_图片损坏问题
- jsonObject.getString() 和jsonObject.optString()区别
- 实现自己的ImageLoader(2)-----LruCache与DiskLruCache缓存详解
- 指针和引用的区别
- 005 ZeroMQ REQ-<ROUTER-Dealer>-REP代理应答与请求
- 太空3D
- 欢迎使用CSDN-markdown编辑器
- pwnable之coin1
- poj 1094 Sorting It All Out 拓扑排序
- Fibonacci(POJ3070)
- PHP开源框架Laravel的安装与配置