DiskLruCache完全解析

BLACKBERRY

重新看了下郭霖的disklrucache,现在在这里做下记录

disklrucache 是J神为解决实际开发项目中的本地缓存问题而贡献的一个小框架,整个小框架仅仅有三个类:

DiskLruCache.java
StrictLineReader.java
Util.java

我们在使用的时候的大致步骤是:

1、先得到DiskLruCache这个对象

2、操作这个对象,其中包括存,取,删等操作

3、清理缓存

下面依次介绍一下:

1、先得到DiskLruCache这个对象
(源自源码):

/* Opens the cache in {@code directory}, creating a cache if none exists there.
@param directory a writable directory
@param valueCount the number of values per cache entry. Must be positive. @param maxSize the maximum number of bytes this cache should use to store
@throws IOException if reading or writing the cache directory fails /
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);
// If journal file also exists just delete backup file.
if (journalFile.exists()) {
backupFile.delete();
} else {
renameTo(backupFile, journalFile, false);
}
}

// 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();
return cache;
} catch (IOException journalIsCorrupt) {
System.out
.println(“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;
}
我们可以看到,DiskLruCache需要通过open的方法拿到实例对象(因为DiskLruCache本身已经被单例模式了)

这个open方法里面大致做了

journal文件的创建 journal备份文件的创建 根据输入文件目录创建出文件夹 一些全局变量的初始化操作

我们在用open方法时需要传递的参数是

1.要创建的文件目录名

2.当前我们这个应用的版本号

3.设置一个文件名对应的文件数量,一般传1就可以

4.设置缓存文件的大小

需要注意的是,open的第一个参数,我们一般设置成为本机的缓存目录,现在我们的手机都已经内置内存卡了,不过我们还是可以进行一次判断,从而拿到手机中的cache缓存目录

拿缓存目录的方法是:getExternalCacheDir()或者是getCacheDir()

cache缓存目录结构是“/sdcard/Android/data/\/cache
当然如果手机比较老,而且没有挂在sd卡的话,
只能拿到这样的目录了“/data/data/\/cache


2、使用DiskLruCache这个对象进行本地缓存文件的存、取、删

我们通过open(目录,app版本号,同名文件数量,缓存控件大小)拿到DiskLruCache之后,下面我们来看一下DiskLruCache的使用方法:

存:edit +commit
取:get
删:remove
清缓存:delete

1⃣️ 存

使用方法其实很简单,其中需要注意的是,一般我们在存数据的时候,存的数据名称并不是数据本来的名称

比如说网络加载的图片,我们一般是把这个图片的网址当做名称标记,把图片网址经过md5加密方式进行加密

得到一串16进制数组成的String字符串当做文件的名字。

这里提供一个md5工具类,网上随便一搜都能搜到

    public String 2Md5(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();  
}  

所以说DiskLruCache的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 = 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;

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

edit方法调用了内部的同步方法 edit(key,-1) 思路是向以lruEntries为对象名的LinkedHashMap里面存入文件名称和Entry,那么Entry就是以后我们存入的流文件


细心的同学应该已经注意到了,edit方法执行完后,就会返回一个Editor对象,因为我们的edit方法只能传入名称,所以往DiskLruCache里面存数据的重任就交给了Editor了

往Editor里面存入数据的方法是:

public OutputStream newOutputStream(int index) throws IOException {
  if (index < 0 || index >= valueCount) {
    throw new IllegalArgumentException("Expected index " + index + " to "
            + "be greater than 0 and less than the maximum value count "
            + "of " + valueCount);
  }
  synchronized (DiskLruCache.this) {
    if (entry.currentEditor != this) {
      throw new IllegalStateException();
    }
    if (!entry.readable) {
      written[index] = true;
    }
    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”的输出流,这个“dirtyFile”是一个准备好的“缓存文件”,后缀是.tmp(“作者提供了两个文件,一个是dirtyFile 一个是cleanFile,dirtyFile会被后期清理时及时清理掉,只有经过clean后的dirtyFile才能转化成为cleanFile”)
拿到上方的OutputStream之后,我们把从网络下载下来的流写入即可存入到dirtyFile中去了。

注意:上方仅仅是进行了流的写入到了dirtyFile,然后以后想要用这个文件,需要进行commit,这样才能变成cleanFile,这些操作在调用完flush后,将会存到日志文件journal中去

2⃣️ 取

取出本地缓存的就比较简单了,下面是源码:

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

那么里面的操作基本上有这么几点

1.检查本地缓存是否关闭,确保处于未关闭状态,这是通过journalWriter来判断的(文件的存取删都跟日志文件有关系

2.检验传入参数“key”的有效性,应为J神在设计的时候对存入的文件名有特殊的规定,必须是1到120个字符以内的,并且取值范围是0-9,a-Z,只能包括下划线和中划线,所以这对文件名做了限定。

3.下面就直接从LinkedHashMap中读取名称为key的Entry数据了(因为我们存的时候就存到了此linkedHashMap之中)

4.由于日志文件和读 取 删 文件都是相关的,所以在取文件的时候也要对journal进行记录

5.比较重要的一步“redundantOpCount++;”这句代码是对journal 中的日志记录进行计数,当达到一定的临界值的时候
journal文件就会被重新创建。代码如下

  /**
 * We only rebuild the journal when it will halve the size of the journal
 * and eliminate at least 2000 ops.
 */
