温故而知新 Volley源码解读与思考

  相比新的网络请求框架Volley真的很落后,一无是处吗,要知道Volley是由google官方推出的,虽然推出的时间很久了,但是其中依然有值得学习的地方。  从命名我们就能看出一些端倪,volley中文意为群射,齐射,官方解释说它适合通信频繁但是数据量不大的网络请求操作( a burst or emission of many things or a large amount at once ),至于为什么我们解读完源码就知道了。

  回想下使用Volley的过程:比如请求一个网页的内容。

  1. 创建RequestQueue对象

 RequestQueue mQueue = Volley.newRequestQueue(MyApplication.getInstance());

  2. 先创建一个StringRequest对象

private StringRequest stringRequest = new StringRequest(
            Request.Method.GET,
            "https://www.baidu.com",
            new Response.Listener<String>() {
                @Override
                public void onResponse(String response) {
                    Log.d(TAG, "current thread :" + Thread.currentThread().getName());  // main thread
                    ((TextView)findViewById(R.id.content)).setText(response);
                }
            },
            new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    Log.d(TAG, "error :" + error.getMessage());
                }
            }
    ) ;
View Code

  3.  将请求对象添加到mQueue中

mQueue.add(stringRequest);

  

  如下流程描述请自行结合Volley中的源码阅读(需要说明的是本文分析的Volley代码不是最新版本,还是1.0.x的版本):

  请求执行流程:

  首先我们要构造RequestQueue, 其内部封装了缓存请求队列:

  首先我们要构造RequestQueue, 其内部封装了缓存请求队列PriorityBlockingQueue<Request<?>> mCacheQueue 和网络请求队列 PriorityBlockingQueue<Request<?>> mNetworkQueue,同时也封装了一条缓存调度线程mCacheDispatcher和若干条网络请求调度线程 NetworkDispatcher[] mDispatchers,虽然RequestQueue的构造方法是public,但是我们还是调用Volley的newRequestQueue方法,因为在newRequestQueue方法有些重要的处理,比如设置DiskBasedCache的目录, 添加请求的User-agent,判断SDK的版本号,如果是2.3(API=9)以下则使用HttpClient, 如果是>=2.3的版本,则使用HttpUrlConnection,接着构建RequestQueue对象,并调用其start方法,创建并启动缓存调度线程和网络请求调度线程,目前的版本是1条缓存线程和4条网络请求线程。

  接着查看RequestQueue.add的相关逻辑:

   将构造的Request添加到RequestQueue中,即调用RequestQueue.add方法,这里会将请求先Add到一个Set集合中,即Set<Request<?>> mCurrentRequests中,然后判断是否禁用了缓存,如果禁用缓存则直接添加到mNetworkQueue中, 又因为NetworkDispatcher调度线程run方法中是while死循环,会一直取队列中的对象,故加入网络请求队列后,就相当于直接发起了网络请求。 而如果允许缓存,即Request.shouldCache返回true,则判断Map(Map<String,Queue<Request<?>> mWaitingRequests中是否有相同的请求,判断的标准就是请求的url,即request.getCacheKey()),如果mWaitingRequests中存在,则做提示处理,如果不存在则将请求添加到map中做记录,并执行mCacheQueue.add(request)

   请求加入了CacheQueue队列中,则缓存调度线程就可以从队列中取出requeset做处理。查看缓存调度线程CacheDispatcher的run方法,while循环中的逻辑如下,先取出缓存queue中的请求对象request,根据请求的url得到cache, 判断cache中entry是否为空,如果为空则说明没有缓存,则将请求添加到mNetworkQueue中,mNetworkQueue.put(request), 交由网络请求线程处理。如果有缓存,判断缓存是否过期,如果过期则同上,如果缓存可用,则取出缓存中数据做解析并返回,即调用request.parseNetworkResponse方法,解析之后调用mDelivery.postResponse方法做结果的投递,这里就将操作从子线程转移到主线程了,具体是由mDelivery去处理切换的操作, mDelivery(具体实现类是ExecutorDelivery)内部封装了Handler和Executor,将最终解析出的结果投递到主线程handler.post(runnable), 此handler是主线程的handler,构造RequestQueue队列时创建了主线程的Handler对象了,代码如下:

