DiskLruCache源码(作用、应用思路、源码注解)

这几天一直优哉游哉的查看图片三级缓存,其中DiskLruCache磁盘缓存是三级缓存中非常重要的知识点(另外有内存软引用,LruCache缓存工具类、线程、线程锁等会另外详细阐述)

DiskLruCache网上有很多阐述,基本都是对的,我再次不过是整合一下,或者可以认为是ctrl+c,ctrl+v。只是因为我将代码都彻底“看”了一遍,还有个非常重要的因素,我对代码进行了代码检测,可以说基本规范还是很明了的。

不得不说,写的真心好,而且很有调理。因为水平有限,到现在只是看懂了思路,代码还有很多地方没有彻底连贯,搞懂。

一、DiskLruCache作用

见名知意,这个应该是软件开发最基本的东西,所以DiskLruCache是引用于磁盘存储的类。

主要功能是为了防止安卓手机加载文件(主要是图片)出现OOM(Out Of Memory,即内存泄漏)。

但是防止OOM,直接引用LruCache就OK了。为什么还需要引入磁盘缓存这个类。磁盘缓存其根本目的还是为了增强用户体验,如果一个app每次进入都需要从网络获取,尤其是文件非常耗流量,而且加载也需要时间,但是如果我们加入磁盘缓存最后一道防线,判断如果磁盘中有资源,那么直接从磁盘加载,那么是不是即解决了耗流量问题又节约时间(当然也是牺牲手机存储为代价的,当然这是小量的存储,何乐而不为)。

磁盘缓存的位置,可以自由地进行设定,但是通常情况下多数应用程序都会将缓存的位置选择为 /sdcard/Android/data/<application package>/cache 这个路径。选择在这个位置有两点好处:第一,这是存储在SD卡上的,因此即使缓存再多的数据也不会对手机的内置存储空间有任何影响,只要SD卡空间足够就行。第二,这个路径被Android系统认定为应用程序的缓存路径,当程序被卸载的时候,这里的数据也会一起被清除掉,这样就不会出现删除程序之后手机上还有很多残留数据的问题。

DiskLruCache使用思路

转自http://blog.it985.com/7784.html

1.打开缓存

public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
        throws IOException

open()方法接收四个参数,第一个参数指定的是数据的缓存地址,第二个参数指定当前应用程序的版本号,第三个参数指定同一个key可以对应多少个缓存文件,基本都是传1,第四个参数指定最多可以缓存多少字节的数据。

其中缓存地址前面已经说过了,通常都会存放在 /sdcard/Android/data/<application package>/cache 这个路径下面,但同时我们又需要考虑如果这个手机没有SD卡,或者SD正好被移除了的情况,因此比较优秀的程序都会专门写一个方法来获取缓存地址,如下所示:

private File getDiskCacheDir(Context context, String uniqueName)

可以看到,当SD卡存在或者SD卡不可被移除的时候,就调用getExternalCacheDir()方法来获取缓存路径,否则就调用getCacheDir()方法来获取缓存路径。前者获取到的就是 /sdcard/Android/data/<application package>/cache 这个路径,而后者获取到的是 /data/data/<application package>/cache 这个路径。

接着又将获取到的路径和一个uniqueName进行拼接,作为最终的缓存路径返回。那么这个uniqueName又是什么呢?其实这就是为了对不同类型的数据进行区分而设定的一个唯一值,比如说在网易新闻缓存路径下看到的bitmap、object等文件夹。

接着是应用程序版本号,我们可以使用如下代码简单地获取到当前应用程序的版本号:

 public static int getAppVersion(Context context)

需要注意的是,每当版本号改变,缓存路径下存储的所有数据都会被清除掉,因为DiskLruCache认为当应用程序有版本更新的时候,所有的数据都应该从网上重新获取。

后面两个参数就没什么需要解释的了,第三个参数传1,第四个参数通常传入10M的大小就够了,这个可以根据自身的情况进行调节。

2.写入缓存

private boolean downloadUrlToStream(String urlString, OutputStream outputStream)

这段代码相当基础,相信大家都看得懂,就是访问urlString中传入的网址,并通过outputStream写入到本地。有了这个方法之后,下面我们就可以使用DiskLruCache来进行写入了,写入的操作是借助DiskLruCache.Editor这个类完成的。类似地,这个类也是不能new的,需要调用DiskLruCache的edit()方法来获取实例,接口如下所示:

public Editor edit(String key) throws IOException