private boolean journalRebuildRequired() {
  final int redundantOpCompactThreshold = 2000;
  return redundantOpCount >= redundantOpCompactThreshold //
      && redundantOpCount >= lruEntries.size();
}

3⃣️ 删

有时候我们需要对特定的文件进行删除,比如我们的splash页面可能会设置成可以变化的图片,这个时候就没有必要对这个图片网址进行缓存了,而我们又不能对某个图片加载框架进行单独的 “不缓存“的设定,所以只能进行这个缓存文件的删除了。

删的操作比取还要简单

/**
  * Drops the entry for {@code key} if it exists and can be removed. Entries
  * actively being edited cannot be removed.
  *
  * @return true if an entry was removed.
  */
 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;
 }

除了跟取有几个相似点之外,其他的就是对LinkedHashMap进行了remove的操作,并且对日志文件进行了新的添加

4⃣️ 其他的一些api的使用

  1. size()
    这个方法会返回当前缓存路径下所有缓存数据的总字节数,以byte为单位,如果应用程序中需要在界面上显示当前缓存数据的总大小,就可以通过调用这个方法计算出来。

J神的DiskLruCache中存在着size的long类型的变量,变量在linkedhashmap操作的过程中也在不停的加 减控件大小,所以当用户调用的时候,只需要把这个变量返回给用户即可:(注意:我们在初始化的时候已经设定了DiskLruCache的大小,所以J神在editer提交全部的时候对size和缓存大小做了比较,如果size较大,那么就会对Journal日志文件进行重置,对缓存文件即LinkedHashMap进行释放(释放规则是下方第二块代码)

/**
   * Returns the number of bytes currently being used to store the values in
   * this cache. This may be greater than the max size if a background
   * deletion is pending.
   */
  public synchronized long size() {
    return size;
  }


private void trimToSize() throws IOException {
while (size > maxSize) {
Map.Entry toEvict = lruEntries.entrySet().iterator().next();
remove(toEvict.getKey());
}
}
释放规则我们可以看懂:即释放了最先存入的那个文件,每次当缓存内存空间大于设定的空间时,就会释放最先存入的那个文件。

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

/** Force buffered operations to the filesystem. */
  public synchronized void flush() throws IOException {
    checkNotClosed();
    trimToSize();
    journalWriter.flush();
  }

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

这个时候用户既不能存也不能取,但是文件还存在本地
/** Closes this cache. Stored values will remain on the filesystem. */
 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;
 }

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

 /**
   * Closes the cache and deletes all of its stored values. This will delete
   * all files in the cache directory including files that weren't created by
   * the cache.
   */
  public void delete() throws IOException {
    close();
    Util.deleteContents(directory);
  }

-------------------------------------------------------------------------------

      /**
   * Deletes the contents of {@code dir}. Throws an IOException if any file
   * could not be deleted, or if {@code dir} is not a readable directory.
   */
  static void deleteContents(File dir) throws IOException {
    File[] files = dir.listFiles();
    if (files == null) {
      throw new IOException("not a readable directory: " + dir);
    }
    for (File file : files) {
      if (file.isDirectory()) {
        deleteContents(file);
      }
      if (!file.delete()) {
        throw new IOException("failed to delete file: " + file);
      }
    }
  }

J神的代码大致上就这样,值得注意的是,此类是实现自Closeable这个接口的,这个接口的实际用处不大,但是却强调了本类是一个可以关闭的数据源,调用这个接口的实现方法用于释放这个类中保存的数据源。