Andoird LruCache和DiskLruCache缓存详解

参考:http://www.2cto.com/kf/201606/517802.html

            http://blog.csdn.net/guolin_blog/article/details/28863651

            http://blog.csdn.net/zxw136511485/article/details/52196400

            http://www.cnblogs.com/whoislcj/p/5547758.html


DiskLruCache GitHub地址:https://github.com/JakeWharton/DiskLruCache/



前言:先说一个Bitmap和Drawable占用内存的比较:

Drawable、Bitmap占用内存探讨

之前一直使用过Afinal 和Xutils 熟悉这两框架的都知道,两者出自同一人,Xutils是Afina的升级版,AFinal中的图片内存缓存使用的是Bitmap 而后来为何Xutils将内存缓存的对象改成了Drawable了呢?我们一探究竟

写个测试程序:

复制代码
        List bitmaps = new ArrayList<>();
        start = System.currentTimeMillis();
        for (int i = 0; i < testMaxCount; i++) {
            Bitmap bitmap = BitmapUtils.readBitMap(this, R.mipmap.ic_app_center_banner);
            bitmaps.add(bitmap);
            Log.e(TAG, "BitmapFactory Bitmap--num-->" + i);
        }
        end = System.currentTimeMillis();
        Log.e(TAG, "BitmapFactory Bitmap--time-->" + (end - start));

        List drawables = new ArrayList<>();
        
        start = System.currentTimeMillis();
        for (int i = 0; i < testMaxCount; i++) {
            Drawable drawable = getResources().getDrawable(R.mipmap.ic_app_center_banner);
            drawables.add(drawable);
            Log.e(TAG, "BitmapFactory Drawable--num-->" + i);
        }
        end = System.currentTimeMillis();
        Log.e(TAG, "BitmapFactory Drawable--time-->" + (end - start));
复制代码

测试数据1000 同一张图片

 Bitmap 直接70条数据的时候挂掉

Andoird LruCache和DiskLruCache缓存详解_第1张图片

Drawable 轻松1000条数据通过

Andoird LruCache和DiskLruCache缓存详解_第2张图片

从测试说明Drawable 相对Bitmap有很大的内存占用优势。这也是为啥现在主流的图片缓存框架内存缓存那一层采用Drawable作为缓存对象的原因。


开始说正题:

1、先推荐一个轻量级缓存框架——ACache(ASimpleCache)

ACache介绍:ACache类似于SharedPreferences,但是比SharedPreferences功能更加强大,SharedPreferences只能保存一些基本数据类型、Serializable、Bundle等数据。 而Acache可以缓存如下数据:普通的字符串、JsonObject、JsonArray、Bitmap、Drawable、序列化的java对象,和 byte数据。 主要特色:
  • 1:轻,轻到只有一个JAVA文件。
  • 2:可配置,可以配置缓存路径,缓存大小,缓存数量等。
  • 3:可以设置缓存超时时间,缓存超时自动失效,并被删除。
  • 4:支持多进程。
应用场景:
  • 1、替换SharePreference当做配置文件
  • 2、可以缓存网络请求数据,比如oschina的android客户端可以缓存http请求的新闻内容,缓存时间假设为1个小时,超时后自动失效,让客户端重新请求新的数据,减少客户端流量,同时减少服务器并发量。
  • 3、您来说...
 下载链接: https://github.com/yangfuhai/ASimpleCache  

2、Android缓存机制

Android缓存分为内存缓存和文件缓存(磁盘缓存)。在早期,各大图片缓存框架流行之前,常用的内存缓存方式是软引用(SoftReference)和弱引用(WeakReference),如大部分的使用方式:HashMap> imageCache;这种形式。从Android 2.3(Level 9)开始,垃圾回收器更倾向于回收SoftReference或WeakReference对象,这使得SoftReference和WeakReference变得不是那么实用有效。同时,到了Android 3.0(Level 11)之后,图片数据Bitmap被放置到了内存的堆区域,而堆区域的内存是由GC管理的,开发者也就不需要进行图片资源的释放工作,但这也使得图片数据的释放无法预知,增加了造成OOM的可能。因此,在Android3.1以后,Android推出了LruCache这个内存缓存类,LruCache中的对象是强引用的。 

2.1 内存缓存——LruCache源码分析

2.1.1 LRU

LRU,全称Least Rencetly Used,即最近最少使用,是一种非常常用的置换算法,也即淘汰最长时间未使用的对象。LRU在操作 系统中的页面置换算法中广泛使用,我们的内存或缓存空间是有限的,当新加入一个对象时,造成我们的缓存空间不足了,此时就需要根据某种算法对缓存中原有数据进行淘汰货删除,而LRU选择的是将最长时间未使用的对象进行淘汰。 

2.1.2 LruCache实现原理

根据LRU算法的思想,要实现LRU最核心的是要有一种数据结构能够基于访问顺序来保存缓存中的对象,这样我们就能够很方便的知道哪个对象是最近访问的,哪个对象是最长时间未访问的。LruCache选择的是LinkedHashMap这个数据结构,LinkedHashMap是一个双向循环链表,在构造LinkedHashMap时,通过一个boolean值来指定LinkedHashMap中保存数据的方式,LinkedHashMap的一个构造方法如下:
?
1
2
3
4
5
6
7
8
9
10
11
/*
      * 初始化LinkedHashMap
      * 第一个参数:initialCapacity,初始大小
      * 第二个参数:loadFactor,负载因子=0.75f
      * 第三个参数:accessOrder=true,基于访问顺序;accessOrder=false,基于插入顺序
      */
     public LinkedHashMap( int initialCapacity, float loadFactor, boolean accessOrder) {
         super (initialCapacity, loadFactor);
         init();
         this .accessOrder = accessOrder;
     }
