LRU 笔记

364 阅读18分钟

内存缓存

由于存储空间有限的,所以达到现在就要删除之前不常用的缓存,来缓存新的可能多次使用的,因此有了LRU 算法,最近最少使用算法,其思想是当缓存满了,优先淘汰那些最近最少使用的缓存对象。Android 比较常用的是LruCache ,接下来,我们来看看LruCache 如何使用。

LruCache 使用

创建

    int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        // LruCache 的缓存大小,一般为当前进程可用容量的1/8
        int cacheSize = maxMemory / 8;
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {

            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
            }
        };

,注意单位一致,就是cacheSize 与 LruCache 的sizeOf 方法返回值单位要保持一致,例如,都 / 1024 ,这样就一致了。

mMemoryCache.put(url,bitmap);

mMemoryCache.get(url);

LruCache 原理

看源码,不难发现LruCache 使用了LinkedHashMap ,

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

LinkedHashMap是数组 + 双向链表的数据结构,双向链表可以有序,可以实现访问顺序与插入顺序。

    public LinkedHashMap(int initialCapacity,
                         float loadFactor,
                         boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
    }

accessOrder 为true则为访问顺序,为false,则为插入顺序。

插入顺序,是普通的先来后到,

访问顺序,是例如,当一个节点被访问了,那么就将该节点移向队尾,而数量满了,移除队头节点,则移除的就是最近未访问或较少访问的。

    public static void main(String[] args) {
        LinkedHashMap<Integer, Integer> map = new LinkedHashMap<>(0, 0.75f, true);
        map.put(0, 0);
        map.put(1, 1);
        map.put(2, 2);
        map.put(3, 3);
        map.put(4, 4);
        map.put(5, 5);
        map.get(1);
        map.get(2);

        for (Map.Entry<Integer, Integer> entry : map.entrySet()) {
            System.out.println(entry.getKey() + ":" + entry.getValue());
        }
    }

输出,

0:0
3:3
4:4
5:5
1:1
2:2

可以看到最近访问的最后输出了(调用get 方法,节点被移动到了队尾),那么这就正好满足的LRU缓存算法的思想,最近最少访问的节点在在队头,可见LruCache 巧妙的使用了LinkedHashMap 。

LruCache put

    @Nullable
    public final V put(@NonNull K key, @NonNull V value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        V previous;
        // size + 1 ,添加缓存到LinkedHashMap
        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;
    }

添加缓存调用LinkedHashMap 的put 方法,调用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!");
                }

                // 没有超过或map 为空,则跳出循环
                if (size <= maxSize || map.isEmpty()) {
                    break;
                }

                // 第一个,队首元素
                Map.Entry<K, V> toEvict = map.entrySet().iterator().next();
                key = toEvict.getKey();
                value = toEvict.getValue();
                // 移除
                map.remove(key);
                // 更新size
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }

trimToSize()方法不断地移除LinkedHashMap 队头的元素,即最近最少访问的,直到缓存大小不超过容量。

LruCache get

    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++;
        }

        ...
    }

可以看到,调用了LinkedHashMap 的get 方法,

看一下LinkedHashMap 的get 方法

    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e); // 调整访问顺序的方法
        return e.value;
    }

可以看到如果accessOrder 为true 使用了访问顺序,则会调用afterNodeAccess 方法, 调整队列,这样当get 一次,就是访问了一次,那么调整使队列为访问顺序。

看一下afterNodeAccess 方法

void afterNodeAccess(Node<K,V> e) { // move node to last
        LinkedHashMapEntry<K,V> last;
        if (accessOrder && (last = tail) != e) {
            LinkedHashMapEntry<K,V> p =
                (LinkedHashMapEntry<K,V>)e, b = p.before, a = p.after;
            p.after = null;
            if (b == null)
                head = a;
            else
                b.after = a; // 1
            if (a != null)
                a.before = b; // 2
            else
                last = b;
            if (last == null)
                head = p;
            else {
                p.before = last;
                last.after = p;
            }
            // 将节点移到了队尾
            tail = p;
            ++modCount;
        }
    }

可以看到将节点移到队尾,刚访问的移到队尾,即accessOrder 设置为true 的LinkedHashMap ,调用get 方法访问元素,那么该元素会被移动到队尾。

