Volley的使用(三)

一.概述

今天带大家从源码的角度来分析一下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();
            }
       }
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值