显然,在LruCache中选择的是accessOrder = true;此时,当accessOrder 设置为 true时,每当我们更新(即调用put方法)或访问(即调用get方法)map中的结点时,LinkedHashMap内部都会将这个结点移动到链表的尾部,因此,在链表的尾部是最近刚刚使用的结点,在链表的头部是是最近最少使用的结点,当我们的缓存空间不足时,就应该持续把链表头部结点移除掉,直到有剩余空间放置新结点。可以看到,LinkedHashMap完成了LruCache中的核心功能,那LruCache中剩下要做的就是定义缓存空间总容量,当前保存数据已使用的容量,对外提供put、get方法。 

2.1.3 LruCache源码分析

在了解了LruCache的核心原理之后,就可以开始分析LruCache的源码了。 (1)关键字段根据上面的分析,首先要有总容量、已使用容量、linkedHashMap这几个关键字段,LruCache中提供了下面三个关键字段:
?
1
2
3
4
5
6
//核心数据结构
     private final LinkedHashMap "" > map;
     // 当前缓存数据所占的大小
     private int size;
     //缓存空间总容量
     private int maxSize;
要注意的是size字段,因为map中可以存放各种类型的数据,这些数据的大小测量方式也是不一样的,比如Bitmap类型的数据和String类型的数据计算他们的大小方式肯定不同,因此,LruCache中在计算放入数据大小的方法sizeOf中,只是简单的返回了1,需要我们重写这个方法,自己去定义数据的测量方式。因此,我们在使用LruCache的时候,经常会看到这种方式:
?
1
2
3
4
5
6
7
private static final int CACHE_SIZE = 4 * 1024 * 1024 ; //4Mib
     LruCache bitmapCache = new LruCache(CACHE_SIZE){
         @Override
         protected int sizeOf(String key, Bitmap value) {
             return value.getByteCount(); //自定义Bitmap数据大小的计算方式
         }
     };
(2)构造方法
?
1
2
3
4
5
6
7
public LruCache( int maxSize) {
     if (maxSize <= 0 ) {
         throw new IllegalArgumentException( "maxSize <= 0" );
     }
     this .maxSize = maxSize;
     this .map = new LinkedHashMap "" >( 0 , 0 .75f, true );
}
LruCache只有一个唯一的构造方法,在构造方法中,给定了缓存空间的总大小,初始化了LinkedHashMap核心数据结构,在LinkedHashMap中的第三个参数指定为true,也就设置了accessOrder=true,表示这个LinkedHashMap将是基于数据的访问顺序进行排序。  (3)sizeOf()和safeSizeOf()方法根据上面的解释,由于各种数据类型大小测量的标准不统一,具体测量的方法应该由使用者来实现,如上面给出的一个在实现LruCache时重写sizeOf的一种常用实现方式。通过多态的性质,再具体调用sizeOf时会调用我们重写的方法进行测量,LruCache对sizeOf()的调用进行一层封装,如下:
?
1
2
3
4
5
6
7
private int safeSizeOf(K key, V value) {
     int result = sizeOf(key, value);
     if (result < 0 ) {
         throw new IllegalStateException( "Negative size: " + key + "=" + value);
     }
     return result;
}
里面其实就是调用sizeOf()方法,返回sizeOf计算的大小。上面就是LruCache的基本内容,下面就需要提供LruCache的核心功能了。  (4)put方法缓存数据首先看一下它的源码实现:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
/**
   * 给对应key缓存value,并且将该value移动到链表的尾部。
   */