这样,当超过容量移除队头节点,就实现了移除最近最少使用的原则。

磁盘缓存

内部存储缓存

对于应用的私有缓存数据,可以保存在内部存储的缓存目录,

public abstract File getCacheDir();

是Context 的一个方法,获取如下目录,

/data/data/<application package>/cache

需要注意,应用的私有缓存文件不应该过大。如果内部存储空间不足,系统可能会删除这些缓存文件。为了保证良好的用户体验,应用应该定期主动清除自己的缓存数据。

内部存储主要用于保存应用的私有文件,其他应用无法访问这些数据。当应用卸载的时候,这些数据也会被删除。

内部缓存不如外部缓存大,能不用就不用

外部存储缓存

在外部存储也有专门缓存文件的空间,可以通过Context的getExternalCacheDir方法访问缓存文件目录,

public abstract File getExternalCacheDir();

注意,当应用卸载时,缓存目录下的文件也会被系统删除。当然,官方建议开发者应该主动移除不再需要的缓存文件,这有助于节省存储空间并保持应用性能。

获取目录是,

/storage/emulated/0/Android/data/<application package>/cache

可用性检查

由于外部存储存在被移除的情况,在使用外部存储前首先应该进行可用性检查,使用Environment的getExternalStorageState方法可以获得外部存储的状态,通过判断返回的状态就实现了对外部存储的可用性检查。

1.判断外部存储是否可写和可读

String state=Environment.getExternalStorageState();
if(Environment.MEDIA_MOUNTED.equals(state)){
    //外部存储可写、可读
}

2.判断外部存储是否至少可读

String state=Environment.getExternalStorageState();
if(Environment.MEDIA_MOUNTED.equals(state)||
        Environment.MEDIA_MOUNTED_READ_ONLY.equals(state)){
    //外部存储至少可读
}

实际上,getExternalStorageState方法有10种返回值,如下:

1.MEDIA_UNKNOWN:未知状态
2.MEDIA_REMOVED:移除状态(外部存储不存在)
3.MEDIA_UNMOUNTED:未装载状态(外部存储存在但是没有装载)
4.MEDIA_CHECKING:磁盘检测状态
5.MEDIA_NOFS:外部存储存在,但是磁盘为空或使用了不支持的文件系统
6.MEDIA_MOUNTED:就绪状态(可读、可写)
7.MEDIA_MOUNTED_READ_ONLY:只读状态
8.MEDIA_SHARED:共享状态(外部存储存在且正通过USB共享数据)
9.MEDIA_BAD_REMOVAL:异常移除状态(外部存储还没有正确卸载就被移除了)
10.MEDIA_UNMOUNTABLE:不可装载状态(外部存储存在但是无法被装载,一般是磁盘的文件系统损坏造成的)

使用DiskLruCache

DiskLruCache不是官方所写,但是得到了官方推荐,使用DiskLruCache 需添加依赖,

implementation 'com.jakewharton:disklrucache:2.0.2'

看一下它的使用,

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) {
    ...
}

directory :缓存路径

appVersion :应用版本号,因为DiskLruCache 在应用版本更新时,所以数据都从网上重新获取,所以,当版本号变动,缓存路径的数据会被清除。

valueCount :指定一个key 可以对应几个缓存文件

maxSize :指定缓存空间的大小

获取缓存路径

考虑内部缓存与外部缓存,还有SD 卡可能被拔出去的情况,我们可以定义如下方法来获取缓存路径,

    private File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

外部存储可以,则优先考虑使用,调用getExternalCacheDir()方法来获取缓存路径;

否则调用getCacheDir()方法来获取缓存路径,内部缓存空间小,所以在外部缓存可以的情况下不使用。

获取版本号

public int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return info.versionCode;
        } catch (NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

valueCount 这里可以传1,

缓存的容量,可以视情况而定,这里使用10M ,

调用open 方法,创建DiskLruCache

    try {
            File cacheDir = getDiskCacheDir(context, "image");
            if (!cacheDir.exists()) {
                cacheDir.mkdir();
            }
            mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 1, 10 * 1024 * 1024);
        } catch (IOException e) {
            e.printStackTrace();
        }

写入缓存