可以看到,edit()方法接收一个参数key,这个key将会成为缓存文件的文件名,并且必须要和图片的URL是一一对应的。那么怎样才能让key和图片的URL能够一一对应呢?直接使用URL来作为key?不太合适,因为图片URL中可能包含一些特殊字符,这些字符有可能在命名文件时是不合法的。其实最简单的做法就是将图片的URL进行MD5编码,编码后的字符串肯定是唯一的,并且只会包含0-F这样的字符,完全符合文件的命名规则。

那么我们就写一个方法用来将字符串进行MD5编码,代码如下所示:

public String hashKeyForDisk(String key)

代码很简单,现在我们只需要调用一下hashKeyForDisk()方法,并把图片的URL传入到这个方法中,就可以得到对应的key了。

有了DiskLruCache.Editor的实例之后,我们可以调用它的newOutputStream()方法来创建一个输出流,然后把它传入到downloadUrlToStream()中就能实现下载并写入缓存的功能了。注意newOutputStream()方法接收一个index参数,由于前面在设置valueCount的时候指定的是1,所以这里index传0就可以了。在写入操作执行完之后,我们还需要调用一下commit()方法进行提交才能使写入生效,调用abort()方法的话则表示放弃此次写入。

由于这里调用了downloadUrlToStream()方法来从网络上下载图片,所以一定要确保这段代码是在子线程当中执行的。注意在代码的最后我还调用了一下flush()方法,这个方法并不是每次写入都必须要调用的,但在这里却不可缺少,我会在后面说明它的作用。

3.读取缓存

缓存已经写入成功之后,接下来我们就该学习一下如何读取了。读取的方法要比写入简单一些,主要是借助DiskLruCache的get()方法实现的,接口如下所示:

public synchronized Snapshot get(String key) throws IOException

4.移除缓存

学习完了写入缓存和读取缓存的方法之后,最难的两个操作你就都已经掌握了,那么接下来要学习的移除缓存对你来说也一定非常轻松了。移除缓存主要是借助DiskLruCache的remove()方法实现的,接口如下所示:

public synchronized boolean remove(String key) throws IOException

4.其他

除了写入缓存、读取缓存、移除缓存之外,DiskLruCache还提供了另外一些比较常用的API,我们简单学习一下。

1. size()

这个方法会返回当前缓存路径下所有缓存数据的总字节数,以byte为单位,如果应用程序中需要在界面上显示当前缓存数据的总大小,就可以通过调用这个方法计算出来。比如网易新闻中就有这样一个功能

2.flush()

这个方法用于将内存中的操作记录同步到日志文件(也就是journal文件)当中。这个方法非常重要,因为DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容。前面在讲解写入缓存操作的时候我有调用过一次这个方法,但其实并不是每次写入缓存都要调用一次flush()方法的,频繁地调用并不会带来任何好处,只会额外增加同步journal文件的时间。比较标准的做法就是在Activity的onPause()方法中去调用一次flush()方法就可以了。

3.close()

这个方法用于将DiskLruCache关闭掉,是和open()方法对应的一个方法。关闭掉了之后就不能再调用DiskLruCache中任何操作缓存数据的方法,通常只应该在Activity的onDestroy()方法中去调用close()方法。

4.delete()

这个方法用于将所有的缓存数据全部删除,比如说网易新闻中的那个手动清理缓存功能,其实只需要调用一下DiskLruCache的delete()方法就可以实现了。

由于现在只缓存了一张图片,所以journal中并没有几行日志,我们一行行进行分析。第一行是个固定的字符串“libcore.io.DiskLruCache”,标志着我们使用的是DiskLruCache技术。第二行是DiskLruCache的版本号,这个值是恒为1的。第三行是应用程序的版本号,我们在open()方法里传入的版本号是什么这里就会显示什么。第四行是valueCount,这个值也是在open()方法中传入的,通常情况下都为1。第五行是一个空行。前五行也被称为journal文件的头,这部分内容还是比较好理解的,但是接下来的部分就要稍微动点脑筋了。

第六行是以一个DIRTY前缀开始的,后面紧跟着缓存图片的key。通常我们看到DIRTY这个字样都不代表着什么好事情,意味着这是一条脏数据。没错,每当我们调用一次DiskLruCache的edit()方法时,都会向journal文件中写入一条DIRTY记录,表示我们正准备写入一条缓存数据,但不知结果如何。然后调用commit()方法表示写入缓存成功,这时会向journal中写入一条CLEAN记录,意味着这条“脏”数据被“洗干净了”,调用abort()方法表示写入缓存失败,这时会向journal中写入一条REMOVE记录。也就是说,每一行DIRTY的key,后面都应该有一行对应的CLEAN或者REMOVE的记录,否则这条数据就是“脏”的,会被自动删除掉。

