重温Volley源码(一):工作流程

目录

一、写在前面

二、工作流程

参考资料

一、写在前面

Volley是Google在2013年I/O大会上推出的Android异步网络请求框架和图片加载框架,新技术的日新月异发展,感觉已经慢慢被OkHttp替代了,现在重新去读它的源码,虽然稍显得有些过气,但还是有很大的学习价值的,在此记录下自己的脚印。

先来看看关于Volley的几个关键特点:

  • 适合数据量小,通信频繁的网络操作
  • 基于接口设计,面向接口编程,可扩展性强
  • 一定程度符合Http规范(ResponseCode请求响应码、请求头处理、缓存机制、请求重试、优先级定义)
  • Android 2.2以下基于HttpClient,2.3及以上基于HttpUrlConnenction
  • 提供了简便的图片加载工具

二、工作流程

既然是探索Volley的工作流程,我们可以一步步追踪其源码,先来看一个典型的发送Volley网络请求的用法:


        //1、创建请求队列
        RequestQueue mQueue = Volley.newRequestQueue(this);

        //2、创建一个网络请求
        StringRequest stringRequest = new StringRequest("https://www.baidu.com",
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String response) {
                            Log.i(TAG, response);
                    }
                }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                     Log.i(TAG, error.getMessage(), error);          
            }
        });

        //3、将网络请求添加到请求队列中
        mQueue.add(stringRequest);

简简单单的三个步骤,完成了一个网络请求,并在onResponse中返回。那么Volley里面具体帮我们干了哪些事情呢,我们进入Volley.newRequestQueue(this) 这个方法内部一探究竟:


    public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
        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 {
                // Prior to Gingerbread, HttpUrlConnection was unreliable.
                // See: http://android-developers.blogspot.com/2011/09/androids-http-clients.html
                stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
            }
        }

        Network network = new BasicNetwork(stack);
        
        RequestQueue queue;
        if (maxDiskCacheBytes <= -1)
        {
            // No maximum size specified
            queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        }
        else
        {
            // Disk cache size specified
            queue = new RequestQueue(new DiskBasedCache(cacheDir, maxDiskCacheBytes), network);
        }

        queue.start();

        return queue;
    }

  • 如果HttpStack参数为null,则根据系统在API Level>=9采用HurlStack(内部为HttpUrlConnection),如果<9,采用基于HttpClientStack
  • 根据HttpStack 创建一个NetWork的具体实现类BasicNetwork对象
  • 根据DiskBasedCache磁盘缓存对象、network对象构建一个RequestQueue,调用RequestQueue的start方法启动。

通过源码可以看出,我们可以抛开Volley工具类构建自定义的RequestQueue,采用自定义的HttpStack,采用自定义的NetWork实现,采用自定义的Cache实现来构建RequestQueue,Volley的面向接口编程,高可拓展性的魅力就源于此。


关于HttpURLConnection 和 HttpClient的选择及原因

  1. 在Android2.2之前,HttpURLConnection 有个重大的bug,调用 close() 函数会影响连接池,导致连接复用失效,所以在Android2.2之前使用 HttpURLConnection 需要关闭 keepAlive
  2. 在Android2.3,HttpURLConnection 默认开启了 gzip 压缩,提高了 HTTPS 的性能;在Android 4.0,HttpURLConnection 支持了请求结果缓存

HttpURLConnection 本身 API 相对简单,所以对 Android 来说,在2.3之前建议使用HttpClient,之后建议使用 HttpURLConnection。

本着对Volley请求执行流程的侧重把握,我们接着看RequestQueue的start方法:


    public void start() {
        stop();  // Make sure any currently running dispatchers are stopped.
        // Create the cache dispatcher and start it.
        mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
        mCacheDispatcher.start();

        // Create network dispatchers (and corresponding threads) up to the pool size.
        for (int i = 0; i < mDispatchers.length; i++) {
            NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                    mCache, mDelivery);
            mDispatchers[i] = networkDispatcher;
            networkDispatcher.start();
        }
    }

  • 调用stop方法,停止所有的线程(CacheDispatcher 和 NetworkDispatcher)
  • 创建一个缓存调度线程CacheDispatcher并启动
  • 创建n个网络调度线程NetworkDispatcher并启动