这里用的是OkHttp ,当然你可以用其他的,

    public boolean imageRequestToStream(String url, OutputStream outputStream) throws IOException {
        int buf_size = 8 * 1024;
        BufferedInputStream in = null;
        BufferedOutputStream out = null;
        Request request = new Request.Builder()
                .url(url)
                .build();
        try {
            Response response = client.newCall(request).execute();
            in = new BufferedInputStream(response.body().byteStream(), buf_size);
            out = new BufferedOutputStream(outputStream, buf_size);
            int b;
            while ((b = in.read()) != -1) {
                out.write(b);
            }
            return true;
        } finally {
            if (out != null) {
                out.close();
            }
            if (in != null) {
                in.close();
            }
        }
    }

网络请求,并通过outputStream写入到本地。

那么这个outputStream 是怎么获得的呢?

需要通过DiskLruCache的edit()来获取,

DiskLruCache.Editor editor = mDiskLruCache.edit(key);
OutputStream outputStream = editor.newOutputStream(0);

可以看到edit()接收一个参数key ,这个key 将会成为缓存文件的文件名,直接使用URL来作为key?不太合适,因为图片URL可能包含一些特殊字符,这些字符有可能命名文件是不合法的。

其实简单的做法就是将图片的URL进行MD5编码,编码后的字符串肯定是唯一的,并且只会包含0-F这样的字符,完全符合文件的命名规则。

 public String hashKeyForDisk(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        return sb.toString();
    }

调用hashKeyForDisk(),并把图片的URL传入到这个方法,就可以得到对应的key了。

一次完整写入操作如下,

