一.概述
今天带大家从源码的角度来分析一下volley的缓存机制,目的就是为了家声理解。
我们知道,在创建请求队列的时候会使用如下的代码
RequestQueue queue = Volley.newRequestQueue(this);
方法定义如下
public static RequestQueue newRequestQueue(Context context) {
return newRequestQueue(context, null);
}
我们继续往下看
public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
String userAgent = "volley/0";
try {
String packageName = context.getPackageName();
PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
userAgent = packageName + "/" + info.versionCode;
} catch (NameNotFoundException e) {
}
if (stack == null) {
if (Build.VERSION.SDK_INT >= 9) {
stack = new HurlStack();
} else {
//如果版本小于9,添加userAgent,因为大于9以后,会默认自带userAgent
stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
}
}
Network network = new BasicNetwork(stack);
RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
queue.start();
return queue;
}
总结一下上面的步骤:
1.创建缓存目录,路径为data/data/包名/cache/volley
2.根据包名和版本号拼接userAgent
3.根据当前系统的版本号创建不同的HttpStack,HttpStack是用来执行请求的,当系统版本大于等于9的时候,创建HurlStack,底层使用的是HttpUrlConnection,小于9的时候,创建AndroidHttpClient,并传入userAgent,底层使用的是HttpClient
4.将创建好的httpstack对象封装到Network中,创建并启动请求队列
因为本篇是分析缓存的,我们重点看下面一行代码
RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
我们在创建请求队列的时候,传入了一个DiskBasedCache对象。并且把缓存路径传递给了DiskBasedCache。
DiskBasedCache相关源码分析
接下来我们分析一下DiskBasedCache的代码
首先会调用如下方法
public DiskBasedCache(File rootDirectory) {
this(rootDirectory, DEFAULT_DISK_USAGE_BYTES);
}
DEFAULT_DISK_USAGE_BYTES从字面上我们就可以知道是缓存默认使用的磁盘大小,
private static final int DEFAULT_DISK_USAGE_BYTES = 5 * 1024 * 1024;
我们可以看到是5M,我们也可以手动设定这个大小,在创建DiskBasedCache对象的时候我们调用下面的这个方法,传入自己设置的缓存大小即可,
public DiskBasedCache(File rootDirectory, int maxCacheSizeInBytes) {
mRootDirectory = rootDirectory;
mMaxCacheSizeInBytes = maxCacheSizeInBytes;
}
Volley是如何把从网络请求到的数据放到缓存中的
Volley每次会把网络请求的数据保存到本地的缓存路径中,我们看看是如何实现的,相关代码在NetworkDispatcher中
//解析结果
Response<?> response =
//解析数据
request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
//判断是否需要缓存以及缓存是否不为空
if (request.shouldCache() && response.cacheEntry != null) {
//保存缓存
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}
// 回调结果
request.markDelivered();
mDelivery.postResponse(request, response);
我们看重点代码,是下面这句
mCache.put(request.getCacheKey(), response.cacheEntry);
调用了mCache对象的put方法把响应的内容保存了起来,那么这个mCache对象是什么呢?直接告诉你,是DiskBasedCache,为什么,接下来我们验证一下
首先看mCache这个对象在哪里赋值的
public NetworkDispatcher(BlockingQueue<Request<?>> queue,
Network network, Cache cache,
ResponseDelivery delivery) {
mQueue = queue;
mNetwork = network;
mCache = cache;
mDelivery = delivery;
}
是在构造方法中,然后我们就应该看看构造方法在哪里调用了,我们在RequestQueue中发现了,
public void start() {
stop(); // 只允许一个缓存调度线程运行
//创建缓存调度线程
mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
mCacheDispatcher.start();
// 创建网络缓存线程
for (int i = 0; i < mDispatchers.length; i++) {
NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
mCache, mDelivery);
mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
}
}
那么start方法中的mCache是什么东西呢?
我们知道,在创建请求队列的时候,会调用如下的方法
public RequestQueue(Cache cache, Network network) {
this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);//默认线程池容量为4
}
最终会调用
public RequestQueue(Cache cache, Network network, int threadPoolSize,
ResponseDelivery delivery) {
mCache = cache;
mNetwork = network;
mDispatchers = new NetworkDispatcher[threadPoolSize];
mDelivery = delivery;
}
我们看到mCache就是在这里赋值的,cache就是创建请求队列时传入的DiskBasedCache对象。这就验证了我们的结论。
结论:Volley每次从网络请求的数据,会调用DiskBasedCache对象的put方法保存到本地的缓存路径,方法如下
mCache.put(request.getCacheKey(), response.cacheEntry);
键为请求的Url,
public String getCacheKey() {
return getUrl();
}
值为响应内容,包含以下内容
/** The data returned from cache. */
public byte[] data;
/** ETag for cache coherency. */
public String etag;
/** Date of this response as reported by the server. */
public long serverDate;
/** The last modified date for the requested object. */
public long lastModified;
/** TTL for this record. */
public long ttl;
/** Soft TTL for this record. */
public long softTtl;
put方法分析
经过上面的分析之后,我们就该看看DiskBasedCache的put方法了
@Override
public synchronized void put(String key, Entry entry) {
pruneIfNeeded(entry.data.length);
File file = getFileForKey(key);
try {
BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
CacheHeader e = new CacheHeader(key, entry);
boolean success = e.writeHeader(fos);
if (!success) {
fos.close();
VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
throw new IOException();
}
fos.write(entry.data);
fos.close();
putEntry(key, e);
return;
} catch (IOException e) {
}
boolean deleted = file.delete();
if (!deleted) {
VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
}
}
首先调用了pruneIfNeeded这个方法,传入要保存的数据的长度
private void pruneIfNeeded(int neededSpace) {
//缓存未达到最大容量,不做处理
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
return;
}
if (VolleyLog.DEBUG) {
VolleyLog.v("Pruning old cache entries.");
}
long before = mTotalSize;
int prunedFiles = 0;//删除的文件个数
long startTime = SystemClock.elapsedRealtime();
Iterator<Map.Entry<String, CacheHeader>> iterator = mEntries.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, CacheHeader> entry = iterator.next();
CacheHeader e = entry.getValue();
boolean deleted = getFileForKey(e.key).delete();
if (deleted) {
mTotalSize -= e.size;
} else {
VolleyLog.d("Could not delete cache entry for key=%s, filename=%s",
e.key, getFilenameForKey(e.key));
}
iterator.remove();
prunedFiles++;
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
break;
}
}
if (VolleyLog.DEBUG) {
VolleyLog.v("pruned %d files, %d bytes, %d ms",
prunedFiles, (mTotalSize - before), SystemClock.elapsedRealtime() - startTime);
}
}
首先说一下这个方法是干嘛的,是在缓存总大小不够用的时候,删除旧的缓存内容的,以保证我们新的缓存内容能够能加进来。
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes) {
return;
}
判断当前容量加上需要的容量是否超出了最大缓存容量,没有超出,则什么都不做。
然后遍历mEntries,那么mEntries是什么呢?
private final Map<String, CacheHeader> mEntries =
new LinkedHashMap<String, CacheHeader>(16, .75f, true);
是一个LinkedHashMap对象,并且指定了默认容量为16,加载因子为0.75,最后一个参数为true,代表链接哈希映像将使用访问顺序而不是插入顺序来迭代各个映像
既然要遍历这个mEntries ,肯定要有值啊,我们在哪里为其添加数据了呢?就是在保存缓存的put方法里面,put方法内部会调用一个putEntry方法,为mEntries添加数据,我们看看
private void putEntry(String key, CacheHeader entry) {
if (!mEntries.containsKey(key)) {
mTotalSize += entry.size;
} else {
CacheHeader oldEntry = mEntries.get(key);
mTotalSize += (entry.size - oldEntry.size);
}
mEntries.put(key, entry);
}
如果当前集合指定的key不存在,在当前总容量的基础上加上内容的容量,如果指定的key存在,首先获取旧的缓存对象,然后当前总容量加上新的缓存大小减去旧的缓存大小。
在遍历的过程中,根据CacheHeader对象的key属性去删除对应的文件,这个key是什么呢?
static class CacheHeader {
/** The size of the data identified by this CacheHeader. (This is not
* serialized to disk. */
public long size;
/** The key that identifies the cache entry. */
public String key;
在源码中可以看到是标识缓存内容的
if ((mTotalSize + neededSpace) < mMaxCacheSizeInBytes * HYSTERESIS_FACTOR) {
break;
}
意思就是当当前的容量+需要的容量小于磁盘最大容量的百分之九十时就可以跳出循环了。不用在继续进行删除操作了,至于为什么要这么做,还不是很明白。
缓存内容的写入
既然要写入内容,肯定要先创建对应的文件价才对,首先我们看看文件夹的创建过程,在DiskBasedCache的initialize方法中进行了缓存目录的创建,那个这个方法是在哪里调用的呢,我们知道,在请求队列启动的时候,会创建缓存调度线程并且启动它,在run方法里就进行了缓存目录的创建。
@Override
public void run() {
if (DEBUG) VolleyLog.v("start new dispatcher");
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
mCache.initialize();
接下来我们看看initialize方法的具体实现
public synchronized void initialize() {
//如果目录不存在,这个目录是data/data/包名/cache/volley文件夹
if (!mRootDirectory.exists()) {
//创建文件目录
if (!mRootDirectory.mkdirs()) {
VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
}
return;
}
//获取缓存目录中的所有文件
File[] files = mRootDirectory.listFiles();
if (files == null) {
return;
}
for (File file : files) {
BufferedInputStream fis = null;
try {
fis = new BufferedInputStream(new FileInputStream(file));
//读取文件中的头信息
CacheHeader entry = CacheHeader.readHeader(fis);
entry.size = file.length();
//添加到缓存集合中
putEntry(entry.key, entry);
} catch (IOException e) {
if (file != null) {
file.delete();
}
} finally {
try {
if (fis != null) {
fis.close();
}
} catch (IOException ignored) { }
}
}
}
创建好了目录,接下来就是写入文件,写入之前先判断缓存容量是否足够,不够就删除一些文件,那么是删除哪些文件呢?会删除最近最少使用的文件,原因分析如下:
我们的缓存是存放在LinkedHashMap中的,在构造LinkedHashMap时候,是这样的
private final Map<String, CacheHeader> mEntries =
new LinkedHashMap<String, CacheHeader>(16, .75f, true);
最后一个参数为true,代表将使用访问顺序而不是插入顺序来迭代各个映像
如果缓存没有达到指定容量,继续向里面存
File file = getFileForKey(key);
public File getFileForKey(String key) {
return new File(mRootDirectory, getFilenameForKey(key));
}
//根据url创建缓存文件名,这里的实现方式是把url长度分为两部分,分别陈胜hasCode,最后的文件名就是两部分hasCode的拼接
private String getFilenameForKey(String key) {
int firstHalfLength = key.length() / 2;
String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
return localFilename;
}
接下来是写入内容到文件
@Override
public synchronized void put(String key, Entry entry) {
pruneIfNeeded(entry.data.length);
//分居url获取创建对应文件
File file = getFileForKey(key);
try {
BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
CacheHeader e = new CacheHeader(key, entry);
//将内容写入到文件中
boolean success = e.writeHeader(fos);
if (!success) {
fos.close();
VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
throw new IOException();
}
//将data写入到文件
fos.write(entry.data);
fos.close();
//将内容保存到集合
putEntry(key, e);
return;
} catch (IOException e) {
}
boolean deleted = file.delete();
if (!deleted) {
VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
}
}
NetworkDispatcher相关源码
启动请求队列以后,同时会启动四个网络调度线程,执行它的run方法
@Override
public void run() {
Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
while (true) {
long startTimeMs = SystemClock.elapsedRealtime();
Request<?> request;
try {
//从请求队列中取出一个请求
request = mQueue.take();
} catch (InterruptedException e) {
if (mQuit) {
return;
}
continue;
}
try {
request.addMarker("network-queue-take");
// 如果请求被取消了,我们不执行网络请求
if (request.isCanceled()) {
request.finish("network-discard-cancelled");
continue;
}
addTrafficStatsTag(request);
// 执行网络请求,具体实现在BasicNetWork中.
NetworkResponse networkResponse = mNetwork.performRequest(request);
request.addMarker("network-http-complete");
// 如果服务器返回304并且我们已经投递了一个结果,不要投递两次相同的结果
if (networkResponse.notModified && request.hasHadResponseDelivered()) {
request.finish("not-modified");
continue;
}
//在工作线程解析结果
Response<?> response = request.parseNetworkResponse(networkResponse);
request.addMarker("network-parse-complete");
// 添加到缓存中
if (request.shouldCache() && response.cacheEntry != null) {
mCache.put(request.getCacheKey(), response.cacheEntry);
request.addMarker("network-cache-written");
}
request.markDelivered();
//投递结果
mDelivery.postResponse(request, response);
} catch (VolleyError volleyError) {
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
parseAndDeliverNetworkError(request, volleyError);
} catch (Exception e) {
VolleyLog.e(e, "Unhandled exception %s", e.toString());
VolleyError volleyError = new VolleyError(e);
volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
mDelivery.postError(request, volleyError);
}
}
}
上面再执行完请求之后调用了mDelivery对象的postResponse方法将结果投递。那么这个mDelivery是什么呢?是一个ExecutorDelivery对象,何时初始化的呢?我们创建请求队列的时候就进行了初始化。
public RequestQueue(Cache cache, Network network, int threadPoolSize) {
this(cache, network, threadPoolSize,
new ExecutorDelivery(new Handler(Looper.getMainLooper())));
}
并且这个ExecutorDelivery持有主线程的Handler对象,接下来我们看看结果的投递
public ExecutorDelivery(final Handler handler) {
// Make an Executor that just wraps the handler.
mResponsePoster = new Executor() {
@Override
public void execute(Runnable command) {
handler.post(command);
}
};
}
@Override
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
request.markDelivered();
request.addMarker("post-response");
mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
}
调用了execute方法将一个runnable对象传递到了主线程,因为我们获取的handler是主线程的handler.接下来我们看看这个ResponseDeliveryRunnable,当执行handler.post(Runnable)时,就会执行Runnable对象的run方法,这是一种命令模式的体现。
我们看看ResponseDeliveryRunnable的run方法
public void run() {
// If this request has canceled, finish it and don't deliver.
if (mRequest.isCanceled()) {
mRequest.finish("canceled-at-delivery");
return;
}
// Deliver a normal response or error, depending.
if (mResponse.isSuccess()) {
//将结果投递,我们需要重写此方法
mRequest.deliverResponse(mResponse.result);
} else {
mRequest.deliverError(mResponse.error);
}
if (mResponse.intermediate) {
mRequest.addMarker("intermediate-response");
} else {
mRequest.finish("done");
}
if (mRunnable != null) {
mRunnable.run();
}
}