public RequestQueue(Cache cache, Network network, int threadPoolSize) {

        this(cache, network, threadPoolSize,

                new ExecutorDelivery(new Handler(Looper.getMainLooper())));

}

    5. 当请求添加到网络请求队列queue之后,在NetworkDispatcher的run方法中执行真正的网络请求,首先会判断线程是否退出了,或者request是否被取消了等逻辑,一切ok则执行mNetwork.performRequest(request),发起网络请求,然后解析结果,做缓存操作,派发解析结果到主线程等等

 

// 这里注意BlockingQueue的add offer put//// remove poll take peek等方法的区别

  1.add 将元素插入queue中,如果立即可行且不违反容量规则返回true,如果当前没有可用空间,则抛出IllegalStateExecption

        2.offer 与add方法类似,但是使用有限制容量的queue时,此方法通常优于add方法,后者可能可能无法插入元素,只是抛出一个异常

  3. put 插入元素到queue尾部,如果空间不够,则等待空间变得可用

  -----------------------------------------------------------------------------------------------------------------------------------

    4. remove 移除元素,返回true如果queue总包含此元素

       5.poll  获取并移除头部元素, E poll(), 如果queue为空,则返回null

  6.take  获取并移除头部元素,如果没有则等待直到有头部元素变得可用, E take() throws InterruptedException。

  7.peek 只是获取头部元素,并不做移除操作,如果queue为空,则返回null。

 

缓存执行流程

    上面简要分析了请求执行的过程,那么Volley是如何实现缓存和获取缓存的呢,我们接着分析,试想我们第一次请求某个网络资源时,必然是没有缓存的,那么最终会走到网络调用线程NetworkDispatcher  run方法中的逻辑,执行网络请求拿到NetworkResponse,然后解析networkResponse,即调用request的parseNetworkResponse得到Response对象,然后判断request是否允许缓存,如果需要缓存且response中的Cache.Entry即缓存对象不为空,则做缓存的操作。Cache.Entry对象cacheEntry什么时候被赋值的呢?就是在parseNetworkResponse返回Response对象的过程中,构造Response对象调用Response.success(result, HttpHeaderParser.parseCacheHeaders(response));, success函数的第二参数即为cacheEntry,查看parseCacheHeaders方法可以看到,entry中包含有data, etag,softTtl,lastModified,responseHeaders等数据。我们要缓存就是上边的cacheEntry,对应代码中的mCache.put(request.getCacheKey(), response.cacheEntry); 这里的mCache又是什么呢。查找mCache的源头又回到了Volley.newRequestQueue方法中,这里构建RequestQueue时传入了DiskBasedCache,那么看来mCache的具体实现类就是DiskBasedCache了。查看DiskBasedCache的源码,可以看到其默认缓存路径是/data/data/packagename/cache/volley/  , 默认的缓存大小为10M,其中最关键的就是put方法,put(String key, Entry entry) ,此方法首先会根据entry中data数组的长度判断是否能够缓存得下,也就是缓存后是否超过了设定的最大缓存容量值。具体在pruneINeed中做判断,如果超过最大值,则会按顺序依次从已缓存的文件中做删除操作(PS:如何做到按顺序删除呢,因为在putEntry方法中将key和cacheHeader的信息存储在了LinkedHashMap中了, 所以删除的时候才能依次按照缓存的先后顺序删除,最先缓存的先被删除掉),直到缓存本次data不再超过最大值为止,然后创建一个File对象存储缓存数据,File的name是将Url字符串的前半部分的hashcode加上字符串后半部分的hashcode组合而成,具体请查看getFilenameForKey(String key)方法,然后构建FileOutputStream对象分别将CacheHeader信息和data数据部分信息写入文件,如果写入的过程中发生了异常,则会做删除文件的处理。至于读取的操作请查看get方法.

  ClearCacheRequest请求执行流程

  可以看到在toolbox包下有一个ClearCacheRequest的类,看名字大概能猜测出来它是做清除缓存操作的。因为我们已经知道在Volley中的缓存逻辑是在DiskBasedCache中,查看DiskBasedCache中的的代码,可以找个一个clear方法, 我们可以在此方法的第一行打上断点,然后构造一个ClearCacheRequest对象,并添加到请求队列中(在构造ClearCacheRequest方法中需要传递两个参数,一个是mCache,一个是Runnable,其实mCache就是我们内部实现缓存的引用,Runnable可以做Clear后主线程上的操作), 启动调试模式,可以看到其执行流程 CacheDispatcher.run --- > ClearCacheReqeuest.isCanceled -->