在这段start方法执行完后,Volley就已经默认创建了5个线程(1个CacheDispatcher+4个NetworkDispatcher),这里存在优化的余地,比如我们可以根据CPU核数以及网络类型计算更合适的并发数

start方法执行完,由此得到RequestQueue,我们只需要构建相应的Request,然后调用RequstQueue的add方法,就可以完成网络请求操作。

关于Request类

  1. Request是一个网络请求的抽象类,非抽象子类有StringRequest、JsonRequest、ImageRequest或者自定义子类,我们通过构建这个对象,将其加入RequestQueue来完成一次网络请求操作
  2. Request子类必须实现的方法有两个:parseNetworkResponse 和 deliverResponse
    Volley支持8中请求方式:GETPOSTPUTDELETEHEADOPTIONSTRACEPATCH
  3. Request类包含了请求URL,请求方式,请求Header,请求Body,请求优先级等信息

RequestQueue的add方法内部实现:


    public <T> Request<T> add(Request<T> request) {
        // Tag the request as belonging to this queue and add it to the set of current requests.
        request.setRequestQueue(this);
        synchronized (mCurrentRequests) {
            mCurrentRequests.add(request);
        }

        // Process requests in the order they are added.
        request.setSequence(getSequenceNumber());
        request.addMarker("add-to-queue");

        // If the request is uncacheable, skip the cache queue and go straight to the network.
        if (!request.shouldCache()) {
            mNetworkQueue.add(request);
            return request;
        }

        // Insert request into stage if there's already a request with the same cache key in flight.
        synchronized (mWaitingRequests) {
            String cacheKey = request.getCacheKey();
            if (mWaitingRequests.containsKey(cacheKey)) {
                // There is already a request in flight. Queue up.
                Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
                if (stagedRequests == null) {
                    stagedRequests = new LinkedList<Request<?>>();
                }
                stagedRequests.add(request);
                mWaitingRequests.put(cacheKey, stagedRequests);
                if (VolleyLog.DEBUG) {
                    VolleyLog.v("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
                }
            } else {
                // Insert 'null' queue for this cacheKey, indicating there is now a request in
                // flight.
                mWaitingRequests.put(cacheKey, null);
                mCacheQueue.add(request);
            }
            return request;
        }
    }

  • 判断是否可以缓存,如果不能缓存则直接加入网络请求队列mNetworkQueue,能缓存则只需执行加入到缓存队列mCacheQueue中
  • 默认情况下,Volley的每条请求都是可以缓存的,如果不需要缓存,可以调用Request的setShouldCache(false)方法来改变这一默认行为

既然将请求加入到mNetworkQueue或者mCacheQueue中,接下来就是在对应的NetworkDispatcher或CacheDispatcher线程中执行了。

这里只看NetworkDispatcher的run方法(CacheDispatcher的run方法后半部分和这里类似)


    public class NetworkDispatcher extends Thread { 
    .....
    @Override
    public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        Request<?> request;
        while (true) {
            long startTimeMs = SystemClock.elapsedRealtime();
            // release previous request object to avoid leaking request object when mQueue is drained.
            request = null;
            try {
                // Take a request from the queue.
                request = mQueue.take();
            } catch (InterruptedException e) {
                // We may have been interrupted because it was time to quit.
                if (mQuit) {
                    return;
                }
                continue;
            }

            try {
                request.addMarker("network-queue-take");

                // If the request was cancelled already, do not perform the
                // network request.
                if (request.isCanceled()) {
                    request.finish("network-discard-cancelled");
                    continue;
                }

                addTrafficStatsTag(request);

                // Perform the network request.
                NetworkResponse networkResponse = mNetwork.performRequest(request);
                request.addMarker("network-http-complete");

                // If the server returned 304 AND we delivered a response already,
                // we're done -- don't deliver a second identical response.
                if (networkResponse.notModified && request.hasHadResponseDelivered()) {
                    request.finish("not-modified");
                    continue;
                }

                // Parse the response here on the worker thread.
                Response<?> response = request.parseNetworkResponse(networkResponse);
                request.addMarker("network-parse-complete");

                // Write to cache if applicable.
                // TODO: Only update cache metadata instead of entire record for 304s.
                if (request.shouldCache() && response.cacheEntry != null) {
                    mCache.put(request.getCacheKey(), response.cacheEntry);
                    request.addMarker("network-cache-written");
                }

                // Post the response back.
                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);
            }
        }
    }
    }

  • 外部while(true)的死循环,说明网络线程始终运行
  • 调用mNetwork的performRequest方法,将request对象传进入,执行具体的网络请求
  • 根据请求的返回值,调用Request的parseNetworkResponse方法来解析NetworkResponse的数据,以及将数据写入到缓存,这个方法的实现是交给Request的子类来完成的,因为不同种类的Request解析的方式也不同,就像在自定义Request中,必须重写parseNetworkResponse方法一样。