前面我们所学的size()方法可以获取到当前缓存路径下所有缓存数据的总字节数,其实它的工作原理就是把journal文件中所有CLEAN记录的字节数相加,求出的总合再把它返回而已。

除了DIRTY、CLEAN、REMOVE之外,还有一种前缀是READ的记录,这个就非常简单了,每当我们调用get()方法去读取一条缓存数据时,就会向journal文件中写入一条READ记录。因此,像网易新闻这种图片和数据量都非常大的程序,journal文件中就可能会有大量的READ记录。

那么你可能会担心了,如果我不停频繁操作的话,就会不断地向journal文件中写入数据,那这样journal文件岂不是会越来越大?这倒不必担心,DiskLruCache中使用了一个redundantOpCount变量来记录用户操作的次数,每执行一次写入、读取或移除缓存的操作,这个变量值都会加1,当变量值达到2000的时候就会触发重构journal的事件,这时会自动把journal中一些多余的、不必要的记录全部清除掉,保证journal文件的大小始终保持在一个合理的范围内。

三、DiskLruCache源码注解

代码注释如下:

/*
 * 文 件 名:  DiskLruCache.java
 * 版    权:  Anhui Shixu Intelligent Technology CO.,LTD. Copyright YYYY-YYYY,  All rights reserved
 * 描    述:  <描述>
 * 修 改 人:  **
 * 修改时间:  2016-10-8
 * 跟踪单号:  <跟踪单号>
 * 修改单号:  <修改单号>
 * 修改内容:  <修改内容>
 */
package com.example.demo.utils.imagecache;


import java.io.BufferedInputStream;
import java.io.BufferedWriter;
import java.io.Closeable;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.Writer;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;


