Volley的使用(三)
2016-03-20 12:01
465 查看
一.概述
今天带大家从源码的角度来分析一下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(); } }
相关文章推荐
- Category(类别)和扩展(Extension)的区别
- 两颗线段树
- python常用数学函数
- 多线程“基础篇”04之 synchronized关键字
- VIM学习笔记 缩进 (Indent)
- Spark 1.X DatandaLone伪分布环境搭建
- Android 通用标题栏简单封装实现
- 在centos 7上装载php7.0.2、mysql 5.7.11 和 nginx-1.9.12
- 上百个Android开源项目分享
- Running Android Lint has encountered a... Failed. java.lang.NullPointerException
- Git使用
- 【mahapps.metro】如何快速让WPF窗体具有Metro扁平化风格
- 第四周项目三 猜数字游戏
- SciTE 编辑器汉化
- 夯实基础——static关键字
- C语言之辗转相除法
- 强力删除文件代码
- Gym 100015A Another Rock-Paper-Scissors Problem
- linux用户管理命令
- 快速开发一个属于自己的android数据库类库