android缓存系列(ASimpleCache源码分析)

提兵百万西湖上,立马吴山第一峰!这篇文章主要讲述android缓存系列:ASimpleCache源码分析相关的知识,希望能为你提供帮助。
接触Acache是因为阅读oschina的开源android端代码,发现oschina采用了该框架缓存新闻分页数据。后来知道这是个杨福海的开源项目,他还开源过afinal框架,项目的地址如下:
https://github.com/yangfuhai/ASimpleCache
一.官方介绍ASimpleCache 是一个为android制定的 轻量级的 开源缓存框架。轻量到只有一个java文件(由十几个类精简而来)。
1、它可以缓存什么东西?普通的字符串、JsonObject、JsonArray、Bitmap、Drawable、序列化的java对象,和 byte数据。
2、它有什么特色?特色主要是:
1:轻,轻到只有一个JAVA文件。
2:可配置,可以配置缓存路径,缓存大小,缓存数量等。
3:可以设置缓存超时时间,缓存超时自动失效,并被删除。
4:支持多进程。
3、它在android中可以用在哪些场景?1、替换SharePreference当做配置文件
2、可以缓存网络请求数据,比如oschina的android客户端可以缓存http请求的新闻内容,缓存时间假设为1个小时,超时后自动失效,让客户端重新请求新的数据,减少客户端流量,同时减少服务器并发量。
4、如何使用 ASimpleCache?以下有个小的demo,希望您能喜欢:

ACache mCache = ACache.get(this); mCache.put("test_key1", "test value"); mCache.put("test_key2", "test value", 10); //保存10秒,如果超过10秒去获取这个key,将为null mCache.put("test_key3", "test value", 2 * ACache.TIME_DAY); //保存两天,如果超过两天去获取这个key,将为null获取数据 ACache mCache = ACache.get(this); String value = https://www.songbingjia.com/android/mCache.getAsString("test_key1");

二.源码分析ASimpleCache里只有一个JAVA文件——ACache.java
源码分析部分,我们也根据使用的步骤来进行分析:
1.获取缓存实例根据以上的demo,我们最先调用的是get方法来获得一个Acache的缓存实例。
Acache提供了多个get方法,通过调用不同的get方法传入不同的参数可以实现定义缓存保存的位置、缓存的数量、缓存的大小。其实这种方案一种最好的方式是使用建造者模式。不过这里因为配置项不多,使用建造者模式就显得过度设计了。get方法最终调用到的是如下的方法,这个方法其实就是一个典型单例实现。
看源码:
/** * 进行获取缓存管理器,看缓存管理器的Map中是否已经存在指定的管理器,如果不存在进行创建,并且加入到map中 * @param cacheDir * @param max_zise * @param max_count * @return */ public static ACache get(File cacheDir, long max_zise, int max_count) { ACache manager = mInstanceMap.get(cacheDir.getAbsoluteFile() + myPid()); if (manager == null) { manager = new ACache(cacheDir, max_zise, max_count); mInstanceMap.put(cacheDir.getAbsolutePath() + myPid(), manager); } return manager; }private static String myPid() { return "_" + android.os.Process.myPid(); }

Acache为不同进程设置了不同的缓存文件夹。通过一个map来管理不同进程的缓存实例。
接下来我们看下ACache的构造函数,我们发现它主要是new了ACacheManager对象。这里有个问题,作者为何要使用Acache和ACacheManager来实现这个开源项目?其实命名一个类就可以了,为什么呢?我感觉可能是作者是考虑到单一职责,ACacheManager内部简单的实现了LRU,Acache则为ACacheManager提供一层封装,负责对外提供接口。这样可以保证Acache的方法都在以为层次上。感性的考虑,也确实是,低层负责实现LRU来分配内存,高层负责提供对外交流的接口。如果使用一个类确实职责过于多了。
/** * 进行根据缓存路径,缓存数量和缓存大小创建一个缓存管理器 * @param cacheDir * @param max_size * @param max_count */ private ACache(File cacheDir, long max_size, int max_count) { if (!cacheDir.exists() & & !cacheDir.mkdirs()) { throw new RuntimeException("can‘t make dirs in " + cacheDir.getAbsolutePath()); } mCache = new ACacheManager(cacheDir, max_size, max_count); }

