实现自己的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









0 0
原创粉丝点击