/**
 * Closeable 是可以关闭的数据源或目标。调用 close 方法可释放对象保存的资源(如打开文件)
 * 
 * @author gaoqiang
 * @version [版本号, 2016-10-8]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
public class DiskLruCache implements Closeable
{
    /** 图片保存的磁盘位置 */
    private final File directory;
    
    /** 日志文件,这个就是我们可以直接在disk里面看到的journal文件 主要就是对他的操作 */
    private final File journalFile;
    
    /** 构建临时缓存文件,完成后将缓存文件重命名为journal */
    private final File journalFileTmp;
    
    /** app版本号 */
    private final int appVersion;
    
    /** 最大容量 */
    private final long maxSize;
    
    /** 每个entry对应的缓存文件的格式 一般为1 */
    private final int valueCount;
    
    /** 文件名 */
    private static final String JOURNAL_FILE = "journal";
    
    /** 临时文件名 */
    private static final String JOURNAL_FILE_TMP = "journal.tmp";
    
    /** 输入流缓冲区大小设置为8k */
    private static final int IO_BUFFER_SIZE = 8 * 1024;
    
    /** 字符串信息,用于和io流对比 */
    static final String MAGIC = "libcore.io.DiskLruCache";
    
    /** 版本号,用于和io流对比 */
    static final String VERSION_1 = "1";
    
    /** remove标识,表示移除 */
    private static final String REMOVE = "REMOVE";
    
    /** clean标识,表示提取有效信息 */
    private static final String CLEAN = "CLEAN";
    
    /** dirty标识,表示需要重新编辑 */
    private static final String DIRTY = "DIRTY";
    
    /** read标识,表示只读 */
    private static final String READ = "READ";
    
    /** 磁盘被应用多少 */
    private long size = 0;
    
    /** LinkedHashMap是HashMap的一个子类,它保留插入的顺序,如果需要输出的顺序和输入时的相同,那么就选用LinkedHashMap */
    private final LinkedHashMap<String, Entry> lruEntries = new LinkedHashMap<String, Entry>(0, 0.75f, true);
    
    /** 这个是专门用于写入日志文件的writer */
    private Writer journalWriter;
    
    /** 这个值大于一定数目时 就会触发对journal文件的清理了 */
    private int redundantOpCount;
    
    /** 为了区分旧的和当前的快照,每个条目都在每次提交一个编辑时给出一个序列号。快照是陈旧的,如果它的序列号不等于它的条目的序列号 */
    private long nextSequenceNumber = 0;
    
    /** 任意序列号 */
    static final long ANY_SEQUENCE_NUMBER = -1;
    
    /** 新建一个线程池 */
    private final ExecutorService executorService = new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS,
        new LinkedBlockingQueue<Runnable>());
    
    /** 线程Callable 和 Runnable 的使用方法大同小异 */
    private final Callable<Void> cleanupCallable = new Callable<Void>()
    {
        @Override
        public Void call()
            throws Exception
        {
            synchronized (DiskLruCache.this)
            {
                if (journalWriter == null)
                {
                    return null; // closed
                }
                trimToSize();
                // 是否超出了范围,否则就重构日志文件
                if (journalRebuildRequired())
                {
                    rebuildJournal();
                    redundantOpCount = 0;
                }
            }
            return null;
        }
    };
    
    /**
     * 创建一个新的journal,省去了冗余信息。如果它存在,取代当前的日记
     * 
     * @throws IOException 异常
     * @see [类、类#方法、类#成员]
     */
    private synchronized void rebuildJournal()
        throws IOException
    {
        if (journalWriter != null)
        {
            journalWriter.close();
        }
        // 这个地方要注意了 writer 是指向的journalFileTmp 这个日志文件的缓存文件
        Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
        
        // 写入日志文件的文件头
        writer.write(MAGIC);
        writer.write("\n");
        writer.write(VERSION_1);
        writer.write("\n");
        writer.write(Integer.toString(appVersion));
        writer.write("\n");
        writer.write(Integer.toString(valueCount));
        writer.write("\n");
        writer.write("\n");
        
        for (Entry entry : lruEntries.values())
        {
            if (entry.currentEditor != null)
            {
                writer.write(DIRTY + ' ' + entry.key + '\n');
            }
            else
            {
                writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
            }
        }
        
        writer.close();
        journalFileTmp.renameTo(journalFile);
        
        // 把写入日志文件的journalWriter初始化
        journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
    }
    
    /**
     * 整理长度,不被允许超过最大长度maxSize
     * 
     * @throws IOException 异常处理
     * @see [类、类#方法、类#成员]
     */
    private void trimToSize()
        throws IOException
    {
        while (size > maxSize)
        {
            // Map.Entry<String, Entry> toEvict = lruEntries.eldest();
            final Map.Entry<String, Entry> toEvict = lruEntries.entrySet().iterator().next();
            remove(toEvict.getKey());
        }
    }
    
    /**
     * 因为这里涉及到写文件 所以要先校验一下写日志文件的writer 是否被正确的初始化
     * 
     * @see [类、类#方法、类#成员]
     */
    private void checkNotClosed()
    {
        if (journalWriter == null)
        {
            throw new IllegalStateException("cache is closed");
        }
    }
    
    /**
     * 验证key是否正常,网络地址 进行md5加密,而对这个key的格式在这里是有要求的 所以这一步就是验证key是否符合规范
     * 
     * @param key 键值
     * @see [类、类#方法、类#成员]
     */
    private void validateKey(String key)
    {
        if (key.contains(" ") || key.contains("\n") || key.contains("\r"))
        {
            throw new IllegalArgumentException("keys must not contain spaces or newlines: \"" + key + "\"");
        }
    }
    
    /**
     * 删除键值对
     * 
     * @param key 键值
     * @return 是否被删除
     * @throws IOException 异常处理
     * @see [类、类#方法、类#成员]
     */
    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.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;
    }
    
    /**
     * <默认构造函数>
     */
    public 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;
    }
    
    /**
     * 非法参数异常
     * 
     * @param string 传递异常信息
     * @return 处理非法参数异常
     * @throws IllegalArgumentException 异常处理
     * @see [类、类#方法、类#成员]
     */
    private static IllegalArgumentException invalidParameter(String string)
        throws IllegalArgumentException
    {
        throw new IllegalArgumentException(string);
    }
    
    /**
     * 打开缓存,如果没有存在的话,创建一个缓存
     * 
     * @param directory 缓存目录
     * @param appVersion app版本
     * @param valueCount 每次缓存的个数,必须是有效的,即大于1,小于maxsize
     * @param maxSize 最大缓存个数
     * @return 硬盘缓存
     * @throws IOException 异常处理
     * @see [类、类#方法、类#成员]
     */
    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
        throws IOException
    {
        // 非法参数异常
        if (maxSize <= 0)
        {
            invalidParameter("maxSize <= 0");
        }
        if (valueCount <= 0)
        {
            invalidParameter("valueCount <= 0");
        }
        
        // 构造方法赋值
        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        
        // 如果journalFile日志文件存在,就直接去构建entry列表
        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();
            }
            
        }
        // 如果日志文件不存在,需要新建
        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();
        return cache;
        
    }
    
    /**
     * 关闭缓存并删除其存储的值的所有值。这将删除缓存目录中的所有文件,包括未由缓存创建的文件。
     * 
     * @throws IOException 异常
     * @see [类、类#方法、类#成员]
     */
    public void delete()
        throws IOException
    {
        close();
        deleteContents(directory);
    }
    
    /**
     * 递归删除所有相关文件
     * 
     * @param dir 文件
     * @throws IOException 异常处理
     * @see [类、类#方法、类#成员]
     */
    public static void deleteContents(File dir)
        throws IOException
    {
        File[] files = dir.listFiles();
        if (files == null)
        {
            throw new IllegalArgumentException("not a directory: " + dir);
        }
        for (File file : files)
        {
            if (file.isDirectory())
            {
                deleteContents(file);
            }
            if (!file.delete())
            {
                throw new IOException("failed to delete file: " + file);
            }
        }
    }
    
    /**
     * 计算初始大小,并收集作为打开缓存的一部分的垃圾。对不符合条件的数据进行删除
     * 
     * @throws IOException 异常处理
     * @see [类、类#方法、类#成员]
     */
    private void processJournal()
        throws IOException
    {
        deleteIfExists(journalFileTmp);
        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();
            }
        }
    }
    
    /**
     * 本意是如果临时文件存在将其删除,但是现在只是判断如果文件存在并且未执行删除操作将抛出异常
     * 
     * @param file 文件
     * @throws IOException 异常
     * @see [类、类#方法、类#成员]
     */
    private static void deleteIfExists(File file)
        throws IOException
    {
        // try {
        // Libcore.os.remove(file.getPath());
        // } catch (ErrnoException errnoException) {
        // if (errnoException.errno != OsConstants.ENOENT) {
        // throw errnoException.rethrowAsIOException();
        // }
        // }
        if (file.exists() && !file.delete())
        {
            throw new IOException();
        }
    }
    
    /**
     * 读取journalFile
     * 
     * @throws IOException 异常处理
     * @see [类、类#方法、类#成员]
     */
    private void readJournal()
        throws IOException
    {
        // 创建输入流读取journalFile,并且设置size
        InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
        try
        {
            // 这一段就是读文件头的信息
            String magic = readAsciiLine(in);
            String version = readAsciiLine(in);
            String appVersionString = readAsciiLine(in);
            String valueCountString = readAsciiLine(in);
            String blank = readAsciiLine(in);
            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 + "]");
            }
            // 利用读到文件末尾的异常来跳出循环
            while (true)
            {
                try
                {
                    // 构建的lruEntries entry列表
                    readJournalLine(readAsciiLine(in));
                }
                catch (EOFException endOfJournal)
                {
                    break;
                }
            }
        }
        finally
        {
            closeQuietly(in);
        }
    }
    
    /**
     * 关闭输入流
     * 
     * @param closeable 关闭的数据源或目标
     * @see [类、类#方法、类#成员]
     */
    public static void closeQuietly(Closeable closeable)
    {
        if (closeable != null)
        {
            try
            {
                closeable.close();
            }
            catch (RuntimeException rethrown)
            {
                throw rethrown;
            }
            catch (Exception ignored)
            {
                
            }
        }
    }
    
    /**
     * 读取Journal文件中的字符串
     * 
     * @param line Journal内容
     * @throws IOException 异常
     * @see [类、类#方法、类#成员]
     */
    private void readJournalLine(String line)
        throws IOException
    {
        // 以空格问分割线进行分割,并且保存到字符串数组里
        String[] parts = line.split(" ");
        if (parts.length < 2)
        {
            throw new IOException("unexpected journal line: " + line);
        }
        String key = parts[1];
        
        if (parts[0].equals(REMOVE) && parts.length == 2)
        {
            lruEntries.remove(key);
            return;
        }
        
        Entry entry = lruEntries.get(key);
        if (entry == null)
        {
            entry = new Entry(key);
            lruEntries.put(key, entry);
        }
        
        if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount)
        {
            entry.readable = true;
            entry.currentEditor = null;
            entry.setLengths(copyOfRange(parts, 2, parts.length));
        }
        else if (parts[0].equals(DIRTY) && parts.length == 2)
        {
            entry.currentEditor = new Editor(entry);
        }
        else if (parts[0].equals(READ) && parts.length == 2)
        {
            // this work was already done by calling lruEntries.get()
        }
        else
        {
            throw new IOException("unexpected journal line: " + line);
        }
    }
    
    /**
     * 指定范围内赋值并且返回
     * 
     * @param original 数组
     * @param start 开始范围
     * @param end 结束范围
     * @return 需要的值
     * @see [类、类#方法、类#成员]
     */
    @SuppressWarnings("unchecked")
    private static <T> T[] copyOfRange(T[] original, int start, int end)
    {
        // 用于判断异常处理优先级
        final int originalLength = original.length;
        if (start > end)
        {
            throw new IllegalArgumentException();
        }
        if (start < 0 || start > originalLength)
        {
            throw new ArrayIndexOutOfBoundsException();
        }
        final int resultLength = end - start;
        final int copyLength = Math.min(resultLength, originalLength - start);
        final T[] result = (T[])Array.newInstance(original.getClass().getComponentType(), resultLength);
        
        // 第一个是要复制的数组,第二个是从要复制的数组的第几个开始,第三个是复制到那,四个是复制到的数组第几个开始,最后一个是复制长度
        System.arraycopy(original, start, result, 0, copyLength);
        return result;
    }
    
    /**
     * 返回ASCII字符,不包含\r\n等特殊符
     * 
     * @param in 输入流
     * @return 字符串
     * @throws IOException 异常
     * @see [类、类#方法、类#成员]
     */
    public static String readAsciiLine(InputStream in)
        throws IOException
    {
        // String 对象是不可改变的,StringBuilder 对象是动态对象,允许扩充它所封装的字符串中字符的数量,但是您可以为它可容纳的最大字符数指定一个值
        StringBuilder result = new StringBuilder(80);
        while (true)
        {
            int c = in.read();
            if (c == -1)
            {
                throw new EOFException();
            }
            else if (c == '\n')
            {
                break;
            }
            
            result.append((char)c);
        }
        int length = result.length();
        if (length > 0 && result.charAt(length - 1) == '\r')
        {
            result.setLength(length - 1);
        }
        return result.toString();
    }
    
    /**
     * 关闭此流并释放与此流关联的所有系统资源。如果已经关闭该流,则调用此方法无效。
     * 
     * @throws IOException 异常
     */
    @Override
    public synchronized void close()
        throws IOException
    {
        if (journalWriter == null)
        {
            return; // already closed
        }
        
        for (Entry entry : new ArrayList<Entry>(lruEntries.values()))
        {
            if (entry.currentEditor != null)
            {
                entry.currentEditor.abort();
            }
        }
        trimToSize();
        journalWriter.close();
        journalWriter = null;
    }
    
    /**
     * 
     * 编辑类
     * 
     * @author gaoqiang
     * @version [版本号, 2016-10-9]
     * @see [相关类/方法]
     * @since [产品/模块版本]
     */
    public final class Editor
    {
        /** 存放信息 */
        private final Entry entry;
        
        /** 判断是否错误 */
        private boolean hasErrors;
        
        /**
         * <默认构造函数>
         */
        private Editor(Entry entry)
        {
            this.entry = entry;
        }
        
        /**
         * 提交editor,所以他对读者可见。同时释放了编辑锁,所以其他的editor也可以开始编辑了
         * 
         * @throws IOException 异常
         * @see [类、类#方法、类#成员]
         */
        public void commit()
            throws IOException
        {
            if (hasErrors)
            {
                completeEdit(this, false);
                remove(entry.key); // the previous entry is stale
            }
            else
            {
                completeEdit(this, true);
            }
        }
        
        /**
         * 中止此entry,这样释放了编辑锁,所以另外一个entry可以开始编辑(即在同一个key中)
         * 
         * @throws IOException 异常
         * @see [类、类#方法、类#成员]
         */
        public void abort()
            throws IOException
        {
            completeEdit(this, false);
        }
        
        /**
         * 这个index 其实一般传0 就可以了
         * 
         * DiskLruCache 认为 一个key 下面可以对应多个文件,这些文件 用一个数组来存储,
         * 
         * 所以正常情况下 我们都是一个key 对应一个缓存文件 所以传0
         * 
         * @param index 标记
         * @return 输出流
         * @throws IOException 异常处理
         * @see [类、类#方法、类#成员]
         */
        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)));
            }
        }
        
        /**
         * 
         * 文件输出流类
         * 
         * @author gaoqiang
         * @version [版本号, 2016-10-9]
         * @see [相关类/方法]
         * @since [产品/模块版本]
         */
        private class FaultHidingOutputStream extends FilterOutputStream
        {
            
            /**
             * <默认构造函数>
             */
            public FaultHidingOutputStream(OutputStream out)
            {
                super(out);
                
            }
            
            @Override
            public void write(int oneByte)
            {
                try
                {
                    out.write(oneByte);
                }
                catch (IOException e)
                {
                    hasErrors = true;
                }
            }
            
            @Override
            public void write(byte[] buffer, int offset, int length)
            {
                try
                {
                    out.write(buffer, offset, length);
                }
                catch (IOException e)
                {
                    hasErrors = true;
                }
            }
            
            @Override
            public void close()
            {
                try
                {
                    out.close();
                }
                catch (IOException e)
                {
                    hasErrors = true;
                }
            }
            
            @Override
            public void flush()
            {
                try
                {
                    out.flush();
                }
                catch (IOException e)
                {
                    hasErrors = true;
                }
            }
        }
    }
    
    /**
     * 这个就是根据缓存文件的大小 更新disklrucache的总大小 然后再日志文件里对该key加入clean的log.
     * 
     * 最后判断是否超过最大的maxSize 以便对缓存进行清理
     * 
     * @param editor 编辑类
     * @param success 是否成功
     * @throws IOException 异常处理
     * @see [类、类#方法、类#成员]
     */
    private synchronized void completeEdit(Editor editor, boolean success)
        throws IOException
    {
        Entry entry = editor.entry;
        if (entry.currentEditor != editor)
        {
            throw new IllegalStateException();
        }
        // 如果是成功的,并且没有被加载过
        if (success && !entry.readable)
        {
            for (int i = 0; i < valueCount; i++)
            {
                if (!entry.getDirtyFile(i).exists())
                {
                    editor.abort();
                    throw new IllegalStateException("edit didn't create file " + i);
                }
            }
        }
        
        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);
        }
    }
    
    /**
     * 是否被允许重建:只有重建什么时候会使其大小和消除至少2000处
     * 
     * @return 是否重建
     * @see [类、类#方法、类#成员]
     */
    private boolean journalRebuildRequired()
    {
        final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
        return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD && redundantOpCount >= lruEntries.size();
    }
    
    /**
     * 
     * class类,存放需要的信息
     * 
     * @author gaoqiang
     * @version [版本号, 2016-10-9]
     * @see [相关类/方法]
     * @since [产品/模块版本]
     */
    private final class Entry
    {
        /** 键值对 */
        private final String key;
        
        /** 这个entry中 每个文件的长度,这个数组的长度为valueCount 一般都是1 */
        private final long[] lengths;
        
        /** 曾经被发布过 那他的值就是true */
        private boolean readable;
        
        /** 如果未编辑,则无效 */
        private Editor currentEditor;
        
        /** 最新序列号 */
        private long sequenceNumber;
        
        /**
         * <默认构造函数>
         */
        private Entry(String key)
        {
            this.key = key;
            this.lengths = new long[valueCount];
        }
        
        /**
         * get entry长度的内容
         * 
         * @return 一定长度的内容
         * @throws IOException 异常
         * @see [类、类#方法、类#成员]
         */
        public String getLengths()
            throws IOException
        {
            StringBuilder result = new StringBuilder();
            for (long size : lengths)
            {
                result.append(' ').append(size);
            }
            return result.toString();
        }
        
        /**
         * 设置Entry的长度
         * 
         * @param strings 字符串数组
         * @throws IOException 异常处理
         * @see [类、类#方法、类#成员]
         */
        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);
            }
        }
        
        /**
         * 对ioe异常的处理
         * 
         * @param strings 字符串数组
         * @return 异常信息
         * @throws IOException 异常处理
         * @see [类、类#方法、类#成员]
         */
        private IOException invalidLengths(String[] strings)
            throws IOException
        {
            throw new IOException("unexpected journal line: " + Arrays.toString(strings));
        }
        
        /**
         * 临时文件创建成功后,就会重命名正式文件
         * 
         * @param i 键值下标
         * @return 空文件
         * @see [类、类#方法、类#成员]
         */
        public File getCleanFile(int i)
        {
            return new File(directory, key + "." + i);
        }
        
        /**
         * 创建空临时文件
         * 
         * @param i 键值下标
         * @return 空临时文件
         * @see [类、类#方法、类#成员]
         */
        public File getDirtyFile(int i)
        {
            return new File(directory, key + "." + i + ".tmp");
        }
    }
    
    /**
     * 向文件系统缓冲的操作
     * 
     * @throws IOException 异常
     * @see [类、类#方法、类#成员]
     */
    public synchronized void flush()
        throws IOException
    {
        checkNotClosed();
        trimToSize();
        journalWriter.flush();
    }
    
    /**
     * 
     * 这个类持有该entry中每个文件的inputStream 通过这个inputStream 可以读取他的内容。
     * 
     * @author gaoqiang
     * @version [版本号, 2016-10-9]
     * @see [相关类/方法]
     * @since [产品/模块版本]
     */
    public final class Snapshot implements Closeable
    {
        
        /** io流集合 */
        private final InputStream[] ins;
        
        /**
         * <默认构造函数>
         */
        private Snapshot(InputStream[] ins)
        {
            
            this.ins = ins;
        }
        
        /**
         * 返回缓冲流索引值,下标为index
         * 
         * @param index 下标
         * @return 输入流
         * @see [类、类#方法、类#成员]
         */
        public InputStream getInputStream(int index)
        {
            return ins[index];
        }
        
        /**
         * 调用 close 方法可释放对象保存的资源(如打开文件)
         * 
         * @throws IOException 异常
         */
        @Override
        public void close()
            throws IOException
        {
            for (InputStream in : ins)
            {
                closeQuietly(in);
            }
        }
        
    }
    
    /**
     * 获取Snapshot对象
     * 
     * @param key 键值对
     * @return Snapshot对象
     * @throws IOException io流异常
     * @see [类、类#方法、类#成员]
     */
    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;
        }
        
        // 打开所有的流,以保证我们找到想要的snapshot。如果我们打开流缓慢,那么可能来自不同的edits
        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(ins);
    }
    
    /**
     * 返回一个正在编辑的Editor,如果有别的在编辑,则返回null
     * 
     * @param key 键值对
     * @return editor
     * @throws IOException 异常
     * @see [类、类#方法、类#成员]
     */
    public Editor edit(String key)
        throws IOException
    {
        return edit(key, ANY_SEQUENCE_NUMBER);
    }
    
    /**
     * 根据传进去的key 创建一个entry 并且将这个key加入到entry的那个map里 然后创建一个对应的editor,同时在日志文件里加入一条对该key的dirty记录
     * 
     * @param key 键值对
     * @param expectedSequenceNumber 序列号
     * @return Editor 对象
     * @throws IOException 异常
     * @see [类、类#方法、类#成员]
     */
    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; // 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
        }
        
        Editor editor = new Editor(entry);
        entry.currentEditor = editor;
        
        // 在创建文件之前刷新日记以防止文件泄漏
        journalWriter.write(DIRTY + ' ' + key + '\n');
        journalWriter.flush();
        return editor;
    }
}