public final V put(K key, V value) {
     if (key == null || value == null ) {
         throw new NullPointerException( "key == null || value == null" );
     }
 
       V previous;
       synchronized ( this ) {
         // 记录 put 的次数
         putCount++;
         // 通过键值对,计算出要保存对象value的大小,并更新当前缓存大小
         size += safeSizeOf(key, value);
         /*
          * 如果 之前存在key,用新的value覆盖原来的数据, 并返回 之前key 的value
          * 记录在 previous
          */
         previous = map.put(key, value);
         // 如果之前存在key,并且之前的value不为null
         if (previous != null ) {
             // 计算出 之前value的大小,因为前面size已经加上了新的value数据的大小,此时,需要再次更新size,减去原来value的大小
             size -= safeSizeOf(key, previous);
         }
       }
 
     // 如果之前存在key,并且之前的value不为null
     if (previous != null ) {
         /*
          * previous值被剔除了,此次添加的 value 已经作为key的 新值
          * 告诉 自定义 的 entryRemoved 方法
          */
         entryRemoved( false , key, previous, value);
     }
     //裁剪缓存容量(在当前缓存数据大小超过了总容量maxSize时,才会真正去执行LRU)
     trimToSize(maxSize);
       return previous;
}
可以看到,put()方法主要有以下几步:1)key和value判空,说明LruCache中不允许key和value为null;2)通过safeSizeOf()获取要加入对象数据的大小,并更新当前缓存数据的大小;3)将新的对象数据放入到缓存中,即调用LinkedHashMap的put方法,如果原来存在该key时,直接替换掉原来的value值,并返回之前的value值,得到之前value的大小,更新当前缓存数据的size大小;如果原来不存在该key,则直接加入缓存即可;4)清理缓存空间,如下;  (5)trimToSize()清理缓存空间当我们加入一个数据时(put),为了保证当前数据的缓存所占大小没有超过我们指定的总大小,通过调用trimToSize()来对缓存空间进行管理控制。如下:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public void trimToSize( int maxSize) {
     /*
      * 循环进行LRU,直到当前所占容量大小没有超过指定的总容量大小
      */
     while ( true ) {
         K key;
         V value;
         synchronized ( this ) {
             // 一些异常情况的处理
             if (size < 0 || (map.isEmpty() && size != 0 )) {
                 throw new IllegalStateException(
                         getClass().getName() + ".sizeOf() is reporting inconsistent results!" );
             }
             // 首先判断当前缓存数据大小是否超过了指定的缓存空间总大小。如果没有超过,即缓存中还可以存入数据,直接跳出循环,清理完毕
             if (size <= maxSize || map.isEmpty()) {
                 break ;
             }
            /**
             * 执行到这,表示当前缓存数据已超过了总容量,需要执行LRU,即将最近最少使用的数据清除掉,直到数据所占缓存空间没有超标;
             * 根据前面的原理分析,知道,在链表中,链表的头结点是最近最少使用的数据,因此,最先清除掉链表前面的结点
             */
            Map.Entry "" > toEvict = map.entrySet().iterator().next();
             key = toEvict.getKey();
             value = toEvict.getValue();
             map.remove(key);
             // 移除掉后,更新当前数据缓存的大小
             size -= safeSizeOf(key, value);
             // 更新移除的结点数量
             evictionCount++;
         }
         /*
          * 通知某个结点被移除,类似于回调
          */
         entryRemoved( true , key, value, null );
     }
}
trimToSize()方法的作用就是为了保证当前数据的缓存大小不能超过我们指定的缓存总大小,如果超过了,就会开始移除最近最少使用的数据,直到size符合要求。trimToSize()方法在put()的时候一定会调用,在get()的时候有可能会调用。  (6)get方法获取缓存数据get方法源码如下:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/**
* 根据key查询缓存,如果该key对应的value存在于缓存,直接返回value
* 访问到这个结点时,LinkHashMap会将它移动到双向循环链表的的尾部。
* 如果如果没有缓存的值,则返回null。(如果开发者重写了create()的话,返回创建的value
*/
public final V get(K key) {
     if (key == null ) {
         throw new NullPointerException( "key == null" );
     }
 
     V mapValue;
     synchronized ( this ) {
         // LinkHashMap 如果设置按照访问顺序的话,这里每次get都会重整数据顺序
         mapValue = map.get(key);
         // 计算 命中次数
         if (mapValue != null ) {
             hitCount++;
             return mapValue;
         }
         // 计算 丢失次数
         missCount++;
     }
 
     /*
      * 官方解释:
      * 尝试创建一个值,这可能需要很长时间,并且Map可能在create()返回的值时有所不同。如果在create()执行的时
      * 候,用这个key执行了put方法,那么此时就发生了冲突,我们在Map中删除这个创建的值,释放被创建的值,保留put进去的值。
      */
     V createdValue = create(key);
     if (createdValue == null ) {
         return null ;
     }
 
    /***************************
     * 不覆写create方法走不到下面 *
     ***************************/
     /*
      * 正常情况走不到这里
      * 走到这里的话 说明 实现了自定义的 create(K key) 逻辑
      * 因为默认的 create(K key) 逻辑为null
      */
     synchronized ( this ) {
         // 记录 create 的次数
         createCount++;
         // 将自定义create创建的值,放入LinkedHashMap中,如果key已经存在,会返回 之前相同key 的值
         mapValue = map.put(key, createdValue);
 
         // 如果之前存在相同key的value,即有冲突。
         if (mapValue != null ) {
             /*
              * 有冲突
              * 所以 撤销 刚才的 操作
              * 将 之前相同key 的值 重新放回去
              */
             map.put(key, mapValue);
         } else {
             // 拿到键值对,计算出在容量中的相对长度,然后加上
             size += safeSizeOf(key, createdValue);
         }
     }
 
     // 如果上面 判断出了 将要放入的值发生冲突
     if (mapValue != null ) {
         /*
          * 刚才create的值被删除了,原来的 之前相同key 的值被重新添加回去了
          * 告诉 自定义 的 entryRemoved 方法
          */
         entryRemoved( false , key, createdValue, mapValue);
         return mapValue;
     } else {
         // 上面 进行了 size += 操作 所以这里要重整长度
         trimToSize(maxSize);
         return createdValue;
     }
}
get()方法的思路就是: 1)先尝试从map缓存中获取value,即mapVaule = map.get(key);如果mapVaule != null,说明缓存中存在该对象,直接返回即可;2)如果mapVaule == null,说明缓存中不存在该对象,大多数情况下会直接返回null;但是如果我们重写了create()方法,在缓存没有该数据的时候自己去创建一个,则会继续往下走,中间可能会出现冲突,看注释;3)注意:在我们通过LinkedHashMap进行get(key)或put(key,value)时都会对链表进行调整,即将刚刚访问get或加入put的结点放入到链表尾部。  (7)entryRemoved()entryRemoved的源码如下:
?
1
2
3
4
5
6
7
8
9
10
/**
* 1.当被回收或者删掉时调用。该方法当value被回收释放存储空间时被remove调用
* 或者替换条目值时put调用,默认实现什么都没做。
* 2.该方法没用同步调用,如果其他线程访问缓存时,该方法也会执行。
* 3.evicted=true:如果该条目被删除空间 (表示 进行了trimToSize or remove)  evicted=falseput冲突后 或 get里成功create
* 导致
* 4.newValue!=null,那么则被put()get()调用。
*/
protected void entryRemoved( boolean evicted, K key, V oldValue, V newValue) {
}
可以发现entryRemoved方法是一个空方法,说明这个也是让开发者自己根据需求去重写的。entryRemoved()主要作用就是在结点数据value需要被删除或回收的时候,给开发者的回调。开发者就可以在这个方法里面实现一些自己的逻辑:(1)可以进行资源的回收;(2)可以实现二级内存缓存,可以进一步提高性能,思路如下:重写LruCache的entryRemoved()函数,把删除掉的item,再次存入另外一个LinkedHashMap>中,这个数据结构当做二级缓存,每次获得图片的时候,先判断LruCache中是否缓存,没有的话,再判断这个二级缓存中是否有,如果都没有再从sdcard上获取。sdcard上也没有的话,就从网络服务器上拉取。entryRemoved()在LruCache中有四个地方进行了调用:put()、get()、trimToSize()、remove()中进行了调用。  (8)LruCache的线程安全性 LruCache是线程安全的,因为在put、get、trimToSize、remove的方法中都加入synchronized进行同步控制。 