在解析完NetworkResponse的数据后,紧接着会调用ResponseDelivery的实现子类ExecutorDelivery的postResponse方法来回调解析出的数据:


    @Override
    public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
        request.markDelivered();
        request.addMarker("post-response");
        mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
    }

mResponsePoster的execute方法传入了一个ResponseDeliveryRunnable对象:


    private class ResponseDeliveryRunnable implements Runnable {
        private final Request mRequest;
        private final Response mResponse;
        private final Runnable mRunnable;

        public ResponseDeliveryRunnable(Request request, Response response, Runnable runnable) {
            mRequest = request;
            mResponse = response;
            mRunnable = runnable;
        }

        @SuppressWarnings("unchecked")
        @Override
        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 this is an intermediate response, add a marker, otherwise we're done
            // and the request can be finished.
            if (mResponse.intermediate) {
                mRequest.addMarker("intermediate-response");
            } else {
                mRequest.finish("done");
            }

            // If we have been provided a post-delivery runnable, run it.
            if (mRunnable != null) {
                mRunnable.run();
            }
       }
    }

可以看出实现了一个Runnable接口,在run方法内部判断是否响应成功,如果成功则调用Request的deliverResponse方法,否则调用deliverError方法。这里的deliverResponse方法内部最终会回调我们在构建Request时设置的Response.Listener对象,例如StringRequest的deliverResponse内部代码如下。


    public class StringRequest extends Request<String> {
    ......
    @Override
    protected void deliverResponse(String response) {
        if (mListener != null) {
            mListener.onResponse(response);
        }
    }
    }

其实performRequest内部转换成Response的处理过程,这里借用Volley源码解析 里的一张图,更加从宏观上清晰的说明问题了:

volley-response-process-flow-chart

从上到下表示从得到数据后一步步的处理,箭头旁的注释表示该步处理后的实体类。

关于Cache类

  1. 缓存接口,代表一个可以获取请求结果、存储请求结果的缓存
  2. 默认的两个实现子类:NoCache和DiskBasedCache
  3. DiskBasedCache类会把从服务器返回的信息写入磁盘,然后从磁盘取出缓存,这其中涉及了一些静态的方法如writeInt、writeLong等等,何解?原因之一是Java的IO本身是对byte进行操作,一个int占4个byte,需要按位写入,另一方面也是因为网络字节序是大端字节序,在80x86的平台中,是以小端法存放的,比如我们经过网络发送0x12345678这个整型,但实际上在流中是0x87654321

好了,到这里Volley的整体流程大概梳理了一遍,可能稍微讲得有点乱,当然也仅仅是个人的记录为主,最后,放上Volley官方的请求流程图镇楼(原本想着自己画一张流程图,但翻了翻发觉画不出比这个更好的了):

volley-request

参考资料

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,425评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,058评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,186评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,848评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,249评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,554评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,830评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,536评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,239评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,505评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,004评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,346评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,999评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,060评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,821评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,574评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,480评论 2 267

推荐阅读更多精彩内容