2.ACacheManager接下来我们来看下ACacheManager这个类:
public class ACacheManager { private final AtomicLong cacheSize; private final AtomicInteger cacheCount; private final long sizeLimit; private final int countLimit; private final Map< File, Long> lastUsageDates = Collections .synchronizedMap(new HashMap< File, Long> ()); protected File cacheDir; private ACacheManager(File cacheDir, long sizeLimit, int countLimit) { this.cacheDir = cacheDir; this.sizeLimit = sizeLimit; this.countLimit = countLimit; cacheSize = new AtomicLong(); cacheCount = new AtomicInteger(); calculateCacheSizeAndCacheCount(); }/** * 计算 cacheSize和cacheCount */ private void calculateCacheSizeAndCacheCount() { new Thread(new Runnable() { @Override public void run() { int size = 0; int count = 0; File[] cachedFiles = cacheDir.listFiles(); if (cachedFiles != null) { for (File cachedFile : cachedFiles) { size += calculateSize(cachedFile); count += 1; lastUsageDates.put(cachedFile, cachedFile.lastModified()); } cacheSize.set(size); cacheCount.set(count); } } }).start(); }private void put(File file) { int curCacheCount = cacheCount.get(); while (curCacheCount + 1 > countLimit) { long freedSize = removeNext(); cacheSize.addAndGet(-freedSize); curCacheCount = cacheCount.addAndGet(-1); } cacheCount.addAndGet(1); long valueSize = calculateSize(file); long curCacheSize = cacheSize.get(); while (curCacheSize + valueSize > sizeLimit) { long freedSize = removeNext(); curCacheSize = cacheSize.addAndGet(-freedSize); } cacheSize.addAndGet(valueSize); Long currentTime = System.currentTimeMillis(); file.setLastModified(currentTime); lastUsageDates.put(file, currentTime); }private File get(String key) { File file = newFile(key); Long currentTime = System.currentTimeMillis(); file.setLastModified(currentTime); lastUsageDates.put(file, currentTime); return file; }private File newFile(String key) { return new File(cacheDir, key.hashCode() + ""); }private boolean remove(String key) { File image = get(key); return image.delete(); }private void clear() { lastUsageDates.clear(); cacheSize.set(0); File[] files = cacheDir.listFiles(); if (files != null) { for (File f : files) { f.delete(); } } }/** * 移除旧的文件 * * @return */ private long removeNext() { if (lastUsageDates.isEmpty()) { return 0; }Long oldestUsage = null; File mostLongUsedFile = null; Set< Map.Entry< File, Long> > entries = lastUsageDates.entrySet(); synchronized (lastUsageDates) { for (Map.Entry< File, Long> entry : entries) { if (mostLongUsedFile == null) { mostLongUsedFile = entry.getKey(); oldestUsage = entry.getValue(); } else { Long lastValueUsage = entry.getValue(); if (lastValueUsage < oldestUsage) { oldestUsage = lastValueUsage; mostLongUsedFile = entry.getKey(); } } } }long fileSize = calculateSize(mostLongUsedFile); if (mostLongUsedFile.delete()) { lastUsageDates.remove(mostLongUsedFile); } return fileSize; }private long calculateSize(File file) { return file.length(); } }

构造函数得到原子类实例cacheSize和cacheCount,通过calculateCacheSizeAndCacheCount(); 方法计算cacheSize和cacheCount.calculateCacheSizeAndCacheCount方法中开启线程进行大小和数量的计算。
计算完后存入cacheSize和cacheCount,cacheSize和cacheCount在内部类中定义为AtomicLong和AtomicInteger量子类,也就是线程安全的。其基本的特性就是在多线程环境下,当有多个线程同时执行这些类的实例包含的方法时,具有排他性,即当某个线程进入方法,执行其中的指令时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由JVM从等待队列中选择一个另一个线程进入。
接下来我们看下数据的存取工作,这里我们分析一个简单的和一个复杂的,其他的原理都一样。
3.往缓存实例存取Stringput(String key, String value)
public void put(String key, String value) { File file = mCache.newFile(key); BufferedWriter out = null; try { out = new BufferedWriter(new FileWriter(file), 1024); out.write(value); } catch (IOException e) { e.printStackTrace(); } finally { if (out != null) { try { out.flush(); out.close(); } catch (IOException e) { e.printStackTrace(); } } mCache.put(file); } }

我们看下newFile方法:
private File newFile(String key) { return new File(cacheDir, key.hashCode() + ""); }

其实这里就是通过key用父目录和子文件分隔的方式构造File对象。然后我们会把value保存到这个file中。
接下来看下put方法:
private void put(File file) { int curCacheCount = cacheCount.get(); while (curCacheCount + 1 > countLimit) { long freedSize = removeNext(); cacheSize.addAndGet(-freedSize); curCacheCount = cacheCount.addAndGet(-1); } cacheCount.addAndGet(1); long valueSize = calculateSize(file); long curCacheSize = cacheSize.get(); while (curCacheSize + valueSize > sizeLimit) { long freedSize = removeNext(); curCacheSize = cacheSize.addAndGet(-freedSize); } cacheSize.addAndGet(valueSize); Long currentTime = System.currentTimeMillis(); file.setLastModified(currentTime); lastUsageDates.put(file, currentTime); }