2.1.4 LruCache的使用

上面就是整个LruCache中比较核心的的原理和方法,对于LruCache的使用者来说,我们其实主要注意下面几个点:(1)在构造LruCache时提供一个总的缓存大小;(2)重写sizeOf方法,对存入map的数据大小进行自定义测量;(3)根据需要,决定是否要重写entryRemoved()方法;(4)使用LruCache提供的put和get方法进行数据的缓存  小结:
  • LruCache 自身并没有释放内存,只是 LinkedHashMap中将数据移除了,如果数据还在别的地方被引用了,还是有泄漏问题,还需要手动释放内存;

  • 覆写entryRemoved方法能知道 LruCache 数据移除是是否发生了冲突(冲突是指在map.put()的时候,对应的key中是否存在原来的值),也可以去手动释放资源;

 

2.2磁盘缓存(文件缓存)——DiskLruCache分析

LruCache是一种内存缓存策略,但是当存在大量图片的时候,我们指定的缓存内存空间可能很快就会用完,这个时候,LruCache就会频繁的进行trimToSize()操作,不断的将最近最少使用的数据移除,当再次需要该数据时,又得从网络上重新加载。为此,Google提供了一种磁盘缓存的解决方案——DiskLruCache(DiskLruCache并没有集成到Android源码中,在Android Doc的例子中有讲解)。

2.2.1 DiskLruCache实现原理

我们可以先来直观看一下,使用了DiskLruCache缓存策略的APP,缓存目录中是什么样子,如下图: \可以看到,缓存目录中有一堆文件名很长的文件,这些文件就是我们缓存的一张张图片数据,在最后有一个文件名journal的文件,这个journal文件是DiskLruCache的一个日志文件,即保存着每张缓存图片的操作记录,journal文件正是实现DiskLruCache的核心。看到出现了journal文件,基本可以说明这个APP使用了DiskLruCache缓存策略。根据对LruCache的分析,要实现LRU,最重要的是要有一种数据结构能够基于访问顺序来保存缓存中的对象,LinkedHashMap是一种非常合适的数据结构,为此,DiskLruCache也选择了LinkedHashMap作为维护访问顺序的数据结构,但是,对于DiskLruCache来说,单单LinkedHashMap是不够的,因为我们不能像LruCache一样,直接将数据放置到LinkedHashMap的value中,也就是处于内存当中,在DiskLruCache中,数据是缓存到了本地文件,这里的LinkedHashMap中的value只是保存的是value的一些简要信息Entry,如唯一的文件名称、大小、是否可读等信息,如:
?
1
 
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
private final class Entry {
private final String key;
/** Lengths of this entry's files. */
private final long [] lengths;
/** True if this entry has ever been published */
private boolean readable;
/** The ongoing edit or null if this entry is not being edited. */
private Editor currentEditor;
/** The sequence number of the most recently committed edit to this entry. */
private long sequenceNumber;
private Entry(String key) {
this .key = key;
this .lengths = new long [valueCount];
}
public String getLengths() throws IOException {
StringBuilder result = new StringBuilder();
for ( long size : lengths) {
result.append( ' ' ).append(size);
}
return result.toString();
}
 
/**
* Set lengths using decimal numbers like "10123".
*/
private void setLengths(String[] strings) throws IOException {
if (strings.length != valueCount) {
throw invalidLengths(strings);
}
 
try {
for ( int i = 0 ; i < strings.length; i++) {
lengths[i] = Long.parseLong(strings[i]);
}
} catch (NumberFormatException e) {
throw invalidLengths(strings);
}
}
 
private IOException invalidLengths(String[] strings) throws IOException {
throw new IOException( "unexpected journal line: " + Arrays.toString(strings));
}
 
public File getCleanFile( int i) {
return new File(directory, key + "." + i);
}
 
public File getDirtyFile( int i) {
return new File(directory, key + "." + i + ".tmp" );
}
}
DiskLruCache中对于LinkedHashMap定义如下:
?
1
2
private final LinkedHashMap "" > lruEntries
     = new LinkedHashMap "" >( 0 , 0 .75f, true );
