zoukankan      html  css  js  c++  java
  • 温故而知新 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).

    待补充。。。

      

    特别声明:如果转载,请保留出处信息,感谢您的关注和推荐!!
  • 相关阅读:
    python 的基础 学习 第六天 基础数据类型的操作方法 字典
    python 的基础 学习 第五天 基础数据类型的操作方法
    python 的基础 学习 第四天 基础数据类型
    ASP.NET MVC 入门8、ModelState与数据验证
    ASP.NET MVC 入门7、Hellper与数据的提交与绑定
    ASP.NET MVC 入门6、TempData
    ASP.NET MVC 入门5、View与ViewData
    ASP.NET MVC 入门4、Controller与Action
    ASP.NET MVC 入门3、Routing
    ASP.NET MVC 入门2、项目的目录结构与核心的DLL
  • 原文地址:https://www.cnblogs.com/sphere/p/7745304.html
Copyright © 2011-2022 走看看