new Thread(new Runnable() {
        @Override
        public void run() {
            try {
                String imageUrl = "http://pic21.nipic.com/20120525/8956325_100544942000_2.jpg";
                String key = hashKeyForDisk(imageUrl);
                DiskLruCache.Editor editor = mDiskLruCache.edit(key);
                if (editor != null) {
                    OutputStream outputStream = editor.newOutputStream(0);
                    if (imageRequestToStream(imageUrl, outputStream)) {
                        editor.commit();
                    } else {
                        editor.abort();
                    }
                }
                mDiskLruCache.flush();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }).start();

没有问题,别忘了调用editor.commit() ,

出现问题,就editor.abort() 这次写入就彻底中止了,

mDiskLruCache.flush(); 是将内存的操作记录同步到日志文件, 并不是每次写入缓存都调用一次flush() ,频繁地调用并不会带来任何好处,在Activity的onPause() 去调用一次flush()就可以了。

读取缓存

    String key = hashKeyForDisk(url);
        try {
            DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
            if (snapShot != null) {
                InputStream is = snapShot.getInputStream(0);
                Bitmap bitmap = BitmapFactory.decodeStream(is);
                mImageView.setImageBitmap(bitmap);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

根据key 得到DiskLruCache.Snapshot ,然后获取输入流对象,BitmapFactory.decodeStream(is) ,就得到了Bitmap 。

移除缓存

移除很简单,根据key 移除就可以,

String key = hashKeyForDisk(url);
try {
    mDiskLruCache.remove(key);
} catch (IOException e) {
        e.printStackTrace();
}

虽然简单,但是不常用,只有你确定某个key对应的缓存内容已经过期,需要从网上获取最新数据才去remove 缓存。

因为我们用的是DiskLruCache ,在有LRU 的置换策略,会帮我们移除最近最少使用的缓存。

journal 文件

libcore.io.DiskLruCache
1
1
1

DIRTY c3bac86f2e7a291a1a200b853835b664
CLEAN c3bac86f2e7a291a1a200b853835b664 4698
READ c3bac86f2e7a291a1a200b853835b664
DIRTY c59f9eec4b616dc6682c7fa8bd1e061f
CLEAN c59f9eec4b616dc6682c7fa8bd1e061f 4698
READ c59f9eec4b616dc6682c7fa8bd1e061f
DIRTY 536788f4dbdffeecfbb8f350a941eea3
REMOVE 536788f4dbdffeecfbb8f350a941eea3 

前五行也被称为journal文件的头。

第一行固定字符串libcore.io.DiskLruCache ,标志着使用的是DiskLruCache技术;
第二行是DiskLruCache的版本号;
第三行是应用程序的版本号,在open()里传入的appVersion ;
第四行是valueCount,这个值也是在open()传入的,代表一个key 对应几个value ;
第五行是一个空行;

DIRTY 前缀,后面紧跟着缓存的key ,表示我们正准备写入一条缓存数据,但不知结果如何;然后调用commit()写入缓存成功,这时会向journal写入一条CLEAN记录;调用abort()写入缓存失败,这时会向journal中写入一条REMOVE记录。

也就是说,每一行DIRTY 记录,后面都应该有一行对应的CLEAN或者REMOVE记录。

CLEAN key 后面的数字是缓存的大小,以字节为单位。

READ记录,每当我们调用get()方法去读取一条缓存数据时,就会向journal文件写入一条READ记录。

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");
        }

        // If a bkp file exists, use it instead.
        File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
        // 如果备份文件存在
        if (backupFile.exists()) {
            File journalFile = new File(directory, JOURNAL_FILE);
            // 如果journal也文件存在,就删除掉备份文件
            if (journalFile.exists()) {
                backupFile.delete();
            } else {
                // 不存在,就把备份文件重命名为journal文件
                renameTo(backupFile, journalFile, false);
            }
        }

        // Prefer to pick up where we left off.
        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        // journal 存在
        if (cache.journalFile.exists()) {
            try {
                cache.readJournal();
                cache.processJournal();
                cache.journalWriter = new BufferedWriter(
                        new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII));
                return cache;
            } catch (IOException journalIsCorrupt) {
                System.out
                        .println("DiskLruCache "
                                + directory
                                + " is corrupt: "
                                + journalIsCorrupt.getMessage()
                                + ", removing");
                cache.delete();
            }
        }

        // journal 不存在,要重新创建这一切
        
        // Create a new empty cache.
        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();
        return cache;
    }

因为我们在调用open 方法时,可能应用已经创建过磁盘缓存,例如journalFile 已经存在,那么这时候,我们只需读取与处理就行,

还有对backupFile 的处理,都是快速的达到有一个journalFile 的情况;

如果没有journalFile ,例如第一次创建DiskLruCache ,那自然没有什么捷径可走啊,会调用rebuildJournal 方法创建一个新的,接下来看一下该方法。

rebuildJournal

    static final String JOURNAL_FILE = "journal";
    static final String JOURNAL_FILE_TEMP = "journal.tmp";
    static final String JOURNAL_FILE_BACKUP = "journal.bkp";
    static final String MAGIC = "libcore.io.DiskLruCache";
    static final String VERSION_1 = "1";
    
    ...

    private synchronized void rebuildJournal() throws IOException {
        if (journalWriter != null) {
            journalWriter.close();
        }

        // 创建一个journal.tmp文件
        Writer writer = new BufferedWriter(
                new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
        try {
            // 向journal.tmp文件写入第一行
            writer.write(MAGIC);
            writer.write("\n"); // 换行
            // 写入VERSION_1
            writer.write(VERSION_1);
            writer.write("\n");
            // 写入app的版本号
            writer.write(Integer.toString(appVersion));
            writer.write("\n");
            // 写入一个key 对应几个value
            writer.write(Integer.toString(valueCount));
            writer.write("\n");
            // 空行,头部结束标识
            writer.write("\n");

            // 将当前的操作记录写到文件,在编辑的为DIRTY 记录,不在编辑的为CLEAN 记录
            for (Entry entry : lruEntries.values()) {
                if (entry.currentEditor != null) {
                    writer.write(DIRTY + ' ' + entry.key + '\n');
                } else {
                    writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
                }
            }
        } finally {
            writer.close();
        }

        // 如果jorunal文件存在,就重命名为journal.bkp
        if (journalFile.exists()) {
            renameTo(journalFile, journalFileBackup, true);
        }
        // 将journal.tmp文件重命名为journal
        renameTo(journalFileTmp, journalFile, false);
        // 删除journal.bkp文件
        journalFileBackup.delete();

        journalWriter = new BufferedWriter(
                new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
    }

从这里可以看到先写了jorunal文件前五行,头部写完,如果有操作记录,就记录到journal.tmp ,也看到这个journal.tmp 也被重命名为了journal ;

可见该方法为DiskLruCache 呈现了一个journal 文件。

在调用open 方法时,journalFile 不存在,会调用该方法,可是存在的话,会直接读取与处理journal 文件,接下来,看一下readJournal 方法与processJournal 方法。

readJournal

    private void readJournal() throws IOException {
        StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
        try {
            // 读取journal前五行
            String magic = reader.readLine();
            String version = reader.readLine();
            String appVersionString = reader.readLine();
            String valueCountString = reader.readLine();
            String blank = reader.readLine();
            // 验证前5行数据是否合法
            if (!MAGIC.equals(magic)
                    || !VERSION_1.equals(version)
                    || !Integer.toString(appVersion).equals(appVersionString)
                    || !Integer.toString(valueCount).equals(valueCountString)
                    || !"".equals(blank)) {
                throw new IOException("unexpected journal header: [" + magic + ", " + version + ", "
                        + valueCountString + ", " + blank + "]");
            }

            int lineCount = 0;
            while (true) {
                try {
                    // journal头部没问题,接下来就读取剩下的记录,处理
                    readJournalLine(reader.readLine());
                    lineCount++;
                } catch (EOFException endOfJournal) {
                    break;
                }
            }
            redundantOpCount = lineCount - lruEntries.size();
        } finally {
            Util.closeQuietly(reader);
        }
    }

读取journal头部,验证没问题,就读取剩下的记录,具体看readJournalLine 方法;

readJournalLine

private void readJournalLine(String line) throws IOException {
        // 找到第一个' '位置
        int firstSpace = line.indexOf(' ');
        if (firstSpace == -1) {
            throw new IOException("unexpected journal line: " + line);
        }

        // key 的起始位置
        int keyBegin = firstSpace + 1;
        // 找到第二个' '位置
        int secondSpace = line.indexOf(' ', keyBegin);
        final String key;
        // 第二个' '不存在,则不是CLEAN 记录,后面没有缓存数据大小
        if (secondSpace == -1) {
            // 截取key
            key = line.substring(keyBegin);
            //如是REMOVE 记录,就从集合把这个key删掉,并return
            if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
                lruEntries.remove(key);
                return;
            }
        } else {
            // 第二个' '存在,截取key
            key = line.substring(keyBegin, secondSpace);
        }

        // CLEAN 记录是有效缓存,构建Entry put 到lruEntries
        // 这个Entry 对应着一条磁盘缓存数据
        Entry entry = lruEntries.get(key);
        if (entry == null) {
            entry = new Entry(key);
            lruEntries.put(key, entry);
        }

        // 检查记录类型
        if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
            // 截取到文件的大小数组
            String[] parts = line.substring(secondSpace + 1).split(" ");
            // 可读
            entry.readable = true;
            // CLEAN 记录不处于编辑状态
            entry.currentEditor = null;
            // 设置到entry
            entry.setLengths(parts);
        } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
            // DIRTY 记录editor 不为空,出于未编辑完的
            entry.currentEditor = new Editor(entry);
        } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) {
            // This work was already done by calling lruEntries.get().
        } else {
            throw new IOException("unexpected journal line: " + line);
        }
    }

