201709011工作日记--Volley源码详解(二)
1.Cache接口和DiskBasedCache实现类
首先,DiskBasedCache类是Cache接口的实现类,因此我们需要先把Cache接口中的方法搞明白。
首先分析下Cache接口中的东西,首先是接口的内部类 class entry{},用途是返回缓存的数据,下面是内部类的具体实现:
1 class Entry { 2 /** 3 * 从缓存中返回的数据 4 * The data returned from cache. 5 * */ 6 public byte[] data; 7 8 /** 9 * 缓存一致性 10 * ETag for cache coherency. */ 11 public String etag; 12 13 /** 14 * 从服务端返回的响应数据 15 * Date of this response as reported by the server. */ 16 public long serverDate; 17 18 /** 19 * 请求的最后修改时间 20 * The last modified date for the requested object. */ 21 public long lastModified; 22 23 /** 24 * 记录的TTL值。IP包被路由器丢弃之前允许通过的最大网段数量 25 * 作用是限制IP数据包在计算机网络中的存在的时间,IP数据包在计算机网络中可以转发的最大跳数 26 * TTL for this record. */ 27 public long ttl; 28 29 /** Soft TTL for this record. */ 30 public long softTtl; 31 32 /** Immutable response headers as received from server; must be non-null. */ 33 public Map<String, String> responseHeaders = Collections.emptyMap(); 34 35 /**过期,返回true 36 * True if the entry is expired. */ 37 public boolean isExpired() { 38 return this.ttl < System.currentTimeMillis(); 39 } 40 41 /** 42 * 如果需要从数据源跟新数据则返回TRUE 43 * True if a refresh is needed from the original data source. */ 44 public boolean refreshNeeded() { 45 return this.softTtl < System.currentTimeMillis(); 46 } 47 }
上面的是在接口中定义了一个内部类Entry,这种情况之前没见过,还不知道有什么具体作用,下面是接口内部类举例:
1 public class QuestionMain implements ITest{ 2 public static void main(String[] args) { 3 ITest.TestInternal.test(new QuestionMain()); 4 } 5 @Override 6 public void print() { 7 System.out.println("真正实现方法"); 8 } 9 } 10 interface ITest { 11 void print(); 12 public static class TestInternal { 13 public static void test(ITest test) { 14 test.print(); 15 } 16 } 17 }
链接中有一点点相关讨论,但是仍然不知道有啥作:http://www.th7.cn/Program/java/201412/329066.shtmlhttp://bbs.csdn.net/topics/390735550
我的一点理解是:由于接口中的所有方法都需要重写,但是有些方法不想重写的话可以放到接口内部类中去。
接口 Cache 里面封装了一个静态内部类 Entry(登记),这个内部类非常重要,看了 HTTP 协议的同学们会发现,Entry 里面定义的这些成员变量跟 headers(消息报头)里面关于缓存的标签是一样的,这也是前面强调要看协议的原因。其中还维护了一个map 用来保存消息报头中的 key / value,data 来保存 entity 消息实体。除此之外就是一些集合操作了。接口的内部类 Entry,他真的太重要了,我们看连个判断过期和需要刷新的方法分别是,两个成员变量跟当前时间的对比。而 data 是二进制数组,我们都知道在 HTTP 中 start line 和 headers 是明文存储的,而 Entity 是没有规定的,一般我们都用二进制流传输,可以减少传输流量,并且安全,data 这里就是用来保存 Entity 的。
这个接口的作用就是对外提供一个查询、存入、删除缓存条目的作用。
Cache 的默认实现是 DiskBasedCache,实现类的目的是:类的目的是 将文件直接缓存到指定目录中的硬盘上的缓存实现。
参数与初始化函数:
1 /** 2 * 缓存条目 3 * Map of the Key, CacheHeader pairs */ 4 private final Map<String, CacheHeader> mEntries = 5 new LinkedHashMap<String, CacheHeader>(16, .75f, true); 6 7 /** 8 * 目前所使用到的缓存字节数组 9 * Total amount of space currently used by the cache in bytes. */ 10 private long mTotalSize = 0; 11 12 /** 13 * 缓存的根目录 14 * The root directory to use for the cache. */ 15 private final File mRootDirectory; 16 17 /** 18 * 缓存的最大字节数组 19 * The maximum size of the cache in bytes. */ 20 private final int mMaxCacheSizeInBytes; 21 22 /** 23 * 默认的最大硬盘缓存 24 * Default maximum disk usage in bytes. */ 25 private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024; 26 27 /** High water mark percentage for the cache */ 28 private static final float HYSTERESIS_FACTOR = 0.9f; 29 30 /** Magic number for current version of cache file format. */ 31 private static final int CACHE_MAGIC = 0x20150306; 32 33 /**缓存根目录,最大缓存字节数 34 * Constructs an instance of the DiskBasedCache at the specified directory. 35 * @param rootDirectory The root directory of the cache. 36 * @param maxCacheSizeInBytes The maximum size of the cache in bytes. 37 */ 38 public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) { 39 mRootDirectory = rootDirectory; 40 mMaxCacheSizeInBytes = maxCacheSizeInBytes; 41 } 42 43 /** 44 * Constructs an instance of the DiskBasedCache at the specified directory using 45 * the default maximum cache size of 5MB. 46 * @param rootDirectory The root directory of the cache. 47 */ 48 public DiskBasedCache(File rootDirectory) { 49 this(rootDirectory, DEFAULT_DISK_USAGE_BYTES); 50 }
针对缓存数据的处理:
1 /** 2 * 清空缓存,并将缓存目录下面的所有缓存文件删除 3 * Clears the cache. Deletes all cached files from disk. 4 */ 5 @Override 6 public synchronized void clear() { 7 File[] files = mRootDirectory.listFiles(); 8 if (files != null) { 9 for (File file : files) { 10 file.delete(); 11 } 12 } 13 mEntries.clear(); 14 mTotalSize = 0; 15 VolleyLog.d("Cache cleared."); 16 } 17 18 /** 19 * 利用指定----key,去获取缓存条目 20 * Returns the cache entry with the specified key if it exists, null otherwise. 21 */ 22 @Override 23 public synchronized Entry get(String key) { 24 CacheHeader entry = mEntries.get(key); 25 // if the entry does not exist, return. 26 //缓存为空则直接返回 27 if (entry == null) { 28 return null; 29 } 30 File file = getFileForKey(key); 31 try { 32 CountingInputStream cis = new CountingInputStream( 33 new BufferedInputStream(createInputStream(file)), file.length()); 34 try { 35 CacheHeader entryOnDisk = CacheHeader.readHeader(cis); 36 if (!TextUtils.equals(key, entryOnDisk.key)) { 37 // File was shared by two keys and now holds data for a different entry! 38 VolleyLog.d("%s: key=%s, found=%s", 39 file.getAbsolutePath(), key, entryOnDisk.key); 40 // Remove key whose contents on disk have been replaced. 41 removeEntry(key); 42 return null; 43 } 44 byte[] data = streamToBytes(cis, cis.bytesRemaining()); 45 return entry.toCacheEntry(data); 46 } finally { 47 // Any IOException thrown here is handled by the below catch block by design. 48 //noinspection ThrowFromFinallyBlock 49 cis.close(); 50 } 51 } catch (IOException e) { 52 VolleyLog.d("%s: %s", file.getAbsolutePath(), e.toString()); 53 remove(key); 54 return null; 55 } 56 } 57 58 /** 59 * Initializes the DiskBasedCache by scanning for all files currently in the 60 * specified root directory. Creates the root directory if necessary. 61 */ 62 @Override 63 public synchronized void initialize() { 64 if (!mRootDirectory.exists()) { 65 if (!mRootDirectory.mkdirs()) { 66 VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath()); 67 } 68 return; 69 } 70 File[] files = mRootDirectory.listFiles(); 71 if (files == null) { 72 return; 73 } 74 for (File file : files) { 75 try { 76 long entrySize = file.length(); 77 CountingInputStream cis = new CountingInputStream( 78 new BufferedInputStream(createInputStream(file)), entrySize); 79 try { 80 CacheHeader entry = CacheHeader.readHeader(cis); 81 // NOTE: When this entry was put, its size was recorded as data.length, but 82 // when the entry is initialized below, its size is recorded as file.length() 83 entry.size = entrySize; 84 putEntry(entry.key, entry); 85 } finally { 86 // Any IOException thrown here is handled by the below catch block by design. 87 //noinspection ThrowFromFinallyBlock 88 cis.close(); 89 } 90 } catch (IOException e) { 91 //noinspection ResultOfMethodCallIgnored 92 file.delete(); 93 } 94 } 95 } 96 97 /** 98 * Invalidates an entry in the cache. 99 * @param key Cache key 100 * @param fullExpire True to fully expire the entry, false to soft expire 101 */ 102 @Override 103 public synchronized void invalidate(String key, boolean fullExpire) { 104 Entry entry = get(key); 105 if (entry != null) { 106 entry.softTtl = 0; 107 if (fullExpire) { 108 entry.ttl = 0; 109 } 110 put(key, entry); 111 } 112 } 113 114 /** 115 * Puts the entry with the specified key into the cache. 116 */ 117 @Override 118 public synchronized void put(String key, Entry entry) { 119 pruneIfNeeded(entry.data.length); 120 File file = getFileForKey(key); 121 try { 122 BufferedOutputStream fos = new BufferedOutputStream(createOutputStream(file)); 123 CacheHeader e = new CacheHeader(key, entry); 124 boolean success = e.writeHeader(fos); 125 if (!success) { 126 fos.close(); 127 VolleyLog.d("Failed to write header for %s", file.getAbsolutePath()); 128 throw new IOException(); 129 } 130 fos.write(entry.data); 131 fos.close(); 132 putEntry(key, e); 133 return; 134 } catch (IOException e) { 135 } 136 boolean deleted = file.delete(); 137 if (!deleted) { 138 VolleyLog.d("Could not clean up file %s", file.getAbsolutePath()); 139 } 140 } 141 142 /** 143 * Removes the specified key from the cache if it exists. 144 */ 145 @Override 146 public synchronized void remove(String key) { 147 boolean deleted = getFileForKey(key).delete(); 148 removeEntry(key); 149 if (!deleted) { 150 VolleyLog.d("Could not delete cache entry for key=%s, filename=%s", 151 key, getFilenameForKey(key)); 152 } 153 } 154 155 /**为指定的缓存键创建伪唯一文件名。利用key,创建key相关的文件名 156 * Creates a pseudo-unique filename for the specified cache key. 157 * @param key The key to generate a file name for. 158 * @return A pseudo-unique filename. 159 */ 160 private String getFilenameForKey(String key) { 161 int firstHalfLength = key.length() / 2; 162 String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode()); 163 localFilename += String.valueOf(key.substring(firstHalfLength).hashCode()); 164 return localFilename; 165 } 166 167 /** 168 * 根据给定的缓存键,创建该文件,并返回该键对应的文件对象 169 * Returns a file object for the given cache key. 170 */ 171 public File getFileForKey(String key) { 172 return new File(mRootDirectory, getFilenameForKey(key)); 173 } 174 175 /** 176 * 修剪缓存以适应指定的字节数 177 * Prunes the cache to fit the amount of bytes specified. 178 * @param neededSpace The amount of bytes we are trying to fit into the cache. 179 */ 180 private void pruneIfNeeded(int neededSpace) { 181 //如果已经用的和需要用的大小,小于最大缓存字节数,则直接返回 182 if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) { 183 return; 184 } 185 if (VolleyLog.DEBUG) { 186 VolleyLog.v("Pruning old cache entries."); 187 } 188 189 long before = mTotalSize; 190 int prunedFiles = 0; 191 long startTime = SystemClock.elapsedRealtime(); 192 193 Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator(); 194 while (iterator.hasNext()) { 195 Map.Entry<String, CacheHeader> entry = iterator.next(); 196 CacheHeader e = entry.getValue(); 197 boolean deleted = getFileForKey(e.key).delete(); 198 if (deleted) { 199 mTotalSize -= e.size; 200 } else { 201 VolleyLog.d("Could not delete cache entry for key=%s, filename=%s", 202 e.key, getFilenameForKey(e.key)); 203 } 204 iterator.remove(); 205 prunedFiles++; 206 207 if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) { 208 break; 209 } 210 } 211 212 if (VolleyLog.DEBUG) { 213 VolleyLog.v("pruned %d files, %d bytes, %d ms", 214 prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime); 215 } 216 } 217 218 /** 219 * 利用特定的key去添加缓存条目 220 * Puts the entry with the specified key into the cache. 221 * @param key The key to identify the entry by. 222 * @param entry The entry to cache. 223 */ 224 private void putEntry(String key, CacheHeader entry) { 225 //存入条目之前需要需要更改缓存容量的大小 226 if (!mEntries.containsKey(key)) { 227 mTotalSize += entry.size; 228 } else { 229 CacheHeader oldEntry = mEntries.get(key); 230 mTotalSize += (entry.size - oldEntry.size); 231 } 232 mEntries.put(key, entry); //存入缓存条目 233 } 234 235 /** 236 * 删除缓存并且更改缓存大小 237 * Removes the entry identified by 'key' from the cache. 238 */ 239 private void removeEntry(String key) { 240 CacheHeader removed = mEntries.remove(key); 241 if (removed != null) { 242 mTotalSize -= removed.size; 243 } 244 }
实现类里面还有一个静态内部类CacheHeader,作用是将缓存一个条目。其中pruneIfNeeded()方法是在 put 方法的第一行执行的,做的就是这件事,Volley 并没有使用 LRU。而是使用的 FIFO。DiskBasedCache 剩下的就是一些文件操作了,就不挨着看了。
这里顺便提一下内部接口,具体例子在Response.java这个类中,存在着两个接口:
1 /** Callback interface for delivering parsed responses. 传递相应信息时,回调此接口*/ 2 public interface Listener<T> { 3 /** Called when a response is received. */ 4 void onResponse(T response); 5 } 6 7 /** Callback interface for delivering error responses. 传递错误响应信息时,回调此接口*/ 8 public interface ErrorListener { 9 /** 10 * Callback method that an error has been occurred with the 11 * provided error code and optional user-readable message. 12 */ 13 void onErrorResponse(VolleyError error); 14 }
看下内部接口的使用场景:
1 package com.dao.util; 2 3 public class Util { 4 public interface Worker { 5 void work(); 6 } 7 } 8 9 package com.dao.util; 10 11 import com.dao.util.Util.Worker; 12 13 public class Demo implements Worker { 14 public void work() { 15 System.out.println("Is working"); 16 } 17 }
在这个util类里面封装了会改变的Worker,也就是说,util类里面有自己的算法:一部分是固定,另外一部分是可变的,而这个可变的部分就变成了一个接口,接口是特殊的抽象类~其实的意思大概的这个worker可能在不同的工作环境有不同的工作方式,所以在类里面放一个接口不是什么新的设计,而是一种思想方式,让代码扩展性更高。
2. 创建一个 StringRequest 请求
这个是我们发起的请求,我们可以注意上边的方法有四个参数的方法,(int method, String url, Listener listener,ErrorListener errorListener) ,我们可以主要关注一个后边的两个回调,一个是成功的回调,一个是失败的回调,而且我们可以看一下他的源码。如下为利用构造函数创建的一个StringRequest请求代码:
1 StringRequest stringRequest = new StringRequest("http://www.baidu.com", 2 new Response.Listener<String>() { 3 @Override 4 public void onResponse(String response) { 5 6 } 7 }, new Response.ErrorListener() { 8 @Override 9 public void onErrorResponse(VolleyError error) { 10 11 } 12 });
先看下StringRequest继承了Request类,需要实现其中的两个抽象方法;在构造函数中需要两个回调函数。他们之间的关系如下:
StringRequest类:
1 /** 2 * 一个用于在给定URL中以String形式检索响应正文的请求。 3 * A canned request for retrieving the response body at a given URL as a String. 4 */ 5 public class StringRequest extends Request<String> { 6 private final Listener<String> mListener; 7 8 /** 9 * 利用给定的方式创造一个请求消息 10 * Creates a new request with the given method. 11 * 12 * @param method the request {@link Method} to use 请求方式 13 * @param url URL to fetch the string at 用于获取字符串的URL 14 * @param listener Listener to receive the String response 侦听器接收String响应 15 * @param errorListener Error listener, or null to ignore errors 错误侦听器,或null忽略错误 16 */ 17 public StringRequest(int method, String url, Listener<String> listener, 18 ErrorListener errorListener) { 19 super(method, url, errorListener); 20 mListener = listener; 21 } 22 23 /** 24 * 使用指定的GET方式创建新的请求消息 25 * Creates a new GET request. 26 * 27 * @param url URL to fetch the string at 28 * @param listener Listener to receive the String response 29 * @param errorListener Error listener, or null to ignore errors 30 */ 31 public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) { 32 this(Method.GET, url, listener, errorListener); 33 } 34 35 /** 36 * 将解析到的响应传送给他们的监听器,给定的响应response保证为非空值; 37 * 无法解析的响应不会传递。 38 * @param response The parsed response returned by 39 */ 40 @Override 41 protected void deliverResponse(String response) { 42 if (mListener != null) { 43 mListener.onResponse(response); 44 } 45 } 46 /** 47 * 工作线程将会调用这个方法 48 * 方法的目的是:按照HTTP头消息中的编码字符集,去重新构建这个消息的返回信息 49 * 50 * new String(byte[],"UTF-8")是新建了一个UTF-8编码的字符串 51 * 52 * @param response Response from the network 53 * @return 返回成功响应的消息 54 */ 55 @Override 56 protected Response<String> parseNetworkResponse(NetworkResponse response) { 57 String parsed; 58 try { 59 //解析头部信息的编码格式,然后将返回信息按照编码格式进行转换 60 parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers)); 61 } catch (UnsupportedEncodingException e) {//如果指定字符集不受支持 62 parsed = new String(response.data); 63 } 64 return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response)); 65 } 66 }
Response类的内部接口:
1 public class Response<T> { 2 3 /** Callback interface for delivering parsed responses. 传递相应信息时,回调此接口*/ 4 public interface Listener<T> { 5 /** Called when a response is received. */ 6 void onResponse(T response); 7 } 8 9 /** Callback interface for delivering error responses. 传递错误响应信息时,回调此接口*/ 10 public interface ErrorListener { 11 /** 12 * Callback method that an error has been occurred with the 13 * provided error code and optional user-readable message. 14 */ 15 void onErrorResponse(VolleyError error); 16 }