在LruCache中,由于数据是直接缓存中内存中,map中数据的建立是在使用LruCache缓存的过程中逐步建立的,而对于DiskLruCache,由于数据是缓存在本地文件,相当于是持久保存下来的一个文件,即使程序退出文件还在,因此,map中数据的建立,除了在使用DiskLruCache过程中建立外,map还应该包括之前已经存在的缓存文件,因此,在获取DiskLruCache的实例时,DiskLruCache会去读取journal这个日志文件,根据这个日志文件中的信息,建立map的初始数据,同时,会根据journal这个日志文件,维护本地的缓存文件。构造DiskLruCache的方法如下:
?
1
 
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
throws IOException {
if (maxSize <= 0 ) {
throw new IllegalArgumentException( "maxSize <= 0" );
}
if (valueCount <= 0 ) {
throw new IllegalArgumentException( "valueCount <= 0" );
}
 
// prefer to pick up where we left off
DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
if (cache.journalFile.exists()) {
try {
cache.readJournal();
cache.processJournal();
cache.journalWriter = new BufferedWriter( new FileWriter(cache.journalFile, true ),IO_BUFFER_SIZE);
return cache;
} catch (IOException journalIsCorrupt) {
// System.logW("DiskLruCache " + directory + " is corrupt: "
// + journalIsCorrupt.getMessage() + ", removing");
cache.delete();
}
}
 
// create a new empty cache
directory.mkdirs();
cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
cache.rebuildJournal();
return cache;
}
其中,cache.readJournal();cache.processJournal();正是去读取journal日志文件,建立起map中的初始数据,同时维护缓存文件。 那journal日志文件到底保存了什么信息呢,一个标准的journal日志文件信息如下:libcore.io.DiskLruCache//第一行,固定内容,声明1 //第二行,cache的版本号,恒为11 //第三行,APP的版本号2 //第四行,一个key,可以存放多少条数据valueCount//第五行,空行分割行DIRTY335c4c6028171cfddfbaae1a9c313c52CLEAN335c4c6028171cfddfbaae1a9c313c523934REMOVE335c4c6028171cfddfbaae1a9c313c52DIRTY1ab96a171faeeee38496d8b330771a7aCLEAN1ab96a171faeeee38496d8b330771a7a1600234READ335c4c6028171cfddfbaae1a9c313c52READ3400330d1dfc7f3f7f4b8d4d803dfcf6 前五行称为journal日志文件的头,下面部分的每一行会以四种前缀之一开始:DIRTY、CLEAN、REMOVE、READ。

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

在CLEAN前缀和key后面还有一个数值,代表的是该条缓存数据的大小。

 

因此,我们可以总结DiskLruCache中的工作流程:

1)初始化:通过open()方法,获取DiskLruCache的实例,在open方法中通过readJournal(); 方法读取journal日志文件,根据journal日志文件信息建立map中的初始数据;然后再调用processJournal();方法对刚刚建立起的map数据进行分析,分析的工作,一个是计算当前有效缓存文件(即被CLEAN的)的大小,一个是清理无用缓存文件;

2)数据缓存与获取缓存:上面的初始化工作完成后,我们就可以在程序中进行数据的缓存功能和获取缓存的功能了;

缓存数据的操作是借助DiskLruCache.Editor这个类完成的,这个类也是不能new的,需要调用DiskLruCache的edit()方法来获取实例,如下所示:

publicEditoredit(Stringkey)throwsIOException

 

