Kaynağa Gözat

添加二级缓存代码分析

杨充 4 yıl önce
ebeveyn
işleme
39e297aea3

+ 4 - 1
Demo/build.gradle

@@ -64,13 +64,16 @@ dependencies {
 //    implementation project(path: ':VideoKernel')
 //    implementation project(path: ':VideoView')
 //    implementation project(path: ':MusicPlayer')
-    implementation project(path: ':VideoM3u8')
+//    implementation project(path: ':VideoM3u8')
+//    implementation project(path: ':VideoSqlLite')
 
     implementation 'cn.yc:MusicPlayer:1.0.2'
     implementation 'cn.yc:VideoPlayer:3.1.0'
     implementation 'cn.yc:VideoCache:3.0.5'
     implementation 'cn.yc:VideoKernel:3.0.6'
     implementation 'cn.yc:VideoView:3.0.5'
+    implementation 'cn.yc:VideoM3u8:1.0.0'
+    implementation 'cn.yc:VideoSqlLite:1.0.2'
 
     //自己封装的库,都有对应的案例项目【欢迎star】:https://github.com/yangchong211
     implementation 'cn.yc:YCStatusBarLib:1.5.0'

+ 3 - 3
VideoSqlLite/build.gradle

@@ -7,8 +7,8 @@ android {
     defaultConfig {
         minSdkVersion 17
         targetSdkVersion 29
-        versionCode 1
-        versionName "1.0"
+        versionCode 2
+        versionName "1.0.2"
     }
 
     buildTypes {
@@ -39,7 +39,7 @@ group = "cn.yc"
 //发布到JCenter上的项目名字,必须填写
 def libName = "VideoSqlLite"
 // 版本号,下次更新是只需要更改版本号即可
-version = "1.0.0"
+version = "1.0.2"
 
 //生成源文件
 task sourcesJar(type: Jar) {

+ 10 - 0
VideoSqlLite/read/01.内存缓存之LruCache.md

@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+

+ 146 - 0
VideoSqlLite/read/02.磁盘缓存之DiskLruCache.md

@@ -0,0 +1,146 @@
+#### 目录介绍
+- 01.磁盘缓存作用和说明
+- 02.磁盘缓存提问答疑
+- 03.做了那些细节优化
+
+
+
+
+### 01.磁盘缓存作用和说明
+#### 1.1 DiskLruCache的创建
+- DiskLruCache 并不能通过构造方法来创建,它提供了 open() 方法用于创建自身
+    ``` java
+    File path = DiskFileUtils.getFilePath(context);
+    String pathString = path.getPath();
+    try {
+        diskLruCache = DiskLruCache.open(path,1,1,1000);
+    } catch (IOException e) {
+        e.printStackTrace();
+    }
+    ```
+- open方法的四个参数说明
+    - 第一个参数表示磁盘缓存在文件系统中的存储路径。缓存路径可以选择 SD 卡上的缓存目录,具体是指 /sdcard/Android/data/<package_name>/cache 目录,其中 <package_name> 表示当前应用的包名,当应用被卸载后,此目录会一并被删除。当然也可以选择 SD 卡上的其他指定目录,还可以选择 data 下的当前应用的目录,具体可根据需要灵活设定。这里给出一个建议:如果应用卸载后就希望删除缓存文件,那么就选择 SD 卡上的缓存目录,如果希望保留缓存数据那就应该选择 SD 卡上的其他特定目录。
+    - 第二个参数表示应用的版本号,一般设为 1 即可。当版本号发生改变时 DiskLruCache 会清空之前所有的缓存文件,而这个特性在实际开发中作用并不大,很多情况下即使应用的版本号发生了改变缓存文件却仍然是有效的,因此这个参数设为 1 比较好。
+    - 第三个参数表示同一个 key 可以对应多少个缓存文件,一般设为 1 即可。
+    - 第四个参数表示缓存的总大小,比如 50MB,当缓存大小超出这个设定值后,DiskLruCache 会清除一些缓存从而保证总大小不大于这个设定值。
+
+
+
+#### 1.2 DiskLruCache添加数据
+- DiskLruCache 的缓存添加的操作是通过 Editor 完成的
+    - Editor 表示一个缓存对象的编辑对象。这里仍然以图片缓存举例,首先需要获取图片url 所对应的 key,然后根据 key 就可以通过 edit() 来获取 Editor 对象,如果这个缓存正在被编辑,那么 edit() 会返回 null,即 DiskLruCache 不允许同时编辑一个缓存对象。
+    - 之所以要把 url 转换成 key,是因为图片的 url 中很可能有特殊字符,这将影响 url 在 Android 中直接使用,一般采用 url 的 md5 值作为 key。
+- 将图片的 url 转成 key 后,就可以获取 Editor 对象了。
+    - 对于这个 key 来说,如果当前不存在其他 Editor 对象,那么 edit() 就会返回一个新的 Editor 对象,通过它就可以得到一个文件输出流。需要注意的是,由于前面在 DiskLruCache 的 open 方法中设置了一个节点只能有一个数据,因此下面的 DISK_CACHE_INDEX 常量直接设为 0 即可,如下所示
+    ```
+    String key = hashKeyFormUrl(url);
+    DiskLruCache.Editor editor = mDiskLruCache.edit(key);
+    if (editor ! = null) {
+       OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
+    }
+    ```
+- 当从网络下载图片时,图片就可以通过文件输出流写入到文件系统上,这个过程的实现如下所示
+    ```
+    public boolean downloadUrlToStream(String urlString, utputStream outputStream) {
+    	HttpURLConnection urlConnection = null;
+        BufferedOutputStream out = null;
+        BufferedInputStream in = null;
+        try {
+    		final URL url = new URL(urlString);
+    		urlConnection = (HttpURLConnection)url.openConnection();
+    		in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
+            out = new BufferedOutputStream(outputStream, IO_BUFFER_SIZE);
+    		int b;
+    		while ((b = in.read()) ! = -1) {
+    			out.write(b);
+    		}
+    		return true;
+    	} catch (IOException e) {
+    		Log.e(TAG, "Download bitmap failed. " + e);
+    	} finally {
+    		if (urlConnection ! = null) {
+    		urlConnection.disconnect();
+    	}
+        return false;
+    }
+    ```
+- 之后,还必须通过 Editor 的 commit() 来提交写入操作,真正地将图片写入文件系统。
+    - 如果图片下载过程发生了异常,那么还可以通过 Editor 的 abort() 来回退整个操作,这个过程如下所示
+    ```
+    OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
+    if (downloadUrlToStream(url, outputStream)) {
+    	editor.commit();
+    } else {
+    	editor.abort();
+    }
+    mDiskLruCache.flush();
+    ```
+- 经过上面的几个步骤,图片已经被正确地写入到文件系统了,接下来图片获取的操作就不需要请求网络了。
+
+
+
+
+#### 2.3 DiskLruCache 的查找
+- 和缓存的添加过程类似
+    - 缓存查找过程也需要将 url 转换为 key,然后通过 DiskLruCache 的 get() 方法得到一个 Snapshot 对象,接着再通过 Snapshot 对象即可得到缓存的文件输入流,有了文件输入流,自然就可以得到 Bitmap 对象了。
+    - 为了避免加载图片过程中导致的 OOM 问题,一般不建议直接加载原始图片。可以通过 BitmapFactory.Options 对象来加载一张缩放后的图片,但是那种方法对 FileInputStream 的缩放存在问题,原因是 FileInputStream 是一种有序的文件流,而两次 decodeStream 调用影响了文件流的位置属性,导致了第二次 decodeStream 时得到的是 null。
+    - 为了解决这个问题,可以通过文件流来得到它所对应的文件描述符,然后再通过 BitmapFactory.decodeFileDescriptor() 方法来加载一张缩放后的图片,这个过程的实现如下所示
+    ```
+    Bitmap bitmap = null;
+    String key = hashKeyFormUrl(url);
+    DiskLruCache.Snapshot snapShot = mDiskLruCache.get(key);
+    if (snapShot ! = null) {
+    	FileInputStream fileInputStream = (FileInputStream)snapShot.getInputStream(DISK_CACHE_INDEX);
+    	FileDescriptor fileDescriptor = fileInputStream.getFD();
+    	bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, reqWidth, reqHeight);
+        if (bitmap ! = null) {
+    		addBitmapToMemoryCache(key, bitmap);
+    	}
+    }
+    ```
+- 每个实体都是文件,你可以利用 fileInputStream 读取出里面的内容,然后做其他操作。
+    - 上面介绍了 DiskLruCache 的创建、添加和查找过程,除此之外,DiskLruCache 还提供了 remove() 、delete() 等方法用于磁盘缓存的删除操作。
+
+
+
+#### 2.4 DiskLruCache 的移除
+- remove() 方法中要求传入一个 key,然后会删除这个 key 对应的缓存。示例代码如下
+    ```
+    try {
+    	String imageUrl = "https://chittyo/img.jpg";  
+    	String key = hashKeyForDisk(imageUrl);  
+    	mDiskLruCache.remove(key);
+    } catch (IOException e) {
+    	e.printStackTrace();
+    }
+    ```
+- remove() 方法我们并不经常去调用它。
+    - 只有当你确定某个 key 对应的缓存内容已经过期,需要从网络获取最新的数据时才应该调用 remove() 方法来移除缓存。
+- 我们完全不用担心因缓存数据过多而占用太多 SD 卡空间的问题,DiskLruCache 会根据我们在调用 open() 方法时设定的缓存最大值来自动删除多余的缓存。
+
+
+
+
+### 02.磁盘缓存提问答疑
+- 存或取key如何取值?
+    - key将会成为缓存文件的文件名,并且必须要和图片的URL是一一对应的。那么怎样才能让key和图片的URL能够一一对应呢?直接使用URL来作为key?
+    - 不太合适,因为图片URL中可能包含一些特殊字符,这些字符有可能在命名文件时是不合法的。其实最简单的做法就是将图片的URL进行MD5编码,编码后的字符串肯定是唯一的,并且只会包含0-F这样的字符,完全符合文件的命名规则。
+- 这个key为何不用字符串hash而用md5值?
+    - hash可能会出现碰撞,而md5为代表的杂凑函数几乎不可能找到碰撞。看glide源码,发现源码中使用磁盘缓存图片时,对链接也是采用md5加密得到key
+- 存和取数据需要开启子线程吗?
+
+- 读写数据完后需要flush吗?
+    - 这个方法用于将内存中的操作记录同步到日志文件(也就是journal文件)当中。这个方法非常重要,因为DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容。
+    - 在写入缓存操作的时候有调用过一次这个方法,但其实并不是每次写入缓存都要调用一次flush()方法的,频繁地调用并不会带来任何好处,只会额外增加同步journal文件的时间。
+    - 比较标准的做法就是在Activity的onPause()方法中去调用一次flush()方法就可以了。
+- 移除缓存remove需要手动调用吗?
+    - remove()方法中要求传入一个key,然后会删除这个key对应的缓存图片。用法虽然简单,但是你要知道,这个方法我们并不应该经常去调用它。
+    - 因为你完全不需要担心缓存的数据过多从而占用SD卡太多空间的问题,DiskLruCache会根据我们在调用open()方法时设定的缓存最大值来自动删除多余的缓存。只有你确定某个key对应的缓存内容已经过期,需要从网络获取最新数据的时候才应该调用remove()方法来移除缓存。
+
+
+
+### 03.做了那些细节优化
+
+
+
+

+ 25 - 0
VideoSqlLite/read/03.内存磁盘二级缓存.md

@@ -0,0 +1,25 @@
+#### 目录介绍
+- 01.两种缓存简单介绍
+
+
+
+
+### 01.两种缓存简单介绍
+- 什么是二级缓存
+    - LruCache将图片保存在内存,存取速度较快,退出APP后缓存会失效;而DiskLruCache将图片保存在磁盘中,下次进入应用后缓存依旧存在,它的存取速度相比LruCache会慢上一些。
+- DiskLruCache持久存储
+    - DiskLruCache最大的特点就是持久化存储,所有的缓存以文件的形式存在。在用户进入APP时,它根据日志文件将DiskLruCache恢复到用户上次退出时的情况,日志文件journal保存每个文件的下载、访问和移除的信息,在恢复缓存时逐行读取日志并检查文件来恢复缓存。
+
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 573 - 0
VideoSqlLite/read/04.LruCache源码分析.md

@@ -0,0 +1,573 @@
+#### 目录介绍
+- 01.图片缓存策略
+    - 1.1 两级缓存
+    - 1.2 LruCache介绍
+- 02.LruCache源码分析
+    - 2.1 源代码
+    - 2.2 构造方法分析
+    - 2.3 put源码分析
+    - 2.4 trimToSize(maxSize)
+    - 2.5 get源码分析
+
+
+
+
+
+### 01.图片缓存策略
+#### 1.1 两级缓存
+- 现在主流的app中图片等资源的缓存策略一般是分两级,一个是内存级别的缓存,一个是磁盘级别的缓存。
+- 作为android系统的维护者google也开源了其缓存方案,LruCache和DiskLruCache。
+    - 从android3.1开始LruCache已经作为android源码的一部分维护在android系统中,为了兼容以前的版本android的support-v4包也提供了LruCache的维护,如果App需要兼容到android3.1之前的版本就需要使用support-v4包中的LruCache,如果不需要兼容到android3.1则直接使用android源码中的LruCache即可。
+    - 需要注意的是DiskLruCache并不是android源码的一部分。
+
+
+#### 1.2 LruCache介绍
+- 在LruCache的源码中,关于LruCache有这样的一段介绍:
+    - cache对象通过一个强引用来访问内容。每次当一个item被访问到的时候,这个item就会被移动到一个队列的队首。当一个item被添加到已经满了的队列时,这个队列的队尾的item就会被移除。
+    ```
+    A cache that holds strong references to a limited number of values. Each time a value is accessed, it is moved to the head of a queue. When a value is added to a full cache, the value at the end of that queue is evicted and may become eligible for garbage collection.
+    ```
+
+#### 1.3 LruCache核心思想
+- LRU是近期最少使用的算法,它的核心思想是当缓存满时,会优先淘汰那些近期最少使用的缓存对象。采用LRU算法的缓存有两种:LrhCache和DisLruCache,分别用于实现内存缓存和硬盘缓存,其核心思想都是LRU缓存算法。
+
+
+
+### 02.LruCache源码分析
+#### 2.1 源代码
+- 下面具体看一下LruCache的实现:
+    ```
+    public class LruCache<K, V> {
+        private final LinkedHashMap<K, V> map;
+    
+        /** Size of this cache in units. Not necessarily the number of elements. */
+        private int size;
+        private int maxSize;
+    
+        private int putCount;
+        private int createCount;
+        private int evictionCount;
+        private int hitCount;
+        private int missCount;
+    
+        /**
+         * @param maxSize for caches that do not override {@link #sizeOf}, this is
+         *     the maximum number of entries in the cache. For all other caches,
+         *     this is the maximum sum of the sizes of the entries in this cache.
+         */
+        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);
+        }
+    
+        /**
+         * Sets the size of the cache.
+         *
+         * @param maxSize The new maximum size.
+         */
+        public void resize(int maxSize) {
+            if (maxSize <= 0) {
+                throw new IllegalArgumentException("maxSize <= 0");
+            }
+    
+            synchronized (this) {
+                this.maxSize = maxSize;
+            }
+            trimToSize(maxSize);
+        }
+    
+        /**
+         * Returns the value for {@code key} if it exists in the cache or can be
+         * created by {@code #create}. If a value was returned, it is moved to the
+         * head of the queue. This returns null if a value is not cached and cannot
+         * be created.
+         */
+        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++;
+            }
+    
+            /*
+             * Attempt to create a value. This may take a long time, and the map
+             * may be different when create() returns. If a conflicting value was
+             * added to the map while create() was working, we leave that value in
+             * the map and release the created value.
+             */
+    
+            V createdValue = create(key);
+            if (createdValue == null) {
+                return null;
+            }
+    
+            synchronized (this) {
+                createCount++;
+                mapValue = map.put(key, createdValue);
+    
+                if (mapValue != null) {
+                    // There was a conflict so undo that last put
+                    map.put(key, mapValue);
+                } else {
+                    size += safeSizeOf(key, createdValue);
+                }
+            }
+    
+            if (mapValue != null) {
+                entryRemoved(false, key, createdValue, mapValue);
+                return mapValue;
+            } else {
+                trimToSize(maxSize);
+                return createdValue;
+            }
+        }
+    
+        /**
+         * Caches {@code value} for {@code key}. The value is moved to the head of
+         * the queue.
+         *
+         * @return the previous value mapped by {@code key}.
+         */
+        public final V put(K key, V value) {
+            if (key == null || value == null) {
+                throw new NullPointerException("key == null || value == null");
+            }
+    
+            V previous;
+            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;
+        }
+    
+        /**
+         * Remove the eldest entries until the total of remaining entries is at or
+         * below the requested size.
+         *
+         * @param maxSize the maximum size of the cache before returning. May be -1
+         *            to evict even 0-sized elements.
+         */
+        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!");
+                    }
+    
+                    if (size <= maxSize) {
+                        break;
+                    }
+    
+                    Map.Entry<K, V> toEvict = map.eldest();
+                    if (toEvict == null) {
+                        break;
+                    }
+    
+                    key = toEvict.getKey();
+                    value = toEvict.getValue();
+                    map.remove(key);
+                    size -= safeSizeOf(key, value);
+                    evictionCount++;
+                }
+    
+                entryRemoved(true, key, value, null);
+            }
+        }
+    
+        /**
+         * Removes the entry for {@code key} if it exists.
+         *
+         * @return the previous value mapped by {@code key}.
+         */
+        public final V remove(K key) {
+            if (key == null) {
+                throw new NullPointerException("key == null");
+            }
+    
+            V previous;
+            synchronized (this) {
+                previous = map.remove(key);
+                if (previous != null) {
+                    size -= safeSizeOf(key, previous);
+                }
+            }
+    
+            if (previous != null) {
+                entryRemoved(false, key, previous, null);
+            }
+    
+            return previous;
+        }
+    
+        /**
+         * Called for entries that have been evicted or removed. This method is
+         * invoked when a value is evicted to make space, removed by a call to
+         * {@link #remove}, or replaced by a call to {@link #put}. The default
+         * implementation does nothing.
+         *
+         * <p>The method is called without synchronization: other threads may
+         * access the cache while this method is executing.
+         *
+         * @param evicted true if the entry is being removed to make space, false
+         *     if the removal was caused by a {@link #put} or {@link #remove}.
+         * @param newValue the new value for {@code key}, if it exists. If non-null,
+         *     this removal was caused by a {@link #put}. Otherwise it was caused by
+         *     an eviction or a {@link #remove}.
+         */
+        protected void entryRemoved(boolean evicted, K key, V oldValue, V newValue) {}
+    
+        /**
+         * Called after a cache miss to compute a value for the corresponding key.
+         * Returns the computed value or null if no value can be computed. The
+         * default implementation returns null.
+         *
+         * <p>The method is called without synchronization: other threads may
+         * access the cache while this method is executing.
+         *
+         * <p>If a value for {@code key} exists in the cache when this method
+         * returns, the created value will be released with {@link #entryRemoved}
+         * and discarded. This can occur when multiple threads request the same key
+         * at the same time (causing multiple values to be created), or when one
+         * thread calls {@link #put} while another is creating a value for the same
+         * key.
+         */
+        protected V create(K key) {
+            return null;
+        }
+    
+        private int safeSizeOf(K key, V value) {
+            int result = sizeOf(key, value);
+            if (result < 0) {
+                throw new IllegalStateException("Negative size: " + key + "=" + value);
+            }
+            return result;
+        }
+    
+        /**
+         * Returns the size of the entry for {@code key} and {@code value} in
+         * user-defined units.  The default implementation returns 1 so that size
+         * is the number of entries and max size is the maximum number of entries.
+         *
+         * <p>An entry's size must not change while it is in the cache.
+         */
+        protected int sizeOf(K key, V value) {
+            return 1;
+        }
+    
+        /**
+         * Clear the cache, calling {@link #entryRemoved} on each removed entry.
+         */
+        public final void evictAll() {
+            trimToSize(-1); // -1 will evict 0-sized elements
+        }
+    
+        /**
+         * For caches that do not override {@link #sizeOf}, this returns the number
+         * of entries in the cache. For all other caches, this returns the sum of
+         * the sizes of the entries in this cache.
+         */
+        public synchronized final int size() {
+            return size;
+        }
+    
+        /**
+         * For caches that do not override {@link #sizeOf}, this returns the maximum
+         * number of entries in the cache. For all other caches, this returns the
+         * maximum sum of the sizes of the entries in this cache.
+         */
+        public synchronized final int maxSize() {
+            return maxSize;
+        }
+    
+        /**
+         * Returns the number of times {@link #get} returned a value that was
+         * already present in the cache.
+         */
+        public synchronized final int hitCount() {
+            return hitCount;
+        }
+    
+        /**
+         * Returns the number of times {@link #get} returned null or required a new
+         * value to be created.
+         */
+        public synchronized final int missCount() {
+            return missCount;
+        }
+    
+        /**
+         * Returns the number of times {@link #create(Object)} returned a value.
+         */
+        public synchronized final int createCount() {
+            return createCount;
+        }
+    
+        /**
+         * Returns the number of times {@link #put} was called.
+         */
+        public synchronized final int putCount() {
+            return putCount;
+        }
+    
+        /**
+         * Returns the number of values that have been evicted.
+         */
+        public synchronized final int evictionCount() {
+            return evictionCount;
+        }
+    
+        /**
+         * Returns a copy of the current contents of the cache, ordered from least
+         * recently accessed to most recently accessed.
+         */
+        public synchronized final Map<K, V> snapshot() {
+            return new LinkedHashMap<K, V>(map);
+        }
+    
+        @Override public synchronized final String toString() {
+            int accesses = hitCount + missCount;
+            int hitPercent = accesses != 0 ? (100 * hitCount / accesses) : 0;
+            return String.format("LruCache[maxSize=%d,hits=%d,misses=%d,hitRate=%d%%]",
+                    maxSize, hitCount, missCount, hitPercent);
+        }
+    }
+    ```
+
+#### 2.2 构造方法
+- 可以看到LruCache初始化的时候需要使用泛型,一般这样初始化LruCache对象:
+    ```
+    // 获取应用程序最大可用内存  
+    int maxMemory = (int) Runtime.getRuntime().maxMemory();  
+    int cacheSize = maxMemory / 8;  
+    // 设置图片缓存大小为程序最大可用内存的1/8  
+    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {  
+        @Override  
+        protected int sizeOf(String key, Bitmap bitmap) {  
+            return bitmap.getByteCount();  
+        }  
+    };  
+    ```
+- 例如通过String作为key保存bitmap对象,同时需要传递一个int型的maxSize数值,主要用于设置LruCache链表的最大值。
+    - 查看其构造方法:
+    ```
+    // 获取应用程序最大可用内存  
+    int maxMemory = (int) Runtime.getRuntime().maxMemory();  
+    int cacheSize = maxMemory / 8;  
+    // 设置图片缓存大小为程序最大可用内存的1/8  
+    mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {  
+        @Override  
+        protected int sizeOf(String key, Bitmap bitmap) {  
+            return bitmap.getByteCount();  
+        }  
+    };  
+    ```
+- 可以看到其主要的是初始化了maxSize和map链表对象。
+
+
+
+#### 2.3 put源码分析
+- 查看put方法:
+    - 需要传递两个参数:K和V,首先做了一下参数的判断,然后定义一个保存前一个Value值得临时变量,让putCount(put执行的次数)自增,让map的size大小自增。
+    - LruCache put方法,将键值对压入Map数据结构中,若这是Map的大小已经大于LruCache中定义的最大值,则将Map中最早压入的元素remove掉
+        ```
+        public final V put(K key, V value) {
+            if (key == null || value == null) {
+                throw new NullPointerException("key == null || value == null");
+            }
+        
+            V previous;
+            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;
+        }
+        ```
+    - 需要注意的是这里的
+        ```
+        previous = map.put(key, value);
+        ```
+    - 看一下这里的map.put()的具体实现:
+        ```
+        @Override 
+        public V put(K key, V value) {
+            if (key == null) {
+                return putValueForNullKey(value);
+            }
+        
+            int hash = Collections.secondaryHash(key);
+            HashMapEntry<K, V>[] tab = table;
+            int index = hash & (tab.length - 1);
+            for (HashMapEntry<K, V> e = tab[index]; e != null; e = e.next) {
+                if (e.hash == hash && key.equals(e.key)) {
+                    preModify(e);
+                    V oldValue = e.value;
+                    e.value = value;
+                    return oldValue;
+                }
+            }
+        
+            // No entry for (non-null) key is present; create one
+            modCount++;
+            if (size++ > threshold) {
+                tab = doubleCapacity();
+                index = hash & (tab.length - 1);
+            }
+            addNewEntry(key, value, hash, index);
+            return null;
+        }
+        ```
+    - 将Key与Value的值压入Map中,这里判断了一下如果map中已经存在该key,value键值对,则不再压入map,并将Value值返回,否则将该键值对压入Map中,并返回null;返回继续put方法:
+        ```
+        previous = map.put(key, value);
+        if (previous != null) {
+            size -= safeSizeOf(key, previous);
+        }
+        ```
+    - 可以看到这里判断map.put方法的返回值是否为空,如果不为空的话,则说明我们刚刚并没有将我么你的键值对压入Map中,所以这里的size需要自减;然后下面:
+        ```
+        if (previous != null) {
+            entryRemoved(false, key, previous, value);
+        }
+        ```
+    - 这里判断previous是否为空,如果不为空的话,调用了一个空的实现方法entryRemoved(),也就是说我们可以实现自己的LruCache并在添加缓存的时候若存在该缓存可以重写这个方法;
+
+
+#### 2.4 trimToSize(maxSize)
+- 下面调用了trimToSize(maxSize)方法:
+    - 该方法主要是判断该Map的大小是否已经达到阙值,若达到,则将Map队尾的元素(最不常使用的元素)remove掉。
+    ```
+    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!");
+                }
+    
+                if (size <= maxSize) {
+                    break;
+                }
+    
+                Map.Entry<K, V> toEvict = map.eldest();
+                if (toEvict == null) {
+                    break;
+                }
+    
+                key = toEvict.getKey();
+                value = toEvict.getValue();
+                map.remove(key);
+                size -= safeSizeOf(key, value);
+                evictionCount++;
+            }
+    
+            entryRemoved(true, key, value, null);
+        }
+    }
+    ```
+
+
+
+#### 2.5 get源码分析
+- 查看get方法:
+    - 可以看到参数值为Key,简单的理解就是通过key值从map中取出Value值。
+    - 具体来说,判断map中是否含有key值value值,若存在,则hitCount(击中元素数量)自增,并返回Value值,若没有击中,则执行create(key)方法,这里看到create方法是一个空的实现方法,返回值为null,所以可以重写该方法,在调用get(key)的时候若没有找到value值,则自动创建一个value值并压入map中。
+    ```
+    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++;
+        }
+    
+        /*
+         * Attempt to create a value. This may take a long time, and the map
+         * may be different when create() returns. If a conflicting value was
+         * added to the map while create() was working, we leave that value in
+         * the map and release the created value.
+         */
+    
+        V createdValue = create(key);
+        if (createdValue == null) {
+            return null;
+        }
+    
+        synchronized (this) {
+            createCount++;
+            mapValue = map.put(key, createdValue);
+    
+            if (mapValue != null) {
+                // There was a conflict so undo that last put
+                map.put(key, mapValue);
+            } else {
+                size += safeSizeOf(key, createdValue);
+            }
+        }
+    
+        if (mapValue != null) {
+            entryRemoved(false, key, createdValue, mapValue);
+            return mapValue;
+        } else {
+            trimToSize(maxSize);
+            return createdValue;
+        }
+    }
+    ```
+
+
+
+
+
+
+
+
+
+
+
+
+

+ 648 - 0
VideoSqlLite/read/05.DiskLruCache源码分析.md

@@ -0,0 +1,648 @@
+#### 目录介绍
+- 01.DiskLruCache简单介绍
+- 02.为什么该方式存取效率高
+- 03.缓存日志journal分析
+- 04.DiskLruCache的open()
+- 05.DiskLruCache的edit()
+- 06.DiskLruCache的get()
+- 07.DiskLruCache的remove()
+- 08.DiskLruCache其他方法
+
+
+
+
+### 01.DiskLruCache简单介绍
+- 文件存储
+    - DiskLruCache 用于实现存储设备缓存,即磁盘缓存,它通过将缓存对象写入文件系统从而实现缓存的效果。DiskLruCache 得到了 Android 官方文档的推荐,但它不属于 Android SDK 的一部分,它的 源码及网址文末会贴出来。
+- 缓存Key和Value形式
+    - DiskLruCache是一种存在于文件系统上的缓存,用户可以为它的存储空间设定一个最大值。
+    - 每个缓存实体被称为Entry,它有一个String类型的Key,一个Key对应特定数量的Values,每个Key必须满足[a-z0-9_-]{1,64}这个正则表达式。
+    - 缓存的Value是字节流,可以通过Stream或者文件访问,每个文件的字节大小需要在(0, Integer.MAX_VALUE)之间。
+- 缓存文件处理
+    - 缓存被保存在文件系统的一个文件夹内,该文件夹必须是当前DiskLruCache专用的,DiskLruCache可能会对该文件夹内的文件删除或重写。多进程同时使用相同的缓存文件夹会引发错误。
+    - DiskLruCache对保存在文件系统中的总字节大小设定了最大值,当大小超过最大值时会在后台移除部分缓存实体直到缓存大小达标。该最大值不是严格的,当DiskLruCache在删除Entry时,缓存的整体大小可能会临时超过预设的最大值。该最大值不包括文件系统开销和journal日志文件,因此空间敏感型应用可以设置一个保守的最大值。
+- 用户编辑处理
+    - 用户调用edit()方法来创建或者更改一个Entry的Value,一个Entry在同一时刻只能拥有一个Editor,如果一个Value无法被修改,那么edit()方法会返回null。
+    - 当一个Entry被创建时,需要为该Entry提供完整的Value集合,如有必要,应该使用空的Value作为占位符。当一个Entry被修改的时候,没有必要对所有的Value都设置新的值,Value默认使用原先的值。
+    - 每一个edit()方法调用都与一个commit()或者abort()调用配对,commit操作是原子的,一个Read操作会观察commit前后的完整Value集合。用户通过get()方法读取一个Entry的快照,此时读取到的是get()方法被调用时的值,之后的更新操作并不会影响正在进行中的Read操作。
+- 缓存IO错误处理
+    - DiskLruCache可以容忍IO错误,如果某些缓存文件消失,对应的Entry会被删除。如果在写一个缓存Value时出现了一个错误,edit会静默失败。对于其他错误,调用方需要捕获IOException并处理。
+
+
+### 02.为什么该方式存取效率高
+
+
+### 03.缓存日志journal分析
+- DiskLruCache 能够正常工作是依赖于 journal 文件中的内容,journal 文件中会存储每次读取操作的记录。
+    - 我们来看看 journal 文件中的内容是什么样的吧,如下所示
+    ```
+    libcore.io.DiskLruCache
+    1
+    1
+    1
+    
+    DIRTY 27c7e00adbacc71dc793e5e7bf02f861
+    CLEAN 27c7e00adbacc71dc793e5e7bf02f861 1208
+    READ 27c7e00adbacc71dc793e5e7bf02f861
+    DIRTY b80f9eec4b616dc6682c7fa8bas2061f
+    CLEAN b80f9eec4b616dc6682c7fa8bas2061f 1208
+    READ b80f9eec4b616dc6682c7fa8bas2061f
+    DIRTY be3fgac81c12a08e89088555d85dfd2b
+    CLEAN be3fgac81c12a08e89088555d85dfd2b 99
+    READ be3fgac81c12a08e89088555d85dfd2b
+    DIRTY 536990f4dbddfghcfbb8f350a941wsxd
+    REMOVE 536990f4dbddfghcfbb8f350a941wsxd
+    ```
+- 来看一下前五行:
+    - libcore.io.DiskLruCache 是固定字符串,表明使用的是 DiskLruCache 技术;
+    - DiskLruCache 的版本号,源码中为常量 1;
+    - APP 的版本号,即我们在 open() 方法里传入的版本号;
+    - valueCount,这个值也是在 open() 方法中传入的,指每个 key 对应几个文件,通常情况下都为 1;
+    - 空行
+- 前五行是该文件的文件头,DiskLruCache 初始化的时候,如果该文件存在,就需要校验该文件头。
+    - 接下来看下操作记录:以 DIRTY 为前缀开始的行,后面是缓存文件的 key。DIRTY 英文是“脏的” 的意思,此处译为脏数据。
+    - 每当我们调用一次 DiskLruCache 的 edit() 方法时,都会向 journal 文件中写入一条 DIRTY 记录,表示我们正准备写入一条缓存数据,但不知结果如何。然后调用 commit() 方法表示写入缓存成功,这时会向 journal 中写入一条 CLEAN 记录,意味着这条 “脏” 数据被 “洗干净了” ,调用 abort() 方法表示写入缓存失败,这时会向 journal 中写入一条 REMOVE 记录。也就是说,每一行 DIRTY 的 key,后面都应该有一行对应的 CLEAN 或者 REMOVE 的记录,否则这条数据就是 “脏” 的,会被自动删除掉。
+- 下面来看各个状态的含义:
+    - ① DIRTY: 该状态表示一个Entry正在被创建或正在被更新,任意一个成功的DIRTY操作后面都会有一个CLEAN或REMOVE操作。如果一个DIRTY操作后面没有CLEAN或者REMOVE操作,那就表示这是一个临时文件,应该将其删除。
+    - ② CLEAN: 该状态表示一个缓存Entry已经被成功发布了并且可以读取,该行后面会有每个Value的大小。
+    - ③ READ: 在LRU缓存中被读取了。
+    - ④ REMOVE: 表示被删除的缓存Entry。
+- REMOVE 除了上述的情况,当你自己手动调用 remove(key) 方法的时候也会写入一条 REMOVE 记录。
+    - 如果你足够细心的话应该还会注意到,第七行的那条记录,除了 CLEAN 前缀和 key 之外,后面还有一个1208,这是什么意思呢?
+    - 其实,DiskLruCache 会在每一行 CLEAN 记录的最后加上该条缓存数据的大小,以字节为单位。1208 也就是我们缓存文件的字节数了。
+    - 源码中的 size() 方法可以获取到当前缓存路径下所有缓存数据的总字节数,其实它的工作原理就是把 journal 文件中所有 CLEAN 记录的字节数相加,返回求出的总和。
+- 前缀是 READ 的记录,每当我们调用 get() 方法去读取一条缓存数据时,就会向 journal 文件中写入一条 READ 记录。
+    - 因此,图片和数据量都非常大的 APP 的 journal 文件中就可能会有大量的 READ 记录。那如果我不停地频繁操作的话,就会不断地向 journal 文件中写入数据,那这样 journal 文件岂不是会越来越大?
+    - 这倒不必担心,DiskLruCache 中使用了一个 redundantOpCount 变量来记录用户操作的次数,每执行一次写入、读取或移除缓存的操作,这个变量值都会加 1,当变量值达到 2000 的时候就会触发重构 journal 的事件,这时会自动把 journal 中一些多余的、不必要的记录全部清除掉,保证 journal 文件的大小始终保持在一个合理的范围内。
+
+
+
+### 04.DiskLruCache的open()
+- 源码如下所示
+    ```
+    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;
+    }
+    ```
+- 首先检查 journal 的备份文件是否存在( journal.bkp),如果备份存在,然后检查 journal 文件是否存在,如果 journal 文件存在,备份文件就可以删除了;如果 journal 文件不存在,将备份文件文件重命名为 journal 文件。
+    - 然后检查 journal 文件是否存在,如果不存在,创建 directory,重新构造 DiskLruCache,再调用 rebuildJournal() 建立 journal 文件。
+    ```
+    /**
+    * Creates a new journal that omits redundant information. This replaces the
+    * current journal if it exists.
+    */
+    private synchronized void rebuildJournal() throws IOException {
+        if (journalWriter != null) {
+          journalWriter.close();
+        }
+    
+        Writer writer = new BufferedWriter(
+            new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII));
+        try {
+          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');
+            }
+          }
+        } finally {
+          writer.close();
+        }
+    
+        if (journalFile.exists()) {
+          renameTo(journalFile, journalFileBackup, true);
+        }
+        renameTo(journalFileTmp, journalFile, false);
+        journalFileBackup.delete();
+    
+        journalWriter = new BufferedWriter(
+            new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII));
+    }
+    ```
+- 可以看到首先构建一个 journal.tmp 文件,然后写入文件头(5行),然后遍历 lruEntries( LinkedHashMap<String, Entry> lruEntries =
+new LinkedHashMap<String, Entry>(0, 0.75f, true); ),此时 lruEntries 里没有任何数据。接下来将 tmp 文件重命名为 journal 文件。这样一个 journal 文件便生成了。如果存在,那么调用 readJournal()
+    ```
+    private void readJournal() throws IOException {
+        StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII);
+        try {
+          String magic = reader.readLine();
+          String version = reader.readLine();
+          String appVersionString = reader.readLine();
+          String valueCountString = reader.readLine();
+          String blank = reader.readLine();
+          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 {
+              readJournalLine(reader.readLine());
+              lineCount++;
+            } catch (EOFException endOfJournal) {
+              break;
+            }
+          }
+          redundantOpCount = lineCount - lruEntries.size();
+    
+          // If we ended on a truncated line, rebuild the journal before appending to it.
+          if (reader.hasUnterminatedLine()) {
+            rebuildJournal();
+          } else {
+            journalWriter = new BufferedWriter(new OutputStreamWriter(
+                new FileOutputStream(journalFile, true), Util.US_ASCII));
+          }
+        } finally {
+          Util.closeQuietly(reader);
+        }
+    }
+    ```
+- 首先校验文件头,接下来调用 readJournalLine 按行读取内容。
+    ```
+    private void readJournalLine(String line) throws IOException {
+        int firstSpace = line.indexOf(' ');
+        if (firstSpace == -1) {
+          throw new IOException("unexpected journal line: " + line);
+        }
+    
+        int keyBegin = firstSpace + 1;
+        int secondSpace = line.indexOf(' ', keyBegin);
+        final String key;
+        if (secondSpace == -1) {
+          key = line.substring(keyBegin);
+          if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
+            lruEntries.remove(key);
+            return;
+          }
+        } else {
+          key = line.substring(keyBegin, secondSpace);
+        }
+    
+        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;
+          entry.currentEditor = null;
+          entry.setLengths(parts);
+        } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) {
+          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);
+        }
+    }
+    ```
+- journal 日志每一行中的各个部分都是用 ' ' 空格来分割的,所以先用 空格来截取一下。拿到 key,然后判断 firstSpace 是 REMOVE 就会调用 lruEntries.remove(key); ,若不是 REMOVE ,如果该 key 没有加入到 lruEntries ,则创建并且加入。然后继续判断 firstSpace ,若是 CLEAN ,则初始化 entry ,设置 readable=true , currentEditor 为 null ,初始化长度等。若是 DIRTY ,则设置 currentEditor 对象。若是 READ,无操作。
+- 一般正常操作下 DIRTY 不会单独出现,会和 REMOVE or CLEAN 成对出现;经过上面这个流程,基本上加入到 lruEntries 里面的只有 CLEAN 且没有被 REMOVE 的 key。
+- 然后我们回到 readJournal 方法,在我们按行读取的时候,会记录一下 lineCount ,然后赋值给 redundantOpCount ,该变量记录的是多余的记录条数( redundantOpCount = lineCount - lruEntries.size(); 文件的行数-真正可以的 key 的行数)。最后,如果读取过程中发现 journal 文件有问题,则重建 journal 文件。没有问题的话,初始化下 journalWriter,关闭 reader。
+- 我们再回到 open() 方法中,readJournal 完成了,会继续调用 processJournal() 这个方法
+    ```
+    /**
+    * Computes the initial size and collects garbage as a part of opening the
+    * cache. Dirty entries are assumed to be inconsistent and will be deleted.
+    */
+    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();
+          }
+        }
+    }
+    ```
+- 计算缓存初始大小,赋值给 size,并收集垃圾作为打开缓存的一部分。对于所有非法 DIRTY 状态(就是 DIRTY 单独出现的)的 entry,如果存在 文件则删除,并且从 lruEntries 中移除。此时,剩下的就只有 CLEAN 状态的 key 记录了。
+- 到此 DiskLruCache 就初始化完毕了,为了方便记忆,我们来捋一下流程:
+    - 根据我们传入的 directory,去找 journal 文件,如果没找到,则创建一个,只写入文件头 (5行) 。
+    - 如果找到,则遍历该文件,将里面所有的 CLEAN 记录的 key,存到 lruEntries 中。
+    - 经过 open 以后,journal 文件肯定存在了,lruEntries 里面肯定有值了,size 为当前所有实体占据的容量。
+
+
+### 05.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;
+      }
+      
+     private void validateKey(String key) {
+        Matcher matcher = LEGAL_KEY_PATTERN.matcher(key);
+        if (!matcher.matches()) {
+          throw new IllegalArgumentException("keys must match regex "
+                  + STRING_KEY_PATTERN + ": \"" + key + "\"");
+        }
+     }
+    
+    private void checkNotClosed() {
+        if (journalWriter == null) {
+          throw new IllegalStateException("cache is closed");
+        }
+    }
+    ```
+- 首先验证 key,必须是由字母、数字、下划线、横线(-) 组成,且长度在 1-120 之间。
+    - 然后通过 key 获取实体 Entry ,如果 Entry 不存在,则创建一个 Entry 加入到 lruEntries 中;如果存在且不是正在编辑的实体,则直接使用。然后为 entry.currentEditor 进行赋值为 new Editor(entry); ,最后在 journal 文件中写入一条 DIRTY 记录,代表这个文件正在被操作。拿到 editor 对象以后,就是去调用 newOutputStream 去获得一个文件输入流了。
+    ```
+    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);
+      }
+    }
+    ```
+- 首先校验 index 是否在 (0, valueCount] 范围内,一般我们使用都是一个 key 对应一个文件,所以传入的基本都是 0。接下来就是通过 entry.getDirtyFile(index); 拿到一个 dirtyFile 对象,其实就是个中转文件,文件格式为 key.index.tmp ,将这个文件的 FileOutputStream 通过 FaultHidingOutputStream 封装下传给我们。
+- 最后,我们通过 outputStream 写入数据以后,需要调用 commit 方法
+    ```
+    /**
+     * Commits this edit so it is visible to readers.  This releases the
+     * edit lock so another edit may be started on the same key.
+     */
+    public void commit() throws IOException {
+      if (hasErrors) {
+        completeEdit(this, false);
+        remove(entry.key); // The previous entry is stale.
+      } else {
+        completeEdit(this, true);
+      }
+      committed = true;
+    }
+    ```
+- 首先通过 hasErrors 判断,是否有错误发生,如果有就调用 completeEdit(this, false); 和 remove(entry.key); ;如果没有就调用 completeEdit(this, true); 。
+    - 那么这里这个 hasErrors 哪来的呢?还记得上面 newOutputStream 的时候,返回了一个 outputStream,这个 outputStream 是 FileOutputStream ,但是经过了 FaultHidingOutputStream 的封装,这个类实际上就是重写了 FilterOutputStream 的 write 相关方法,将所有的 IOException 给屏蔽了,如果发生 IOException 就将 hasErrors 赋值为 true。这样的设计的好处是不直接将 OutputStream 返回给用户,如果出错可以检测到,不需要用户手动去调用一些操作。
+- 下面看看 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;
+            }
+          }
+        }
+    
+        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');
+        }
+        journalWriter.flush();
+    
+        if (size > maxSize || journalRebuildRequired()) {
+          executorService.submit(cleanupCallable);
+        }
+    }
+    ```
+- 首先判断 if (success && !entry.readable) 是否成功,且是第一次写入(如果以前这个记录有值,则 readable = true ),内部的判断,我们都不会走,因为 written[i] 在 newOutputStream 的时候被写入 true 了。而且正常情况下,getDirtyFile 是存在的。
+- 然后如果成功,将 dirtyFile 进行重命名为 cleanFile,文件名为:key.index。然后刷新 size 的长度。如果失败,则删除 dirtyFile .
+- 然后,如果成功或者 readable 为 true ,将 readable 设置为 true ,写入一条 CLEAN 记录。如果第一次提交且失败,那么就会从 lruEntries.remove(key) ,写入一条 REMOVE 记录。
+- 写入缓存,要判断是否超过了最大 size ,或者需要重建 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();
+      }
+    ```
+- 如果 redundantOpCount 达到 2000,且超过了 lruEntries.size() 就会重建,这里就可以看到 redundantOpCount 的作用了。防止 journal 文件过大。到此我们的存入缓存就分析完成了。
+- 总结:首先调用 editor,拿到指定的 dirtyFile 的 OutputStream ,然后开始写操作,写完后,记得调用 commit .
+commit 中会检测你是否发生 IOException ,若无,则将 dirtyFile -> cleanFile ,将 readable = true ,写入 CLEAN 记录。如若发生错误,则删除 dirtyFile ,从 lruEntries 中移除,然后写入一条 REMOVE 记录。
+
+
+
+### 06.DiskLruCache的get()
+- 源码如下所示
+    ```
+    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 取到的 Entry 为 null ,或者 readable = false ,则返回 null,否则将 cleanFile 的 FileInputStream 进行封装返回 Snapshot,且写入一条 READ 语句。然后 getInputStream 就是返回该 FileInputStream 了。
+
+
+
+
+### 07.DiskLruCache的remove()
+- 源码如下所示
+    ```
+    /**
+    * 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;
+    }
+    ```
+- 如果实体存在且不是正在被编辑,就可以直接进行删除,然后写入一条 REMOVE 记录。
+    - remove() 与 open() 对应,在使用完成 cache 后可以手动关闭。
+
+
+### 08.DiskLruCache其他方法
+#### 8.1 DiskLruCache 的 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;
+    }
+    ```
+- 这个方法用于将 DiskLruCache 关闭掉,关闭前,会判断所有正在编辑的实体,调用 abort() 方法,最后关闭 journalWriter 。close() 是和 open() 方法对应的一个方法。关闭之后就不能再调用 DiskLruCache 中任何操作缓存数据的方法,通常只应该在 Activity 的 onDestroy() 方法中去调用 close() 方法。
+- 其中 abort() 方法是存储失败时的逻辑,中止此编辑。这将释放编辑锁,以便其他操作可以获取编辑锁,操作同一个 key。
+    ```
+    /**
+     * Aborts this edit. This releases the edit lock so another edit may be
+     * started on the same key.
+     */
+    public void abort() throws IOException {
+        completeEdit(this, false);
+    }
+    ```
+
+
+
+#### 8.2 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();
+        IoUtils.deleteContents(directory);
+    }
+    ```
+- 这个方法用于将所有的缓存数据全部删除。比如 APP 中手动清理缓存功能,只需要调用一下 DiskLruCache 的 delete() 方法就可以实现了。
+
+
+#### 8.3 DiskLruCache 的 size()
+- 如下所示
+    ```
+     /**
+     * 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;
+    }
+    ```
+- 这个方法会返回当前缓存路径下所有缓存数据的总字节数,以 byte 为单位,如果应用程序中需要在界面上显示当前缓存数据的总大小,就可以通过调用这个方法计算出来。如果后台删除挂起,则此值可能大于最大大小。
+
+
+
+#### 8.4 DiskLruCache 的 flush()
+- 代码如下所示
+    ```
+    /**
+     * Force buffered operations to the filesystem.
+     */
+    public synchronized void flush() throws IOException {
+        checkNotClosed();
+        trimToSize();
+        journalWriter.flush();
+    }
+    ```
+- 这个方法用于将内存中的操作记录同步到日志文件(也就是 journal 文件)当中。这个方法非常重要,因为 DiskLruCache 能够正常工作是依赖于 journal 文件中的内容。其实并不是每次写入缓存都要调用一次 flush() 方法,频繁地调用并不会带来任何好处,只会额外增加同步 journal 文件的时间。比较标准的做法是在 Activity 的 onPause() 方法中调用一次 flush() 方法就可以了。
+- 至此,我们的源码分析就结束了。可以看到 DiskLruCache ,利用一个 journal 文件,保证了 cache 实体的可用性(只有CLEAN的可用),且获取文件的长度的时候可以通过在该文件的记录中读取。
+- 利用 FaultHidingOutputStream 对 FileOutPutStream 在写入文件过程中是否发生错误进行捕获,而不是让用户手动去调用出错后的处理方法。其内部的很多细节都很值得我们推敲和学习。不过也可以看到,存取的操作不是特别的方便易用,需要我们自己去操作文件流。
+
+
+
+
+
+
+
+
+

