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 = 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.往缓存实例存取String

put(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);
        }

这里在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/

    原文作者:下一个五年
    原文地址: https://blog.csdn.net/a910626/article/details/52506736
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