在写入完成后,需要进行commit()。如下一个简单示例:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
new Thread( new Runnable() { 
     @Override 
     public void run() { 
         try
             String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg"
             String key = hashKeyForDisk(imageUrl);  //MD5对url进行加密,这个主要是为了获得统一的16位字符
             DiskLruCache.Editor editor = mDiskLruCache.edit(key);  //拿到Editor,往journal日志中写入DIRTY记录
             if (editor != null ) { 
                 OutputStream outputStream = editor.newOutputStream( 0 ); 
                 if (downloadUrlToStream(imageUrl, outputStream)) {  //downloadUrlToStream方法为下载图片的方法,并且将输出流放到outputStream
                     editor.commit();  //完成后记得commit(),成功后,再往journal日志中写入CLEAN记录
                 } else
                     editor.abort();  //失败后,要remove缓存文件,往journal文件中写入REMOVE记录
                
            
             mDiskLruCache.flush();  //将缓存操作同步到journal日志文件,不一定要在这里就调用
         } catch (IOException e) { 
             e.printStackTrace(); 
        
    
}).start();
注意每次调用edit()时,会向journal日志文件写入DIRTY为前缀的一条记录;文件保存成功后,调用commit()时,也会向journal日志中写入一条CLEAN为前缀的一条记录,如果失败,需要调用abort(),abort()里面会向journal文件写入一条REMOVE为前缀的记录。 获取缓存数据是通过get()方法实现的,如下一个简单示例:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
try
     String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg"
     String key = hashKeyForDisk(imageUrl);  //MD5对url进行加密,这个主要是为了获得统一的16位字符
      //通过get拿到value的Snapshot,里面封装了输入流、key等信息,调用get会向journal文件写入READ为前缀的记录
     DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
     if (snapShot != null ) { 
         InputStream is = snapShot.getInputStream( 0 ); 
         Bitmap bitmap = BitmapFactory.decodeStream(is); 
         mImage.setImageBitmap(bitmap); 
    
} catch (IOException e) { 
     e.printStackTrace(); 
}
  3)合适的地方进行flush()在上面进行数据缓存或获取缓存的时候,调用不同的方法会往journal中写入不同前缀的一行记录,记录写入是通过IO下的Writer写入的,要真正生效,还需要调用writer的flush()方法,而DiskLruCache中的flush()方法中封装了writer.flush()的操作,因此,我们只需要在合适地方调用DiskLruCache中的flush()方法即可。其作用也就是将操作记录同步到journal文件中,这是一个消耗效率的IO操作,我们不用每次一往journal中写数据后就调用flush,这样对效率影响较大,可以在Activity的onPause()中调用一下即可。  小结&注意:(1)我们可以在在UI线程中检测内存缓存,即主线程中可以直接使用LruCache;(2)使用DiskLruCache时,由于缓存或获取都需要对本地文件进行操作,因此需要另开一个线程,在子线程中检测磁盘缓存、保存缓存数据,磁盘操作从来不应该在UI线程中实现;(3)LruCache内存缓存的核心是LinkedHashMap,而DiskLruCache的核心是LinkedHashMap和journal日志文件,相当于把journal看作是一块“内存”,LinkedHashMap的value只保存文件的简要信息,对缓存文件的所有操作都会记录在journal日志文件中。  DiskLruCache可能的优化方案:DiskLruCache是基于日志文件journal的,这就决定了每次对缓存文件的操作都需要进行日志文件的记录,我们可以不用journal文件,在第一次构造DiskLruCache的时候,直接从程序访问缓存目录下的缓存文件,并将每个缓存文件的访问时间作为初始值记录在map的value中,每次访问或保存缓存都更新相应key对应的缓存文件的访问时间,这样就避免了频繁的IO操作,这种情况下就需要使用单例模式对DiskLruCache进行构造了,上面的Acache轻量级的数据缓存类就是这种实现方式。 

2.3 二级缓存

LruCache内存缓存在解决数据量不是很大的情况下效果不错,当数据很大时,比图需要加载大量图片,LruCache指定的缓存容量可能很快被耗尽,此时LruCache频繁的替换移除淘汰文件,又频繁要进行网络请求,很有可能出现OOM,为此,在大量数据的情况下,我们可以将磁盘缓存DiskLruCache作为一个二级缓存的模式,优化缓存方案。 流程就是,(1)当我们需要缓存数据的时候,既在内存中缓存,也将文件缓存到磁盘;(2)当获取缓存文件时,先尝试从内存缓存中获取,如果存在,则直接返回该文件;如果不存在,则从磁盘缓存中获取,如果磁盘缓存中还没有,那就只能从网络获取,获取到数据后,同时在内存和磁盘中进行缓存。



转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/28863651


概述


记得在很早之前,我有写过一篇文章Android高效加载大图、多图解决方案,有效避免程序OOM,这篇文章是翻译自Android Doc的,其中防止多图OOM的核心解决思路就是使用LruCache技术。但LruCache只是管理了内存中图片的存储与释放,如果图片从内存中被移除的话,那么又需要从网络上重新加载一次图片,这显然非常耗时。对此,Google又提供了一套硬盘缓存的解决方案:DiskLruCache(非Google官方编写,但获得官方认证)。只可惜,Android Doc中并没有对DiskLruCache的用法给出详细的说明,而网上关于DiskLruCache的资料也少之又少,因此今天我准备专门写一篇博客来详细讲解DiskLruCache的用法,以及分析它的工作原理,这应该也是目前网上关于DiskLruCache最详细的资料了。


那么我们先来看一下有哪些应用程序已经使用了DiskLruCache技术。在我所接触的应用范围里,Dropbox、Twitter、网易新闻等都是使用DiskLruCache来进行硬盘缓存的,其中Dropbox和Twitter大多数人应该都没用过,那么我们就从大家最熟悉的网易新闻开始着手分析,来对DiskLruCache有一个最初的认识吧。


初探


相信所有人都知道,网易新闻中的数据都是从网络上获取的,包括了很多的新闻内容和新闻图片,如下图所示:




但是不知道大家有没有发现,这些内容和图片在从网络上获取到之后都会存入到本地缓存中,因此即使手机在没有网络的情况下依然能够加载出以前浏览过的新闻。而使用的缓存技术不用多说,自然是DiskLruCache了,那么首先第一个问题,这些数据都被缓存在了手机的什么位置呢?


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


那么这里还是以网易新闻为例,它的客户端的包名是com.netease.newsreader.activity,因此数据缓存地址就应该是 /sdcard/Android/data/com.netease.newsreader.activity/cache ,我们进入到这个目录中看一下,结果如下图所示:




可以看到有很多个文件夹,因为网易新闻对多种类型的数据都进行了缓存,这里简单起见我们只分析图片缓存就好,所以进入到bitmap文件夹当中。然后你将会看到一堆文件名很长的文件,这些文件命名没有任何规则,完全看不懂是什么意思,但如果你一直向下滚动,将会看到一个名为journal的文件,如下图所示:




那么这些文件到底都是什么呢?看到这里相信有些朋友已经是一头雾水了,这里我简单解释一下。上面那些文件名很长的文件就是一张张缓存的图片,每个文件都对应着一张图片,而journal文件是DiskLruCache的一个日志文件,程序对每张图片的操作记录都存放在这个文件中,基本上看到journal这个文件就标志着该程序使用DiskLruCache技术了。


下载


好了,对DiskLruCache有了最初的认识之后,下面我们来学习一下DiskLruCache的用法吧。由于DiskLruCache并不是由Google官方编写的,所以这个类并没有被包含在Android API当中,我们需要将这个类从网上下载下来,然后手动添加到项目当中。DiskLruCache的源码在Google Source上,地址如下:

android.googlesource.com/platform/libcore/+/jb-mr2-release/luni/src/main/java/libcore/io/DiskLruCache.java


如果Google Source打不开的话,也可以点击这里下载DiskLruCache的源码。下载好了源码之后,只需要在项目中新建一个libcore.io包,然后将DiskLruCache.Java文件复制到这个包中即可。


打开缓存


这样的话我们就把准备工作做好了,下面看一下DiskLruCache到底该如何使用。首先你要知道,DiskLruCache是不能new出实例的,如果我们要创建一个DiskLruCache的实例,则需要调用它的open()方法,接口如下所示:

[java] view plain copy
  1. public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)  

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


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

