ANDROID缓存机制&一个缓存框架推荐
1、先推荐一个轻量级缓存框架——ACache(ASimpleCache)
- 1:轻,轻到只有一个JAVA文件。
- 2:可配置,可以配置缓存路径,缓存大小,缓存数量等。
- 3:可以设置缓存超时时间,缓存超时自动失效,并被删除。
- 4:支持多进程。
- 1、替换SharePreference当做配置文件
- 2、可以缓存网络请求数据,比如oschina的android客户端可以缓存http请求的新闻内容,缓存时间假设为1个小时,超时后自动失效,让客户端重新请求新的数据,减少客户端流量,同时减少服务器并发量。
- 3、您来说...
2、Android缓存机制
2.1 内存缓存——LruCache源码分析
2.1.1 LRU
2.1.2 LruCache实现原理
- /*
- * 初始化LinkedHashMap
- * 第一个参数:initialCapacity,初始大小
- * 第二个参数:loadFactor,负载因子=0.75f
- * 第三个参数:accessOrder=true,基于访问顺序;accessOrder=false,基于插入顺序
- */
- public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
- super(initialCapacity, loadFactor);
- init();
- this.accessOrder = accessOrder;
- }
2.1.3 LruCache源码分析
- //核心数据结构
- private final LinkedHashMap<K, V> map;
- // 当前缓存数据所占的大小
- private int size;
- //缓存空间总容量
- private int maxSize;
- private static final int CACHE_SIZE = 4 * 1024 * 1024;//4Mib
- LruCache<String,Bitmap> bitmapCache = new LruCache<String,Bitmap>(CACHE_SIZE){
- @Override
- protected int sizeOf(String key, Bitmap value) {
- return value.getByteCount();//自定义Bitmap数据大小的计算方式
- }
- };
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);}
private int safeSizeOf(K key, V value) { int result = sizeOf(key, value); if (result < 0) { throw new IllegalStateException("Negative size: " + key + "=" + value); } return result;}
/** * 给对应key缓存value,并且将该value移动到链表的尾部。 */public final V put(K key, V value) { if (key == null || value == null) { throw new NullPointerException("key == null || value == null"); } V previous; synchronized (this) { // 记录 put 的次数 putCount++; // 通过键值对,计算出要保存对象value的大小,并更新当前缓存大小 size += safeSizeOf(key, value); /* * 如果 之前存在key,用新的value覆盖原来的数据, 并返回 之前key 的value * 记录在 previous */ previous = map.put(key, value); // 如果之前存在key,并且之前的value不为null if (previous != null) { // 计算出 之前value的大小,因为前面size已经加上了新的value数据的大小,此时,需要再次更新size,减去原来value的大小 size -= safeSizeOf(key, previous); } } // 如果之前存在key,并且之前的value不为null if (previous != null) { /* * previous值被剔除了,此次添加的 value 已经作为key的 新值 * 告诉 自定义 的 entryRemoved 方法 */ entryRemoved(false, key, previous, value); } //裁剪缓存容量(在当前缓存数据大小超过了总容量maxSize时,才会真正去执行LRU) trimToSize(maxSize); return previous;}
public void trimToSize(int maxSize) { /* * 循环进行LRU,直到当前所占容量大小没有超过指定的总容量大小 */ 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 || map.isEmpty()) { break; } /** * 执行到这,表示当前缓存数据已超过了总容量,需要执行LRU,即将最近最少使用的数据清除掉,直到数据所占缓存空间没有超标; * 根据前面的原理分析,知道,在链表中,链表的头结点是最近最少使用的数据,因此,最先清除掉链表前面的结点 */ Map.Entry<K, V> toEvict = map.entrySet().iterator().next(); key = toEvict.getKey(); value = toEvict.getValue(); map.remove(key); // 移除掉后,更新当前数据缓存的大小 size -= safeSizeOf(key, value); // 更新移除的结点数量 evictionCount++; } /* * 通知某个结点被移除,类似于回调 */ entryRemoved(true, key, value, null); }} trimToSize()方法的作用就是为了保证当前数据的缓存大小不能超过我们指定的缓存总大小,如果超过了,就会开始移除最近最少使用的数据,直到size符合要求。trimToSize()方法在put()的时候一定会调用,在get()的时候有可能会调用。 (6)get方法获取缓存数据 get方法源码如下: /** * 根据key查询缓存,如果该key对应的value存在于缓存,直接返回value;* 访问到这个结点时,LinkHashMap会将它移动到双向循环链表的的尾部。* 如果如果没有缓存的值,则返回null。(如果开发者重写了create()的话,返回创建的value)*/public final V get(K key) { if (key == null) { throw new NullPointerException("key == null"); } V mapValue; synchronized (this) { // LinkHashMap 如果设置按照访问顺序的话,这里每次get都会重整数据顺序 mapValue = map.get(key); // 计算 命中次数 if (mapValue != null) { hitCount++; return mapValue; } // 计算 丢失次数 missCount++; } /* * 官方解释: * 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时 * 候,用这个key执行了put方法,那么此时就发生了冲突,我们在Map中删除这个创建的值,释放被创建的值,保留put进去的值。 */ V createdValue = create(key); if (createdValue == null) { return null; } /*************************** * 不覆写create方法走不到下面 * ***************************/ /* * 正常情况走不到这里 * 走到这里的话 说明 实现了自定义的 create(K key) 逻辑 * 因为默认的 create(K key) 逻辑为null */ synchronized (this) { // 记录 create 的次数 createCount++; // 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值 mapValue = map.put(key, createdValue); // 如果之前存在相同key的value,即有冲突。 if (mapValue != null) { /* * 有冲突 * 所以 撤销 刚才的 操作 * 将 之前相同key 的值 重新放回去 */ map.put(key, mapValue); } else { // 拿到键值对,计算出在容量中的相对长度,然后加上 size += safeSizeOf(key, createdValue); } } // 如果上面 判断出了 将要放入的值发生冲突 if (mapValue != null) { /* * 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了 * 告诉 自定义 的 entryRemoved 方法 */ entryRemoved(false, key, createdValue, mapValue); return mapValue; } else { // 上面 进行了 size += 操作 所以这里要重整长度 trimToSize(maxSize); return createdValue; }} get()方法的思路就是: 1)先尝试从map缓存中获取value,即mapVaule = map.get(key);如果mapVaule != null,说明缓存中存在该对象,直接返回即可; 2)如果mapVaule == null,说明缓存中不存在该对象,大多数情况下会直接返回null;但是如果我们重写了create()方法,在缓存没有该数据的时候自己去创建一个,则会继续往下走,中间可能会出现冲突,看注释; 3)注意:在我们通过LinkedHashMap进行get(key)或put(key,value)时都会对链表进行调整,即将刚刚访问get或加入put的结点放入到链表尾部。 (7)entryRemoved() entryRemoved的源码如下: /** * 1.当被回收或者删掉时调用。该方法当value被回收释放存储空间时被remove调用* 或者替换条目值时put调用,默认实现什么都没做。* 2.该方法没用同步调用,如果其他线程访问缓存时,该方法也会执行。* 3.evicted=true:如果该条目被删除空间 (表示 进行了trimToSize or remove) evicted=false:put冲突后 或 get里成功create后* 导致* 4.newValue!=null,那么则被put()或get()调用。*/protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
2.1.4 LruCache的使用
LruCache 自身并没有释放内存,只是 LinkedHashMap中将数据移除了,如果数据还在别的地方被引用了,还是有泄漏问题,还需要手动释放内存;
覆写
entryRemoved
方法能知道 LruCache 数据移除是是否发生了冲突(冲突是指在map.put()的时候,对应的key中是否存在原来的值),也可以去手动释放资源;
2.2 磁盘缓存(文件缓存)——DiskLruCache分析
2.2.1 DiskLruCache实现原理
private final class Entry { private final String key; /** Lengths of this entry‘s files. */ private final long[] lengths; /** True if this entry has ever been published */ private boolean readable; /** The ongoing edit or null if this entry is not being edited. */ private Editor currentEditor; /** The sequence number of the most recently committed edit to this entry. */ private long sequenceNumber; private Entry(String key) { this.key = key; this.lengths = new long[valueCount]; } public String getLengths() throws IOException { StringBuilder result = new StringBuilder(); for (long size : lengths) { result.append(‘ ‘).append(size); } return result.toString();} /** * Set lengths using decimal numbers like "10123". */ private void setLengths(String[] strings) throws IOException { if (strings.length != valueCount) { throw invalidLengths(strings); } try { for (int i = 0; i < strings.length; i++) { lengths[i] = Long.parseLong(strings[i]); } } catch (NumberFormatException e) { throw invalidLengths(strings); } } private IOException invalidLengths(String[] strings) throws IOException { throw new IOException("unexpected journal line: " + Arrays.toString(strings)); } public File getCleanFile(int i) { return new File(directory, key + "." + i); } public File getDirtyFile(int i) { return new File(directory, key + "." + i + ".tmp"); }}
private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0, 0.75f, true);在LruCache中,由于数据是直接缓存中内存中,map中数据的建立是在使用LruCache缓存的过程中逐步建立的,而对于DiskLruCache,由于数据是缓存在本地文件,相当于是持久保存下来的一个文件,即使程序退出文件还在,因此,map中数据的建立,除了在使用DiskLruCache过程中建立外,map还应该包括之前已经存在的缓存文件,因此,在获取DiskLruCache的实例时,DiskLruCache会去读取journal这个日志文件,根据这个日志文件中的信息,建立map的初始数据,同时,会根据journal这个日志文件,维护本地的缓存文件。构造DiskLruCache的方法如下:
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;}其中,
以一个DIRTY前缀开始的,后面紧跟着缓存图片的key。以DIRTY这个这个前缀开头,意味着这是一条脏数据。每当我们调用一次DiskLruCache的edit()方法时,都会向journal文件中写入一条DIRTY记录,表示我们正准备写入一条缓存数据,但不知结果如何。然后调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,意味着这条“脏”数据被“洗干净了”,调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。
在CLEAN前缀和key后面还有一个数值,代表的是该条缓存数据的大小。
因此,我们可以总结DiskLruCache中的工作流程:
1)初始化:通过open()方法,获取DiskLruCache的实例,在open方法中通过readJournal(); 方法读取journal日志文件,根据journal日志文件信息建立map中的初始数据;然后再调用processJournal();方法对刚刚建立起的map数据进行分析,分析的工作,一个是计算当前有效缓存文件(即被CLEAN的)的大小,一个是清理无用缓存文件;
2)数据缓存与获取缓存:上面的初始化工作完成后,我们就可以在程序中进行数据的缓存功能和获取缓存的功能了;
缓存数据的操作是借助DiskLruCache.Editor这个类完成的,这个类也是不能new的,需要调用DiskLruCache的edit()方法来获取实例,如下所示:
- new Thread(new Runnable() {
- @Override
- public void run() {
- try {
- String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
- String key = hashKeyForDisk(imageUrl); //MD5对url进行加密,这个主要是为了获得统一的16位字符
- DiskLruCache.Editor editor = mDiskLruCache.edit(key); //拿到Editor,往journal日志中写入DIRTY记录
- if (editor != null) {
- OutputStream outputStream = editor.newOutputStream(0);
- if (downloadUrlToStream(imageUrl, outputStream)) { //downloadUrlToStream方法为下载图片的方法,并且将输出流放到outputStream
- editor.commit(); //完成后记得commit(),成功后,再往journal日志中写入CLEAN记录
- } else {
- editor.abort(); //失败后,要remove缓存文件,往journal文件中写入REMOVE记录
- }
- }
- mDiskLruCache.flush(); //将缓存操作同步到journal日志文件,不一定要在这里就调用
- } catch (IOException e) {
- e.printStackTrace();
- }
- }
- }).start();
- try {
- String imageUrl = "http://img.my.csdn.net/uploads/201309/01/1378037235_7476.jpg";
- String key = hashKeyForDisk(imageUrl); //MD5对url进行加密,这个主要是为了获得统一的16位字符
- //通过get拿到value的Snapshot,里面封装了输入流、key等信息,调用get会向journal文件写入READ为前缀的记录
- DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
- if (snapShot != null) {
- InputStream is = snapShot.getInputStream(0);
- Bitmap bitmap = BitmapFactory.decodeStream(is);
- mImage.setImageBitmap(bitmap);
- }
- } catch (IOException e) {
- e.printStackTrace();
- }