工具类

/*
 * 文 件 名:  Utils.java
 * 版    权:  Anhui Shixu Intelligent Technology CO.,LTD. Copyright YYYY-YYYY,  All rights reserved
 * 描    述:  <描述>
 * 修 改 人:  gaoqiang
 * 修改时间:  2016-10-8
 * 跟踪单号:  <跟踪单号>
 * 修改单号:  <修改单号>
 * 修改内容:  <修改内容>
 */
package com.example.demo.utils;


import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;


import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;


/**
 * 工具类
 * 
 * @author **
 * @version [版本号, 2016-10-8]
 * @see [相关类/方法]
 * @since [产品/模块版本]
 */
public class Utils
{
    /**
     * 获取当前应用程序的版本号。
     * 
     * @param context 资源
     * @return 版本号
     * @see [类、类#方法、类#成员]
     */
    public static int getAppVersion(Context context)
    {
        try
        {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return info.versionCode;
        }
        catch (NameNotFoundException e)
        {
            e.printStackTrace();
        }
        return 1;
    }
    
    /**
     * 使用MD5算法对传入的key进行加密并返回。 图片的url一般都会有特殊字符,是不符合这里的验证的
     * 
     * @param key 图片网络全路径
     * @return 加密之后的网络全路径
     * @see [类、类#方法、类#成员]
     */
    public static String hashKeyForDisk(String key)
    {
        String cacheKey;
        try
        {
            // 拿到一个MD5转换器
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            byte[] bytes = mDigest.digest();
            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);
            }
            cacheKey = sb.toString();
            sb = null;
        }
        catch (NoSuchAlgorithmException e)
        {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值