[java] view plain copy
  1. public File getDiskCacheDir(Context context, String uniqueName) {  
  2.     String cachePath;  
  3.     if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())  
  4.             || !Environment.isExternalStorageRemovable()) {  
  5.         cachePath = context.getExternalCacheDir().getPath();  
  6.     } else {  
  7.         cachePath = context.getCacheDir().getPath();  
  8.     }  
  9.     return new File(cachePath + File.separator + uniqueName);  
  10. }  

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


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


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

[java] view plain copy
  1. public int getAppVersion(Context context) {  
  2.     try {  
  3.         PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);  
  4.         return info.versionCode;  
  5.     } catch (NameNotFoundException e) {  
  6.         e.printStackTrace();  
  7.     }  
  8.     return 1;  
  9. }  
需要注意的是,每当版本号改变,缓存路径下存储的所有数据都会被清除掉,因为DiskLruCache认为当应用程序有版本更新的时候,所有的数据都应该从网上重新获取。


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


因此,一个非常标准的open()方法就可以这样写:

[java] view plain copy
  1. DiskLruCache mDiskLruCache = null;  
  2. try {  
  3.     File cacheDir = getDiskCacheDir(context, "bitmap");  
  4.     if (!cacheDir.exists()) {  
  5.         cacheDir.mkdirs();  
  6.     }  
  7.     mDiskLruCache = DiskLruCache.open(cacheDir, getAppVersion(context), 110 * 1024 * 1024);  
  8. catch (IOException e) {  
  9.     e.printStackTrace();  
  10. }  

首先调用getDiskCacheDir()方法获取到缓存地址的路径,然后判断一下该路径是否存在,如果不存在就创建一下。接着调用DiskLruCache的open()方法来创建实例,并把四个参数传入即可。


有了DiskLruCache的实例之后,我们就可以对缓存的数据进行操作了,操作类型主要包括写入、访问、移除等,我们一个个进行学习。


写入缓存


先来看写入,比如说现在有一张图片,地址是https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg,那么为了将这张图片下载下来,就可以这样写:

[java] view plain copy
  1. private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {  
  2.     HttpURLConnection urlConnection = null;  
  3.     BufferedOutputStream out = null;  
  4.     BufferedInputStream in = null;  
  5.     try {  
  6.         final URL url = new URL(urlString);  
  7.         urlConnection = (HttpURLConnection) url.openConnection();  
  8.         in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);  
  9.         out = new BufferedOutputStream(outputStream, 8 * 1024);  
  10.         int b;  
  11.         while ((b = in.read()) != -1) {  
  12.             out.write(b);  
  13.         }  
  14.         return true;  
  15.     } catch (final IOException e) {  
  16.         e.printStackTrace();  
  17.     } finally {  
  18.         if (urlConnection != null) {  
  19.             urlConnection.disconnect();  
  20.         }  
  21.         try {  
  22.             if (out != null) {  
  23.                 out.close();  
  24.             }  
  25.             if (in != null) {  
  26.                 in.close();  
  27.             }  
  28.         } catch (final IOException e) {  
  29.             e.printStackTrace();  
  30.         }  
  31.     }  
  32.     return false;  
  33. }  
这段代码相当基础,相信大家都看得懂,就是访问urlString中传入的网址,并通过outputStream写入到本地。有了这个方法之后,下面我们就可以使用DiskLruCache来进行写入了,写入的操作是借助DiskLruCache.Editor这个类完成的。类似地,这个类也是不能new的,需要调用DiskLruCache的edit()方法来获取实例,接口如下所示:
[java] view plain copy
  1. public Editor edit(String key) throws IOException  

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


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

[java] view plain copy
  1. public String hashKeyForDisk(String key) {  
  2.     String cacheKey;  
  3.     try {  
  4.         final MessageDigest mDigest = MessageDigest.getInstance("MD5");  
  5.         mDigest.update(key.getBytes());  
  6.         cacheKey = bytesToHexString(mDigest.digest());  
  7.     } catch (NoSuchAlgorithmException e) {  
  8.         cacheKey = String.valueOf(key.hashCode());  
  9.     }  
  10.     return cacheKey;  
  11. }  
  12.   
  13. private String bytesToHexString(byte[] bytes) {  
  14.     StringBuilder sb = new StringBuilder();  
  15.     for (int i = 0; i < bytes.length; i++) {  
  16.         String hex = Integer.toHexString(0xFF & bytes[i]);  
  17.         if (hex.length() == 1) {  
  18.             sb.append('0');  
  19.         }  
  20.         sb.append(hex);  
  21.     }  
  22.     return sb.toString();  
  23. }  
代码很简单,现在我们只需要调用一下hashKeyForDisk()方法,并把图片的URL传入到这个方法中,就可以得到对应的key了。


因此,现在就可以这样写来得到一个DiskLruCache.Editor的实例:

[java] view plain copy
  1. String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
  2. String key = hashKeyForDisk(imageUrl);  
  3. DiskLruCache.Editor editor = mDiskLruCache.edit(key);  