DiskBasedCache.clear方法,其中ClearCacheRequest的isCanceled方法与其他xxxRequest的isCancled方法不同,其内部调用了mCache.clear() ,,并将Runnable对象投递到主线程的消息队列中,如果mCallback不为空的话。在DiskBasedCache的clear方法中则分别做了对文件缓存删除 和对内存缓存mEntries clear的操作。  

  网络请求流程

  发起网络请求的逻辑在BasicNetwork的performRequest方法中,我们可以看到方法内部使用的是while死循环也就是说要么得到请求的结果,要么抛出异常。 而使用while循环也是重试机制的关键。 先看下大致的流程, 添加请求的header (这里会从CacheHeader中获取,如果entry不为空,取出etag,headers.put("If-None-Match", etag, 取出lastModified,headers.put("If-Modified-Since", lastModified)) --> 发起网络请求 mHttpStack.performRequest --> 得到response ---> 解析response --> 返回NetworkResponse。 如果返回的状态码statusCode == 304 ,那么说明服务器在对比etag和lastModified后发现资源没有修改过,客户端直接使用缓存即可, 如果返回的状态码是301或302,则说明请求的资源移动了位置,需要重定向,我们取出响应头中的location信息,调用request.setRedirectUrl(url), 而后由于逻辑的处理返回的状态码不是2XX则会抛出IOException异常, 在catch的处理中会再次判断状态码并调用attemptRetryOnException,而此方法中的默认重试代理是DefaultRetryPolicy, 那么这个RetryPolicy是在哪设置的呢,查看Request的构造方法不难发现, 其中有setRetryPolicy(new DefaultRetryPolicy()) 的身影, 其retry方法中会对重试次数做判断,如果超过最大重试次数,则抛出异常,那么performRequest方法也会终止执行,如果小于等于最大重试次数则while循环的逻辑会再次执行,直到有结果。 其中需要注意到一点, 因为默认的连接超时时间较短只有2500ms,(不管是HttpClientStack的PerformRequest方法还是HurlStack的openConnection方法都会拿到request中设置的超时时间 int time = request.getTimeOutMs();在国内复杂的网络环境中可能从发起请求到响应时间会超过此值,一旦超过此值Volley默认则认为是超时了,从而触发重试的机制,导致一个请求发送两次的情况。解决的办法是可以增大默认超时的时间值,比如设置5000ms,或者设置不使用重试机制。

request.setRetryPolicy(new DefaultRetryPolicy(DefaultRetryPolicy.DEFAULT_TIMEOUT_MS, 0, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT)); 关于这个问题Volley的github库issue中也有提及:https://github.com/google/volley/issues/7

其实说了这么多,还是下面这张流程图的内容:

现在来做下问题总结:

1. 为什么说Volley不适合大文件的下载等操作,而是数据量小的通信网络场景?

  因为从Volley的源码中我们可以发现,其内部执行网络请求的线程是固定数量4条线程,如果下载大文件可能就会导致线程被长时间占用,后面排队的Request可能长时间得不到执行,Volley解析结果是直接放到byte[] 数组中,如果文件较大,则有可能发生oom,  且在Volley内部有缓存机制,如果大文件也允许缓存,而设定的最大缓存容量值较小,则可能发生长时间的IO操作(因为可能超过最大容量而要做删除文件操作),导致应用性能下降。

2. Volley中的缓存调度线程和网络调用线程的run方法中是while死循环,什么时候退出,也就是缓存和网络调度线程什么时候结束工作?

  其实在run方法的内部有相关逻辑, 比如NetworkDispatcher的run方法中,会捕获InterruptedException异常,在异常处理中判断mQuit的值,如果为true则直接返回。而调用Interrupt方法和设置mQuit值的处理就在NetworkDispatcher对应的quit() 方法中。

3.  可否将处理网络请求的线程改成线程池ThreadPoolExecutor?

  可以改,但是即使改为线程池实现,性能可能也不会有提升,一方面对于手机cpu来说其核心数是有限的,如果线程池内的线程数配置的较大,则网络请求时可能导致线程的频繁的发生切换,而线程的切换是有开销的。

4. Volley可否加载较大的图片,比如十几M,几十M等?

  因为Volley中解析完数据是要保存在byte[] data,中的,所以如果数据过大则有可能发生OOM异常。https://github.com/google/volley/issues/12

5. 使用Volley时应该在哪里创建RequestQueue合适?

  具体可以在自定义的Application中,主要是传递给newRequestQueue的Context应该使用ApplicationContext,这样可以避免可能发生的内存泄漏的情况,试想如果持有Activity的context那么Volley内部的工作没有做完则一直持有Activity,导致Activity无法释放,故在自定义的Apllication初始化一个全局的请求队列即可。

6. onResponse是在主线程中执行,但是返回结果后还需要做耗时操作怎么办?

  从Volley的源码中我们能够知道派发器mDelivery的是ExecutorDelivery,其默认实现是传递主线程的handler的构造方法,而ExecutorDelivery的内部还有一个传递executor的构造方法,只要构建一个的executor,在new RequestQueue时,让 mDelivery = new ExecutorDelivery(executor), 那么onResponse最终就在executor的线程中执行, 不再是主线程了。

7. 如何取消某个或者多个网络请求?

  取消单个request可以调用request.cancel(), 如果是多个可以给某个类别的request设置一个tag,想要取消请求调用requestQueue.cancelAll(tag),调用cancel方法后Request内的属性mCanneled即被复制为true,在CacheDispatcher或者NetworkDispatcher的run方法中会对request.isCanceled做判断。如果是取消多个请求,调用cancelAll 方法,则会在当前的请求集合中进行遍历,找到tag一致的request。

7. Volley有什么优缺点。

  优点:  

  还是那句: 适合网络通信频繁,但是通信数据量不大的请求,不适合大文件的下载。

  可以缓存http请求,过滤重复请求(一般网络请求框架也都支持)

      支持请求的优先级

   支持取消请求的API,可以取消单个请求,也可以设置取消请求的范围域

      基于接口的设计,使扩展相对容易(比如写一个XMLRequest类 继承Request,实现onResponse方法和parseNetworkResponse方法)

  缺点:

  对于文件的上传和下载支持的不好

  与Apache的Httpclient 和 HttpUrlConnection耦合较紧密

  Android 6.0系统移除对HttpClient的支持,所以要使用Volley,需要配置org.apache.http.legacy.jar的引用

  https://github.com/google/volley/releases 最新的Volley是1.1.0的版本,修复了如下问题:

  • Apache HTTP is now an optional dependency (#2). See Migrating from Apache HTTP for details on how to avoid using it.
  • Fix OutOfMemoryErrors and NegativeArraySizeExceptions in DiskBasedCache (#12).
  • Fix memory leak in Request#mErrorListener (#15).
  • Support for multiple identical response headers (#21).
  • Fix potential NullPointerException in ImageRequest/JsonRequest/StringRequest (#64).
  • Fix soft TTL for duplicate in-flight requests (#73).
  • Fix case-sensitive header reads from cache (#76).

待补充。。。

  

特别声明:如果转载,请保留出处信息,感谢您的关注和推荐!!
原文地址:https://www.cnblogs.com/sphere/p/7745304.html