内存缓存
由于存储空间有限的,所以达到现在就要删除之前不常用的缓存,来缓存新的可能多次使用的,因此有了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 移除该项,