【android缓存系列(ASimpleCache源码分析)】这里在put的时候,我们首先会判断一下增加后缓存文件个数是否会超过限制的个数,如果超过就移除一个,后面我们再去分析removeNext方法,这里先假设没超过,接下来会给实际缓存文件个数增加1,然后计算该file的大小,如果当前缓存大小+该file大小> 缓存大小限制时,也会执行移除,这里假设没有超过,接下来会接下来会给实际缓存大小增加file的大小,并且设置该file的最后修改时间为系统的时间,并且把该flie文件和修改时间放到一个map集合中(lastUsageDates),即该缓存框架实现LRU的主要方式是通过lastUsageDates这个map。
下面我们分析一下移除方法removeNext的实现:
private long removeNext() { if (lastUsageDates.isEmpty()) { return 0; }Long oldestUsage = null; File mostLongUsedFile = null; Set< Map.Entry< File, Long> > entries = lastUsageDates.entrySet(); synchronized (lastUsageDates) { for (Map.Entry< File, Long> entry : entries) { if (mostLongUsedFile == null) { mostLongUsedFile = entry.getKey(); oldestUsage = entry.getValue(); } else { Long lastValueUsage = entry.getValue(); if (lastValueUsage < oldestUsage) { oldestUsage = lastValueUsage; mostLongUsedFile = entry.getKey(); } } } }long fileSize = calculateSize(mostLongUsedFile); if (mostLongUsedFile.delete()) { lastUsageDates.remove(mostLongUsedFile); } return fileSize; }

大概算法就是去遍历这个lastUsageDates map,然后移除最早的那个缓存文件,并且删除掉,返回因此留出的空余文件容量大小。
put(key, Utils.newStringWithDateInfo(saveTime, value))
分析完ACacheManager的put()后,我们回到put(key, Utils.newStringWithDateInfo(saveTime, value))
其中第二个参数value传入的是Utils.newStringWithDateInfo(saveTime, value),而newStringWithDateInfo是ACache的内部工具类的一个方法,在value内容前面加上了时间信息:
//返回时间信息+value private static String newStringWithDateInfo(int second, String strInfo) { return createDateInfo(second) + strInfo; }

返回拼接createDateInfo(second)和value的字符串。createDateInfo()如下:
//时间信息 private static String createDateInfo(int second) { String currentTime = System.currentTimeMillis() + ""; while (currentTime.length() < 13) {//小于13,前面补0 currentTime = "0" + currentTime; } return currentTime + "-" + second + mSeparator; //当前时间-保存时间 }

返回字符串格式为 当前时间-保存时间“ ”
getAsString
public String getAsString(String key) { File file = mCache.get(key); if (!file.exists()) return null; boolean removeFile = false; BufferedReader in = null; try { in = new BufferedReader(new FileReader(file)); String readString = ""; String currentLine; while ((currentLine = in.readLine()) != null) { readString += currentLine; } if (!Utils.isDue(readString)) { return Utils.clearDateInfo(readString); } else { removeFile = true; return null; } } catch (IOException e) { e.printStackTrace(); return null; } finally { if (in != null) { try { in.close(); } catch (IOException e) { e.printStackTrace(); } } if (removeFile) remove(key); } }

getAsString(String key)方法里首先通过缓存管理器的mCache.get(key)方法获取文件,然后用Utils.isDue(readString)**判断是否字符串数据到期,未到期返回去除时间信息的字符串内容;到期则移除缓存,返回空。**Utils.isDue(readString)调用了isDue(byte[] data)判断:
/** * 判断缓存的byte数据是否到期 * * @param data * @return true:到期了 false:还没有到期 */ private static boolean isDue(byte[] data) { String[] strs = getDateInfoFromDate(data); if (strs != null & & strs.length == 2) { String saveTimeStr = strs[0]; while (saveTimeStr.startsWith("0")) { saveTimeStr = saveTimeStr.substring(1, saveTimeStr.length()); } long saveTime = Long.valueOf(saveTimeStr); long deleteAfter = Long.valueOf(strs[1]); if (System.currentTimeMillis() > saveTime + deleteAfter * 1000) { return true; } } return false; }

至此整个缓存字符串读取过程在ACache的源码分析完成,其他缓存数据类型读取方法分析过程一样。
JsonObject、JsonArray、Bitmap、Drawable、序列化的存入缓存都是转化为字符串/byte格式,再调用函数put(String key, String value)即可。
参考资料http://blog.csdn.net/zhoubin1992/article/details/46379055
http://blog.csdn.net/developer_jiangqq/article/details/49406361?hmsr=toutiao.io& utm_medium=toutiao.io& utm_source=toutiao.io
http://stormzhang.com/android/2014/10/17/android-simple-cache/








    推荐阅读