图片加载是客户端开发中重要的一个模块,如今也有很多优秀的开源库(UIL 、Picasso 、Glide 、Fresco )。大部分图片库都采用了Memory缓存和Disk缓存。
1. Memory缓存(LruCache) 最直接的方法就是看源码
1.1 成员变量 1 2 3 4 5 private final LinkedHashMap<K, V> map; private int size;private int maxSize;
map用于存放缓存的Bitmap size当前缓存的总大小 maxSize是缓存允许的最大大小,由构造函数赋值。
1.2 重要方法 1.2.1 添加数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public final V put (K key, V value) { 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; }
(1) 首先计算存入的数据大小。 (2) 添加数据的大小。 (3) 保存到map中。 (4) 如果之前key保存的数据存在减去之前数据占用的大小。 (5) 检查缓存。
1.2.2 检查缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 private 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.entrySet().iterator().next(); if (toEvict == null ) { break ; } key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); size -= safeSizeOf(key, value); evictionCount++; } entryRemoved(true , key, value, null ); } }
循环判断当前缓存大小是否超过最大值,如果超过清除使用。 LinkedHashMap提供特殊的构造方法来创建链接哈希映射,该哈希映射的迭代顺序就是最后访问其条目的顺序,从近期访问最少到近期访问最多的顺序(访问顺序)。这种映射很适合构建 LRU 缓存, 这里不做深入解释。所以第一条数据是很久没使用的数据,即为删除的数据。将缓存大小减去删除数据的大小。
1.2.3 获取数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public final V get (K key) { if (key == null ) { throw new NullPointerException("key == null" ); } V mapValue; synchronized (this ) { mapValue = map.get(key); if (mapValue != null ) { hitCount++; return mapValue; } missCount++; } .... }
2. Disk缓存(DiskLruCache) *2.1 成员变量 *
1 2 3 4 5 6 7 8 9 10 11 12 13 private final File directory; private final File journalFile;private final File journalFileTmp;private final int appVersion;private final long maxSize;private final int valueCount;private long size = 0 ;private Writer journalWriter; private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0 , 0.75f , true );
*2.2 重要方法 *
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public static DiskLruCache open (File directory, int appVersion, int valueCount, long maxSize) throws IOException { 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) { cache.delete(); } } 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; }
(1)创建DiskLruCache实例对象。 (2)判断文件系统中是否存在缓存系统信息的文件。 (3)根据不同的情况分别初始化
首先了解 : DIRTY这个字样都不代表着什么好事情,意味着这是一条脏数据。没错,每当我们调用一次DiskLruCache的edit()方法时,都会向journal文件中写入一条DIRTY记录,表示我们正准备写入一条缓存数据,但不知结果如何。然后调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,意味着这条“脏”数据被“洗干净了”,调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 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 ; } if (entry == null ) { entry = new Entry(key); lruEntries.put(key, entry); } else if (entry.currentEditor != null ) { return null ; } Editor editor = new Editor(entry); entry.currentEditor = editor; journalWriter.write(DIRTY + ' ' + key + '\n' ); journalWriter.flush(); return editor; }
从缓存系统获取编辑器,然后想缓存信息文件中添加记录数据。
然后从Edit中获取OutputStream,从而网文件系统中写文件。
1 2 3 4 5 6 7 8 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))); } }
当文件写完以后成功则调用Edit.commit(),失败则调用Edit.abort()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 public void commit () throws IOException { if (hasErrors) { completeEdit(this , false ); remove(entry.key); } else { completeEdit(this , true ); } } public void abort () throws IOException { completeEdit(this , false ); }
继续看最重要的保存数据方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 private synchronized void completeEdit (Editor editor, boolean success) throws IOException { Entry entry = editor.entry; .... 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); } }
如果成功将DirtyFile转化为CleanFile,然后添加文件描述。 如果失败则删除DirtyFile,然后添加文件描述。 最后检查缓存大小,清理缓存系统。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 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 ; } InputStream[] ins = new InputStream[valueCount]; try { for (int i = 0 ; i < valueCount; i++) { ins[i] = new FileInputStream(entry.getCleanFile(i)); } } catch (FileNotFoundException e) { return null ; } redundantOpCount++; journalWriter.append(READ + ' ' + key + '\n' ); if (journalRebuildRequired()) { executorService.submit(cleanupCallable); } return new Snapshot(key, entry.sequenceNumber, ins); }
每当我们调用get()方法去读取一条缓存数据时,就会向journal文件中写入一条READ记录。因为每次读取都会向journal文件中写入一条READ记录,所以也需要检查缓存。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 private final Callable<Void> cleanupCallable = new Callable<Void>() { @Override public Void call () throws Exception { synchronized (DiskLruCache.this ) { if (journalWriter == null ) { return null ; } trimToSize(); if (journalRebuildRequired()) { rebuildJournal(); redundantOpCount = 0 ; } } return null ; } };
缓存清理任务。首先清理缓存文件,然后重新生成journal文件。