+ 116 - 53
VideoSqlLite/src/main/java/com/yc/videosqllite/disk/DiskLruCache.java

@@ -101,12 +101,13 @@ public final class DiskLruCache implements Closeable {
                     new DiskLruCacheThreadFactory());
     private final Callable<Void> cleanupCallable = new Callable<Void>() {
         public Void call() throws Exception {
-            synchronized (com.yc.videosqllite.disk.DiskLruCache.this) {
+            synchronized (DiskLruCache.this) {
                 if (journalWriter == null) {
                     return null; // Closed.
                 }
                 trimToSize();
                 if (journalRebuildRequired()) {
+                    //创建一个新的日志,删除多余的信息
                     rebuildJournal();
                     redundantOpCount = 0;
                 }
@@ -138,7 +139,7 @@ public final class DiskLruCache implements Closeable {
      * @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 com.yc.videosqllite.disk.DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
+    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
             throws IOException {
         if (maxSize <= 0) {
             throw new IllegalArgumentException("maxSize <= 0");
@@ -147,11 +148,11 @@ public final class DiskLruCache implements Closeable {
             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 {
@@ -159,11 +160,13 @@ public final class DiskLruCache implements Closeable {
             }
         }
 
-        // Prefer to pick up where we left off.
-        com.yc.videosqllite.disk.DiskLruCache cache = new com.yc.videosqllite.disk.DiskLruCache(directory, appVersion, valueCount, maxSize);
+        // 首先尝试读取日志文件
+        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
         if (cache.journalFile.exists()) {
             try {
+                //日志文件的读取过程
                 cache.readJournal();
+                //用于统计缓存文件的总体大小,并删除脏文件
                 cache.processJournal();
                 return cache;
             } catch (IOException journalIsCorrupt) {
@@ -177,13 +180,20 @@ public final class DiskLruCache implements Closeable {
             }
         }
 
-        // Create a new empty cache.
+        // 此时日志文件不存在或读取出错,新建一个DiskLruCache实例
         directory.mkdirs();
-        cache = new com.yc.videosqllite.disk.DiskLruCache(directory, appVersion, valueCount, maxSize);
+        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
+        //创建一个新的日志,删除多余的信息
         cache.rebuildJournal();
         return cache;
     }
 
+    /**
+     * 日志文件的读取过程
+     * readJournal()方法其实就是通过readJournalLine(reader.readLine())方法读取日志文件中的每一行,
+     * 最终会读取到lruEntries中,lruEntries是DiskLruCache在内存中的表现形式。
+     * @throws IOException                      异常
+     */
     private void readJournal() throws IOException {
         StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), DiskUtils.US_ASCII);
         try {
@@ -204,6 +214,7 @@ public final class DiskLruCache implements Closeable {
             int lineCount = 0;
             while (true) {
                 try {
+                    //该方法用于读取每一行日志
                     readJournalLine(reader.readLine());
                     lineCount++;
                 } catch (EOFException endOfJournal) {
@@ -214,6 +225,7 @@ public final class DiskLruCache implements Closeable {
 
             // If we ended on a truncated line, rebuild the journal before appending to it.
             if (reader.hasUnterminatedLine()) {
+                //创建一个新的日志,删除多余的信息
                 rebuildJournal();
             } else {
                 journalWriter = new BufferedWriter(new OutputStreamWriter(
@@ -224,6 +236,20 @@ public final class DiskLruCache implements Closeable {
         }
     }
 
+    /**
+     * 该方法用于读取每一行日志。日志文件的每一行都是DIRTY、CLEAN、READ或REMOVE四种行为之一,那么该方法就需要对这4中情况分别处理。
+     * 大概思路:
+     * 1.首先取出该行记录的key,然后根据该记录是否为REMOVE进行不同的操作,如果是REMOVE,则将该key的缓存从lruEntries中移除。
+     * 2.如果不是REMOVE,说明该key存在一个对应的缓存实体Entry,则先新建一个Entry并添加到lruEntries中。
+     * 3.之后再判断日志的类型,如果日志是CLEAN,代表该文件已经保存完毕了,将currentEditor设置为null;
+     * 4.如果日志是DIRTY,代表文件没有保存完毕,为其currentEditor新建一个Editor。
+     *
+     * 为什么要这么做呢?
+     * 保存一个文件时会先写入DIRTY日志,保存成功后再写入CLEAN日志,一般来说这两条日志会成对出现。
+     * 这里的currentEditor相当于一个标志位,如果为空,表示文件完整,如果不为空,表示该文件是临时文件。
+     * @param line                              每行内容
+     * @throws IOException                      io流异常
+     */
     private void readJournalLine(String line) throws IOException {
         int firstSpace = line.indexOf(' ');
         if (firstSpace == -1) {
@@ -235,6 +261,7 @@ public final class DiskLruCache implements Closeable {
         final String key;
         if (secondSpace == -1) {
             key = line.substring(keyBegin);
+            // 如果是REMOVE,则将该key代表的缓存从lruEntries中移除
             if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) {
                 lruEntries.remove(key);
                 return;
@@ -243,12 +270,13 @@ public final class DiskLruCache implements Closeable {
             key = line.substring(keyBegin, secondSpace);
         }
 
+        //取出当前key对应的缓存Entry
         Entry entry = lruEntries.get(key);
         if (entry == null) {
             entry = new Entry(key);
             lruEntries.put(key, entry);
         }
-
+        // 如果是CLEAN、DIRTY或READ
         if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) {
             String[] parts = line.substring(secondSpace + 1).split(" ");
             entry.readable = true;
@@ -264,8 +292,8 @@ public final class DiskLruCache implements Closeable {
     }
 
     /**
-     * Computes the initial size and collects garbage as a part of opening the
-     * cache. Dirty entries are assumed to be inconsistent and will be deleted.
+     * 计算初始大小并收集垃圾,作为打开缓存。假设脏条目是不一致的,将被删除。
+     *
      */
     private void processJournal() throws IOException {
         deleteIfExists(journalFileTmp);
@@ -287,8 +315,7 @@ public final class DiskLruCache implements Closeable {
     }
 
     /**
-     * Creates a new journal that omits redundant information. This replaces the
-     * current journal if it exists.
+     * 创建一个新的日志,删除多余的信息。如果当前日志存在,则替换当前日志。
      */
     private synchronized void rebuildJournal() throws IOException {
         if (journalWriter != null) {
@@ -345,12 +372,12 @@ public final class DiskLruCache implements Closeable {
     }
 
     /**
-     * Returns a snapshot of the entry named {@code key}, or null if it doesn't
-     * exist is not currently readable. If a value is returned, it is moved to
-     * the head of the LRU queue.
+     * 读取缓存
+     * 返回名为{@code key}的条目的快照,如果不存在则返回null,这是当前不可读的。如果一个值被返回,它将被移动到LRU队列的头。
      */
     public synchronized Value get(String key) throws IOException {
         checkNotClosed();
+        //取出当前key对应的缓存Entry
         Entry entry = lruEntries.get(key);
         if (entry == null) {
             return null;
@@ -363,6 +390,7 @@ public final class DiskLruCache implements Closeable {
         for (File file : entry.cleanFiles) {
             // A file must have been deleted manually!
             if (!file.exists()) {
+                // 除非用户手动删了文件, 否则不会执行到这里...
                 return null;
             }
         }
@@ -380,31 +408,50 @@ public final class DiskLruCache implements Closeable {
     }
 
     /**
-     * Returns an editor for the entry named {@code key}, or null if another
-     * edit is in progress.
+     * 返回名为{@code key}的条目的编辑器,如果正在进行另一个编辑,则返回null。
+     * 这个key将会成为缓存文件的文件名
      */
     public Editor edit(String key) throws IOException {
         return edit(key, ANY_SEQUENCE_NUMBER);
     }
 
+    /**
+     * 如何写缓存?
+     * 写缓存的时候需要先通过edit(String key)方法新建一个Editor,然后将数据写入Editor的输出流中,最后成功则调用
+     * Editor.commit(),失败则调用Editor.abort()。
+     *
+     * 该方法大概思路
+     * 1.取出当前key对应的缓存Entry,如果Entry不存在则新建并添加到lruEntries中,
+     * 2.如果存在且entry.currentEditor不为空,表示Entry正在进行缓存编辑。
+     * 3.随后新建一个Editor,并在日志文件中输出一行DIRTY日志表示开始编辑缓存文件。
+     * @param key                                           key
+     * @param expectedSequenceNumber                        number
+     * @return
+     * @throws IOException                                  异常
+     */
     private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
         checkNotClosed();
+        //取出当前key对应的缓存Entry
         Entry entry = lruEntries.get(key);
         if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null
                 || entry.sequenceNumber != expectedSequenceNumber)) {
             return null; // Value is stale.
         }
         if (entry == null) {
+            //如果Entry不存在则新建并添加到lruEntries中
             entry = new Entry(key);
             lruEntries.put(key, entry);
         } else if (entry.currentEditor != null) {
+            //如果存在且entry.currentEditor不为空,表示Entry正在进行缓存编辑
             return null; // Another edit is in progress.
         }
 
+        //随后新建一个Editor,并在日志文件中输出一行DIRTY日志表示开始编辑缓存文件。
         Editor editor = new Editor(entry);
         entry.currentEditor = editor;
 
         // Flush the journal before creating files to prevent file leaks.
+        // 为了防止文件泄露,在创建文件前,将日志立即写入journal中
         journalWriter.append(DIRTY);
         journalWriter.append(' ');
         journalWriter.append(key);
@@ -414,23 +461,21 @@ public final class DiskLruCache implements Closeable {
     }
 
     /**
-     * Returns the directory where this cache stores its data.
+     * 返回该缓存存储其数据的目录。
      */
     public File getDirectory() {
         return directory;
     }
 
     /**
-     * Returns the maximum number of bytes that this cache should use to store
-     * its data.
+     * 返回此缓存用于存储其数据的最大字节数。
      */
     public synchronized long getMaxSize() {
         return maxSize;
     }
 
     /**
-     * Changes the maximum number of bytes the cache can store and queues a job
-     * to trim the existing store, if necessary.
+     * 更改缓存可以存储的最大字节数,并在必要时对作业进行排队,以精简现有的存储。
      */
     public synchronized void setMaxSize(long maxSize) {
         this.maxSize = maxSize;
@@ -438,21 +483,28 @@ public final class DiskLruCache implements Closeable {
     }
 
     /**
-     * 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.
+     * 返回当前用于在此缓存中存储值的字节数。如果后台删除挂起,这个值可能大于最大大小。
+     * 这个方法会返回当前缓存路径下所有缓存数据的总字节数,以byte为单位,
+     * 如果应用程序中需要在界面上显示当前缓存数据的总大小,就可以通过调用这个方法计算出来。
      */
     public synchronized long size() {
         return size;
     }
 
+    /**
+     * 该方法首先根据文件写入是否成功来重命名或者删除tmp文件,随后向journal写入日志,最后判断是否需要清理磁盘空间。
+     * @param editor                                        editor对象
+     * @param success                                       是否成功
+     * @throws IOException
+     */
     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.
+        // 如果当前编辑是第一次创建Entry,那么每个索引上都应该有值
+        // valueCount表示一个Entry中的value数量
         if (success && !entry.readable) {
             for (int i = 0; i < valueCount; i++) {
                 if (!editor.written[i]) {
@@ -465,7 +517,8 @@ public final class DiskLruCache implements Closeable {
                 }
             }
         }
-
+        // 遍历Entry上的每个文件
+        // 如果编辑成功就将临时文件改名, 如果失败则删除临时文件
         for (int i = 0; i < valueCount; i++) {
             File dirty = entry.getDirtyFile(i);
             if (success) {
@@ -493,6 +546,8 @@ public final class DiskLruCache implements Closeable {
             journalWriter.append('\n');
 
             if (success) {
+                // 给Entry的sequenceNumber赋值, 用于标记snapshot是否过期
+                // 如果Entry和snapshot的sequenceNumber不同, 则表示数据已经过期了
                 entry.sequenceNumber = nextSequenceNumber++;
             }
         } else {
@@ -503,15 +558,14 @@ public final class DiskLruCache implements Closeable {
             journalWriter.append('\n');
         }
         journalWriter.flush();
-
+        // 判断是否需要清理磁盘空间
         if (size > maxSize || journalRebuildRequired()) {
             executorService.submit(cleanupCallable);
         }
     }
 
     /**
-     * We only rebuild the journal when it will halve the size of the journal
-     * and eliminate at least 2000 ops.
+     * 只有当日志大小减半并消除至少2000个ops时,我们才重新生成日志。
      */
     private boolean journalRebuildRequired() {
         final int redundantOpCompactThreshold = 2000;
@@ -520,13 +574,12 @@ public final class DiskLruCache implements Closeable {
     }
 
     /**
-     * Drops the entry for {@code key} if it exists and can be removed. Entries
-     * actively being edited cannot be removed.
-     *
+     * 删除{@code key}的条目,如果它存在并且可以被删除。正在编辑的条目不能删除。
      * @return true if an entry was removed.
      */
     public synchronized boolean remove(String key) throws IOException {
         checkNotClosed();
+        //取出当前key对应的缓存Entry
         Entry entry = lruEntries.get(key);
         if (entry == null || entry.currentEditor != null) {
             return false;
@@ -570,7 +623,11 @@ public final class DiskLruCache implements Closeable {
     }
 
     /**
-     * Force buffered operations to the filesystem.
+     * 强制对文件系统进行缓冲操作。
+     * 这个方法用于将内存中的操作记录同步到日志文件(也就是journal文件)当中。
+     * 这个方法非常重要,因为DiskLruCache能够正常工作的前提就是要依赖于journal文件中的内容。
+     * 并不是每次写读缓存都要调用一次flush()方法,频繁地调用并不会带来任何好处,只会额外增加同步journal文件的时间。
+     * 比较标准的做法就是在Activity的onPause()方法中去调用一次flush()方法就可以了。
      */
     public synchronized void flush() throws IOException {
         checkNotClosed();
@@ -579,7 +636,9 @@ public final class DiskLruCache implements Closeable {
     }
 
     /**
-     * Closes this cache. Stored values will remain on the filesystem.
+     * 这个方法用于将DiskLruCache关闭掉,是和open()方法对应的一个方法。
+     * 关闭掉了之后就不能再调用DiskLruCache中任何操作缓存数据的方法
+     * 通常只应该在Activity的onDestroy()方法中去调用close()方法。
      */
     public synchronized void close() throws IOException {
         if (journalWriter == null) {
@@ -603,9 +662,7 @@ public final class DiskLruCache implements Closeable {
     }
 
     /**
-     * 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();
@@ -613,6 +670,7 @@ public final class DiskLruCache implements Closeable {
     }
 
     private static String inputStreamToString(InputStream in) throws IOException {
+        //写数据,一次读取一个字节
         return DiskUtils.readFully(new InputStreamReader(in, DiskUtils.UTF_8));
     }
 
@@ -638,7 +696,7 @@ public final class DiskLruCache implements Closeable {
          * is in progress.
          */
         public Editor edit() throws IOException {
-            return com.yc.videosqllite.disk.DiskLruCache.this.edit(key, sequenceNumber);
+            return DiskLruCache.this.edit(key, sequenceNumber);
         }
 
         public File getFile(int index) {
@@ -662,7 +720,7 @@ public final class DiskLruCache implements Closeable {
     }
 
     /**
-     * Edits the values for an entry.
+     * 编辑条目的值。
      */
     public final class Editor {
         private final Entry entry;
@@ -675,11 +733,10 @@ public final class DiskLruCache implements Closeable {
         }
 
         /**
-         * Returns an unbuffered input stream to read the last committed value,
-         * or null if no value has been committed.
+         * 返回一个未缓冲的输入流来读取最后提交的值,如果没有提交值,则返回null。
          */
         private InputStream newInputStream(int index) throws IOException {
-            synchronized (com.yc.videosqllite.disk.DiskLruCache.this) {
+            synchronized (DiskLruCache.this) {
                 if (entry.currentEditor != this) {
                     throw new IllegalStateException();
                 }
@@ -695,16 +752,17 @@ public final class DiskLruCache implements Closeable {
         }
 
         /**
-         * Returns the last committed value as a string, or null if no value
-         * has been committed.
+         * 以字符串的形式返回最后一个提交的值,如果没有提交值,则返回null。
          */
         public String getString(int index) throws IOException {
+            //创建io流对象
             InputStream in = newInputStream(index);
+            //如果流对象不为空,则写入数据
             return in != null ? inputStreamToString(in) : null;
         }
 
         public File getFile(int index) throws IOException {
-            synchronized (com.yc.videosqllite.disk.DiskLruCache.this) {
+            synchronized (DiskLruCache.this) {
                 if (entry.currentEditor != this) {
                     throw new IllegalStateException();
                 }
@@ -725,17 +783,21 @@ public final class DiskLruCache implements Closeable {
         public void set(int index, String value) throws IOException {
             Writer writer = null;
             try {
-                OutputStream os = new FileOutputStream(getFile(index));
+                File file = getFile(index);
+                //创建输出流对象
+                OutputStream os = new FileOutputStream(file);
                 writer = new OutputStreamWriter(os, DiskUtils.UTF_8);
+                //写数据
                 writer.write(value);
             } finally {
+                //关闭流对象
                 DiskUtils.closeQuietly(writer);
             }
         }
 
         /**
-         * Commits this edit so it is visible to readers.  This releases the
-         * edit lock so another edit may be started on the same key.
+         * 成功后调用Editor.commit()
+         * 提交此编辑,使其对读者可见。这释放了编辑锁,因此可以在同一键上启动另一个编辑。
          */
         public void commit() throws IOException {
             // The object using this Editor must catch and handle any errors
@@ -747,8 +809,8 @@ public final class DiskLruCache implements Closeable {
         }
 
         /**
-         * Aborts this edit. This releases the edit lock so another edit may be
-         * started on the same key.
+         * 失败后调用Editor.abort()方法
+         * 中止这个编辑。这释放了编辑锁,因此可以在同一键上启动另一个编辑。
          */
         public void abort() throws IOException {
             completeEdit(this, false);
@@ -856,7 +918,8 @@ public final class DiskLruCache implements Closeable {
     private static final class DiskLruCacheThreadFactory implements ThreadFactory {
         @Override
         public synchronized Thread newThread(Runnable runnable) {
-            Thread result = new Thread(runnable, "glide-disk-lru-cache-thread");
+            //设置线程优先级
+            Thread result = new Thread(runnable, "video-disk-lru-cache-thread");
             result.setPriority(Thread.MIN_PRIORITY);
             return result;
         }