可见该方法是处理一条记录的,如果是REMOVE 记录,则移除,方法返回; 其他类型记录再做处理,注释已描述,

这里说一下READ 记录,注释READ 记录的处理工作已经通过lruEntries.get() 做了,那么这个lruEntries 是什么呢?entry 也放到了lruEntries 里,

很简单就可以发现,是 LinkedHashMap<String, Entry> 类型,可见DiskLruCache 也是通过 LinkedHashMap 来实现最近最少使用原则的

LinkedHashMap 的get 方法会把访问的节点移向队尾。

processJournal

private void processJournal() throws IOException {
        // 如果存在删除journal.tmp临时文件
        deleteIfExists(journalFileTmp);
        // for 循环遍历,计算文件总size
        for (Iterator<Entry> i = lruEntries.values().iterator(); i.hasNext(); ) {
            Entry entry = i.next();
            if (entry.currentEditor == null) {
                for (int t = 0; t < valueCount; t++) {
                    size += entry.lengths[t];
                }
            } else {
                // 不为空的走删除逻辑
                entry.currentEditor = null;
                for (int t = 0; t < valueCount; t++) {
                    deleteIfExists(entry.getCleanFile(t));
                    deleteIfExists(entry.getDirtyFile(t));
                }
                i.remove();
            }
        }
    }