有了DiskLruCache.Editor的实例之后,我们可以调用它的newOutputStream()方法来创建一个输出流,然后把它传入到downloadUrlToStream()中就能实现下载并写入缓存的功能了。注意newOutputStream()方法接收一个index参数,由于前面在设置valueCount的时候指定的是1,所以这里index传0就可以了。在写入操作执行完之后,我们还需要调用一下commit()方法进行提交才能使写入生效,调用abort()方法的话则表示放弃此次写入。


因此,一次完整写入操作的代码如下所示:

[java] view plain copy
  1. new Thread(new Runnable() {  
  2.     @Override  
  3.     public void run() {  
  4.         try {  
  5.             String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
  6.             String key = hashKeyForDisk(imageUrl);  
  7.             DiskLruCache.Editor editor = mDiskLruCache.edit(key);  
  8.             if (editor != null) {  
  9.                 OutputStream outputStream = editor.newOutputStream(0);  
  10.                 if (downloadUrlToStream(imageUrl, outputStream)) {  
  11.                     editor.commit();  
  12.                 } else {  
  13.                     editor.abort();  
  14.                 }  
  15.             }  
  16.             mDiskLruCache.flush();  
  17.         } catch (IOException e) {  
  18.             e.printStackTrace();  
  19.         }  
  20.     }  
  21. }).start();  
由于这里调用了downloadUrlToStream()方法来从网络上下载图片,所以一定要确保这段代码是在子线程当中执行的。注意在代码的最后我还调用了一下flush()方法,这个方法并不是每次写入都必须要调用的,但在这里却不可缺少,我会在后面说明它的作用。


现在的话缓存应该是已经成功写入了,我们进入到SD卡上的缓存目录里看一下,如下图所示:




可以看到,这里有一个文件名很长的文件,和一个journal文件,那个文件名很长的文件自然就是缓存的图片了,因为是使用了MD5编码来进行命名的。


读取缓存


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

[java] view plain copy
  1. public synchronized Snapshot get(String key) throws IOException  
很明显,get()方法要求传入一个key来获取到相应的缓存数据,而这个key毫无疑问就是将图片URL进行MD5编码后的值了,因此读取缓存数据的代码就可以这样写:
[java] view plain copy
  1. String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
  2. String key = hashKeyForDisk(imageUrl);  
  3. DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
很奇怪的是,这里获取到的是一个DiskLruCache.Snapshot对象,这个对象我们该怎么利用呢?很简单,只需要调用它的getInputStream()方法就可以得到缓存文件的输入流了。同样地,getInputStream()方法也需要传一个index参数,这里传入0就好。有了文件的输入流之后,想要把缓存图片显示到界面上就轻而易举了。所以,一段完整的读取缓存,并将图片加载到界面上的代码如下所示:
[java] view plain copy
  1. try {  
  2.     String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";  
  3.     String key = hashKeyForDisk(imageUrl);  
  4.     DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);  
  5.     if (snapShot != null) {  
  6.         InputStream is = snapShot.getInputStream(0);  
  7.         Bitmap bitmap = BitmapFactory.decodeStream(is);  
  8.         mImage.setImageBitmap(bitmap);  
  9.     }  
  10. catch (IOException e) {  
  11.     e.printStackTrace();  
  12. }  

我们使用了BitmapFactory的decodeStream()方法将文件流解析成Bitmap对象,然后把它设置到ImageView当中。如果运行一下程序,将会看到如下效果:




OK,图片已经成功显示出来了。注意这是我们从本地缓存中加载的,而不是从网络上加载的,因此即使在你手机没有联网的情况下,这张图片仍然可以显示出来。


移除缓存


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

[java] view plain copy
  1. public synchronized boolean remove(String key) throws IOException  
相信你已经相当熟悉了,remove()方法中要求传入一个key,然后会删除这个key对应的缓存图片,示例代码如下:
[java] view plain copy
  1. try {  
  2.     String imageUrl = "https://img-my.csdn.net/uploads/201309/01/1378037235_7476.jpg";    
  3.     String key = hashKeyForDisk(imageUrl);    
  4.     mDiskLruCache.remove(key);  
  5. catch (IOException e) {  
  6.     e.printStackTrace();  
  7. }  

用法虽然简单,但是你要知道,这个方法我们并不应该经常去调用它。因为你完全不需要担心缓存的数据过多从而占用SD卡太多空间的问题,DiskLruCache会根据我们在调用open()方法时设定的缓存最大值来自动删除多余的缓存。只有你确定某个key对应的缓存内容已经过期,需要从网络获取最新数据的时候才应该调用remove()方法来移除缓存。


其它API


除了写入缓存、读取缓存、移除缓存之外,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


前面已经提到过,DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容,因此,能够读懂journal文件对于我们理解DiskLruCache的工作原理有着非常重要的作用。那么journal文件中的内容到底是什么样的呢?我们来打开瞧一瞧吧,如下图所示:





由于现在只缓存了一张图片,所以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的记录,否则这条数据就是“脏”的,会被自动删除掉。


如果你足够细心的话应该还会注意到,第七行的那条记录,除了CLEAN前缀和key之外,后面还有一个152313,这是什么意思呢?其实,DiskLruCache会在每一行CLEAN记录的最后加上该条缓存数据的大小,以字节为单位。152313也就是我们缓存的那张图片的字节数了,换算出来大概是148.74K,和缓存图片刚刚好一样大,如下图所示:




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


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


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


你可能感兴趣的:(Android学习)