可以看到计算总size ,遍历LinkedHashMap , entry.currentEditor == null 对应CLEAN 记录的,累加size , 否则应该是把完成的,半成品DIRTY 的删除,可见每一行DIRTY记录后面,都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。

写入数据

添加数据一般的操作是通过 通过调用edit()方法,获取到Editor。然后通过editor来保存数据,最后调用editor.commit()方法来完成的。

先来分析一下获取editor的方法吧。

edit

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 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.
        }

        // 为entry 新建editor
        Editor editor = new Editor(entry);
        entry.currentEditor = editor;

        // 添加DIRTY 记录
        // Flush the journal before creating files to prevent file leaks.
        journalWriter.write(DIRTY + ' ' + key + '\n');
        journalWriter.flush();
        return editor;
    }

如果新key ,则集合肯定没有对应的entry ,那么新建entry 并为其新建editor ,还有添加该key 对应的DIRTY 记录。

newOutputStream

public OutputStream newOutputStream(int index) throws IOException {
        synchronized (DiskLruCache.this) {
            if (entry.currentEditor != this) {
                throw new IllegalStateException();
            }
            if (!entry.readable) {
                written[index] = true;
            }
            // 缓存路径/key.index.tmp 文件
            File dirtyFile = entry.getDirtyFile(index);
            FileOutputStream outputStream;
            try {
                outputStream = new FileOutputStream(dirtyFile);
            } catch (FileNotFoundException e) {
                // Attempt to recreate the cache directory.
                directory.mkdirs();
                try {
                    outputStream = new FileOutputStream(dirtyFile);
                } catch (FileNotFoundException e2) {
                    // We are unable to recover. Silently eat the writes.
                    return NULL_OUTPUT_STREAM;
                }
            }
            return new FaultHidingOutputStream(outputStream);
        }
    }

返回dirtyFile 的输出流,把数据通过该流写到磁盘。

    public File getDirtyFile(int i) {
        return new File(directory, key + "." + i + ".tmp");
    }

可见dirtyFile对应的是directory/key.i.tmp 文件,是一个可以理解为临时文件,可以理解DIRTY 记录的是没有完成整个操作的。

    public void commit() throws IOException {
      if (hasErrors) {
        completeEdit(this, false);
        remove(entry.key); // The previous entry is stale.
      } else {
        completeEdit(this, true);
      }
      committed = true;
    }

可见有错误,调用completeEdit 方法,传入false ,没错误调用completeEdit 方法传入true ,

    public void abort() throws IOException {
      completeEdit(this, false);
    }

abort 方法调用completeEdit 直接传入false ,

可见要想完成正确的操作应该是要走这句的completeEdit(this, true);

completeEdit

    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 (!editor.written[i]) {
                    editor.abort();
                    throw new IllegalStateException("Newly created entry didn't create value for index " + i);
                }
                if (!entry.getDirtyFile(i).exists()) {
                    editor.abort();
                    return;
                }
            }
        }

        // 由于valueCount 的缘故(即一个key 可能对应多个文件),这里要遍历,
        for (int i = 0; i < valueCount; i++) {
            File dirty = entry.getDirtyFile(i);
            if (success) {
                // 临时文件存在
                if (dirty.exists()) {
                    // 创建最终的缓存File
                    File clean = entry.getCleanFile(i);
                    // 直接进行了重命名
                    dirty.renameTo(clean);
                    // 更新length
                    long oldLength = entry.lengths[i];
                    long newLength = clean.length();
                    entry.lengths[i] = newLength;
                    // 更新size
                    size = size - oldLength + newLength;
                }
            } else {
                // 传入的false ,直接删除dirty
                deleteIfExists(dirty);
            }
        }

        // 操作次数加一
        redundantOpCount++;
        // 将实例的当前编辑器置空
        entry.currentEditor = null;
        if (entry.readable | success) {
            //可读
            entry.readable = true;
            // 写入CLEAN 记录
            journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
            if (success) {
                entry.sequenceNumber = nextSequenceNumber++;
            }
        } else {
            // 如果不成功,就从集合中删除掉这个缓存
            lruEntries.remove(entry.key);
            // 写入REMOVE 记录
            journalWriter.write(REMOVE + ' ' + entry.key + '\n');
        }
        journalWriter.flush();

        // 添加完缓存,要判断是否达到容量,
        // 如达到容量要在线程池里提交任务,进行清除最近最少使用的缓存
        // 还有journalRebuildRequired 方法的返回值也可以决定是否清理
        if (size > maxSize || journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }
    }

写入缓存的收尾工作,写入过程没错误的话,会对临时文件进行重命名,

    public File getCleanFile(int i) {
      return new File(directory, key + "." + i);
    }

这个命名就是在手机上缓存路径里可以查看到的命名格式,

并会写入CLEAN 记录或REMOVE 记录,并且添加完还会考虑是否达到容量要进行清理,

还有journalRebuildRequired 方法的返回值也可以决定是否清理,那么我们看一下,

    private boolean journalRebuildRequired() {
    final int redundantOpCompactThreshold = 2000;
    return redundantOpCount >= redundantOpCompactThreshold //
        && redundantOpCount >= lruEntries.size();
  }

可见操作次数超过2000 并且,redundantOpCount >= lruEntries.size() 也要进行清理。

清理操作

  private final Callable<Void> cleanupCallable = new Callable<Void>() {
    public Void call() throws Exception {
      synchronized (DiskLruCache.this) {
        if (journalWriter == null) {
          return null; // Closed.
        }
        trimToSize();
        if (journalRebuildRequired()) {
          rebuildJournal();
          redundantOpCount = 0;
        }
      }
      return null;
    }
  };

提交该Callable 到线程池,可见cleanupCallable 的call 方法,会调用trimToSize() 进行裁剪,

还会调用之前提到的journalRebuildRequired方法 ,达到操作次数,且操作次数不小于当前lruEntries.size() ,要进行journal 文件的重建。

来看一下trimToSize 方法,看看DiskLruCache 是如何裁剪的,

  private void trimToSize() throws IOException {
    while (size > maxSize) {
      Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
      remove(toEvict.getKey());
    }
  }

取出lruEntries 的头部,移除,

之前的分析已经知道,新访问过的节点会移到尾部,自然头部元素是该移除的那个啦。

读取数据

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;
    }

    // Open all streams eagerly to guarantee that we see a single published
    // snapshot. If we opened streams lazily then the streams could come
    // from different edits.
    InputStream[] ins = new InputStream[valueCount];
    try {
      // 新建对应缓存文件的输入流
      for (int i = 0; i < valueCount; i++) {
        ins[i] = new FileInputStream(entry.getCleanFile(i));
      }
    } catch (FileNotFoundException e) {
      // A file must have been deleted manually!
      for (int i = 0; i < valueCount; i++) {
        if (ins[i] != null) {
          Util.closeQuietly(ins[i]);
        } else {
          break;
        }
      }
      return null;
    }

    redundantOpCount++;
    journalWriter.append(READ + ' ' + key + '\n');
    if (journalRebuildRequired()) {
      executorService.submit(cleanupCallable);
    }

    return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths);
  }

新建对应key缓存文件的输入流,向journal文件写入READ 记录,返回包含该输入流的Snapshot ,

这样在使用时通过该Snapshot 的getInputStream 方法,可以拿到对应缓存文件的输入流,就可以读取缓存文件啦。

删除数据

  public synchronized boolean remove(String key) throws IOException {
    checkNotClosed();
    validateKey(key);
    Entry entry = lruEntries.get(key);
    if (entry == null || entry.currentEditor != null) {
      return false;
    }

    for (int i = 0; i < valueCount; i++) {
      File file = entry.getCleanFile(i);
      if (file.exists() && !file.delete()) {
        throw new IOException("failed to delete " + file);
      }
      size -= entry.lengths[i];
      entry.lengths[i] = 0;
    }

    redundantOpCount++;
    journalWriter.append(REMOVE + ' ' + key + '\n');
    lruEntries.remove(key);

    if (journalRebuildRequired()) {
      executorService.submit(cleanupCallable);
    }

    return true;
  }

更新size ,操作数+1 ,向journal文件添加REMOVE 记录,lruEntries 移除该项,