Android Volley 源码解析(一),网络请求的执行流程

前言

花了好几天,重新研究了 Volley 的源码实现,比起之前又有了一番新的体会,啃源码真的是一件让人纠结的事情,阅读优秀的源码,特别是难度相对较大的源码,一旦陷入代码细节或者情绪一烦躁,很容易让人奔溃,但是真正的啃下来,收获真的很大。从优秀的代码中学习优秀的编程思想以及良好的代码设计和代码风格是一个非常好的方法,这次通读了 Volley 的源码之后,对于 Volley 的代码质量和拓展性深感佩服,为了更好的记录这次的源码研究之旅,写几篇博客记录一下。

一、Volley 简介


Volley 是 Google 在 2013 年的 I/O 大会上推出的 「Android 异步网络请求框架和图片加载框架」,它的设计目标就是去进行 数据量不大,但 通信频繁 的网络操作,而对于大数据量的网络操作,比如下载文件等,Volley 的表现就会非常糟糕。

Volley 的使用方法

在进行源码分析之前,先让我们来看下平时是怎样使用 Volley 的

   RequestQueue requestQueue = Volley.newRequestQueue(context);
   StringRequest stringRequest = new StringRequest(url
           , new Response.Listener<String>() {
       @Override
       public void onResponse(String s) {
           // TODO:
       }
   }, new Response.ErrorListener() {
       @Override
       public void onErrorResponse(VolleyError error) {
          // TODO:
       }
   });
   requestQueue.add(stringRequest);

1、通过 Volley.newRequestQueue(Context) 获取一个 RequestQueue
2、传入 URL 构建 Request,并实现相应的回调
3、将 Request 加入到 RequestQueue 中

Volley 中比较重要的类

在这先把 Volley 中比较重要的类说一下,到时候看源码能更加明白:

类名 作用
Volley 对外暴露的 API,主要作用是构建 RequestQueue
Request 所有网络请求的抽象类,StringRequest、JsonRequest、ImageRequest 都是它的子类
RequestQueue 存放请求的队列,里面包括 CacheDispatcher、NetworkDispatcher 和 ResponseDelivery
Response 封装一个解析后的结果以便分发
CacheDispatcher 用于执行缓存队列请求的线程
NetworkDispatcher 用户执行网络队列请求的线程
Cache 缓存请求结果,Volley 默认使用的是基于 sdcard 的 DiskBaseCache
HttpStack 处理 Http 请求,并返回请求结果
Network 调用 HttpStack 处理请求,并将结果转换成可被 ResponseDelivery 处理的 NetworkResponse
ResponseDelivery 返回结果的分发接口

二、请求的执行流程


我们从 Volley 的使用方法入手,一步一步探究底层的源码实现,我们的入手点就是
Volley.newRequestQueue(context)

    public static RequestQueue newRequestQueue(Context context) {
        return newRequestQueue(context, (BaseHttpStack) null);
    }

这个方法只有一行代码,只是调用了 newRequestQueue() 的方法重载,并给第二个参数传入 null,那我们看下带有两个参数的 newRequestQueue 方法中的代码

    public static RequestQueue newRequestQueue(Context context, BaseHttpStack stack) {
        BasicNetwork network;
        if (stack == null) {
            if (Build.VERSION.SDK_INT >= 9) {
                network = new BasicNetwork(new HurlStack());
            } else {
                String userAgent = "volley/0";
                try {
                    String packageName = context.getPackageName();
                    PackageInfo info = context.getPackageManager().getPackageInfo(packageName, 0);
                    userAgent = packageName + "/" + info.versionCode;
                } catch (NameNotFoundException e) {
                }

                network = new BasicNetwork(
                        new HttpClientStack(AndroidHttpClient.newInstance(userAgent)));
            }
        } else {
            network = new BasicNetwork(stack);
        }

        return newRequestQueue(context, network);
    }

    private static RequestQueue newRequestQueue(Context context, Network network) {
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);
        RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);
        queue.start();
        return queue;
    }

可以看到,这个方法中先判断 stack 是否为 null,如果是的话,这里会根据 Android 手机的系统版本号来进行相应的处理,当 SDK >= 9,则创建一个 HurlStack 实例,否则创建一个 HttpClientStack 实例,实际上 HurlStack 内部使用的是 HttpURLConnction 进行网络请求,而 HttpClientStack 则是使用 HttpClient 进行网络请求,这里之所以要这么处理,主要是因为在 Android 2.3(SDK = 9)之前,HttpURLConnection 存在一个很严重的问题,所以这时候用 HttpClient 来进行网络请求会比较合适,具体的原因可以看下这篇文章:Android 一起来看看 HttpURLConnection 和 HttpClient 的区别

不过由于现在的 Android 手机基本都是 4.0 以上的,而且 HttpClient 已经由于某些原因被弃用了,所以现在只要了解 HttpURLConnection 相关的知识就够了。思路拉回来,我们继续看代码,拿到 Stack 的实例之后将其构建成一个 Network 对象,它是用于根据传入的 Stack 对象来处理网络请求的,紧接着构建出一个 RequestQueue 对象,并调用 start() 方法。

我们接着看 start() 方法究竟做了什么:

    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();
        }
    }

    public void stop() {
        if (mCacheDispatcher != null) {
            mCacheDispatcher.quit();
        }
        for (final NetworkDispatcher mDispatcher : mDispatchers) {
            if (mDispatcher != null) {
                mDispatcher.quit();
            }
        }
    }

先调用 stop() 方法将当前正在进行 Dispatcher 都停掉,然后创建了一个 CacheDispatcher 实例,并调用了它的 start() 方法,接着在一个循环里去创建 NetworkDispatcher 的实例,分别调用它们的 start() 方法,这里的 CacheDispatcher 和 NetworkDispatcher 都是继承自 Thread 的,默认情况下 for 循环会执行四次,也就是说当调用了 Volley.newRequestQueue(context) 之后,就会有五个线程在后台运行,等待网络请求的到来,其中 CacheDispatcher 是缓存线程,NetworkDispatcher 是网络请求线程。

得到 RequestQueue 之后,构建相应的 Request,然后调用 add() 方法将其加入到请求队列中

    public <T> Request<T> add(Request<T> request) {
        // 将 Request 标记为属于此队列,并将其放入 mCurrentRequests 中
        request.setRequestQueue(this);
        synchronized (mCurrentRequests) {
            mCurrentRequests.add(request);
        }

        // 让 Request 按照他们被添加的顺序执行
        request.setSequence(getSequenceNumber());
        request.addMarker("add-to-queue");

        //如果请求不需要被缓存,就跳过缓存,直接进行网络请求
        if (!request.shouldCache()) {
            mNetworkQueue.add(request);
            return request;
        }
        mCacheQueue.add(request);
        return request;
     }

可以看到,传入 Request 之后,会先判断该 Request 是否需要进行缓存,如果不需要就直接将其加入到网络请求队列,需要缓存则加入缓存队列。默认情况下,每条请求都是应该缓存的,当然我们也可以调用 Request 的 setShouldCache() 方法来进行设置。

Request 被添加到缓存队列中后,在后台等待的缓存线程就要开始运行起来了,我们看下 CacheDispatcher 的 run() 方法究竟是怎么实现的。

    @Override
    public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        // 初始化 Cache
        mCache.initialize();

        while (true) {
            try {
                processRequest();
            } catch (InterruptedException e) {
                if (mQuit) {
                    return;
                }
            }
        }
    }

    private void processRequest() throws InterruptedException {
        final Request<?> request = mCacheQueue.take();
        request.addMarker("cache-queue-take");

        // 如果请求已经取消了,我们直接结束该请求
        if (request.isCanceled()) {
            request.finish("cache-discard-canceled");
            return;
        }

        // 从 Cache 中取出包含请求缓存数据的 Entry
        Cache.Entry entry = mCache.get(request.getCacheKey());
        if (entry == null) {
            request.addMarker("cache-miss");
            if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
                mNetworkQueue.put(request);
            }
            return;
        }

        // 如果缓存的请求过期了,就将其添加到网络请求队列中
        if (entry.isExpired()) {
            request.addMarker("cache-hit-expired");
            request.setCacheEntry(entry);
            if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
                mNetworkQueue.put(request);
            }
            return;
        }

        // 缓存的数据封装成 NetworkResponse
        Response<?> response = request.parseNetworkResponse(
                new NetworkResponse(entry.data, entry.responseHeaders));

        if (!entry.refreshNeeded()) {
            // 如果缓存没有过期就直接进行分发
            mDelivery.postResponse(request, response);
        } else {
            // 重置该请求的 Entry
            request.setCacheEntry(entry);
            response.intermediate = true;

            if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
                mDelivery.postResponse(request, response, new Runnable() {
                    @Override
                    public void run() {
                        try {
                            mNetworkQueue.put(request);
                        } catch (InterruptedException e) {
                            Thread.currentThread().interrupt();
                        }
                    }
                });
            } else {
                mDelivery.postResponse(request, response);
            }
        }
    }

代码相对比较长,我在关键的地方已经打上注释了,在这里总结一下,可以看到在初始化了 Cache 之后,有一个 while(true) 循环,说明缓存线程是始终执行的,接着会在缓存中取出响应结果,如果为 null 的话,就将其加入到网络请求队列中,如果不为空的话,再判断该缓存是否已过期,已经过期则同样把这条请求加入到网络请求队列中,否则直接使用缓存中的数据。最后将数据进行解析,并进行分发。

看完 CacheDispathcer 的 run() 方法,我们接着看 NetworkDispatcher 的 run() 方法

    @Override
    public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        while (true) {
            try {
                processRequest();
            } catch (InterruptedException e) {
                if (mQuit) {
                    return;
                }
            }
        }
    }

    private void processRequest() throws InterruptedException {
        Request<?> request = mQueue.take();

        long startTimeMs = SystemClock.elapsedRealtime();
        try {
            request.addMarker("network-queue-take");

            // 如果 Request 已经取消了,那就不执行网络请求
            if (request.isCanceled()) {
                request.finish("network-discard-cancelled");
                request.notifyListenerResponseNotUsable();
                return;
            }

            addTrafficStatsTag(request);

            // 执行网络请求
            NetworkResponse networkResponse = mNetwork.performRequest(request);

            // 如果服务器返回 304,而且我们已经分发过该 Request 的结果,那就不用进行第二次分发了
            //(这里补充一下,304 代表服务器上的结果跟上次访问的结果是一样的,也就是说数据没有变化)
            if (networkResponse.notModified && request.hasHadResponseDelivered()) {
                request.finish("not-modified");
                request.notifyListenerResponseNotUsable();
                return;
            }

            // 在子线程解析返回的结果
            Response<?> response = request.parseNetworkResponse(networkResponse);

            // 如果需要的话,就将返回结果写入缓存
            if (request.shouldCache() && response.cacheEntry != null) {
                mCache.put(request.getCacheKey(), response.cacheEntry);            }

            // 分发响应结果
            request.markDelivered();
            mDelivery.postResponse(request, response);
            request.notifyListenerResponseReceived(response);
        } catch (VolleyError volleyError) {
            request.notifyListenerResponseNotUsable();
        } catch (Exception e) {
            request.notifyListenerResponseNotUsable();
        }
    }

在 NetworkDispatcher 同样使用了 while(true),说明网络请求线程也是不断运行的。然后从网络队列里面取出 Request,再调用 Network 的 performRequest() 方法去发送网络请求。Network 其实是一个接口,这里具体的实现是 BasicNetwork,我们来看下它的 performRequest() 方法实现:

    @Override
    public NetworkResponse performRequest(Request<?> request) throws VolleyError {
        long requestStart = SystemClock.elapsedRealtime();
        while (true) {
            HttpResponse httpResponse = null;
            byte[] responseContents = null;
            List<Header> responseHeaders = Collections.emptyList();
            try {
                Map<String, String> additionalRequestHeaders =
                        getCacheHeaders(request.getCacheEntry());
                 // 该注意的地方:调用 Stack 的 executeRequest 进行网络请求
                httpResponse = mBaseHttpStack.executeRequest(request, additionalRequestHeaders);
                int statusCode = httpResponse.getStatusCode();

                responseHeaders = httpResponse.getHeaders();
                if (statusCode == HttpURLConnection.HTTP_NOT_MODIFIED) {
                    Entry entry = request.getCacheEntry();
                    if (entry == null) {
                        return new NetworkResponse(HttpURLConnection.HTTP_NOT_MODIFIED, null, true,
                                SystemClock.elapsedRealtime() - requestStart, responseHeaders);
                    }
                    List<Header> combinedHeaders = combineHeaders(responseHeaders, entry);
                    return new NetworkResponse(HttpURLConnection.HTTP_NOT_MODIFIED, entry.data,
                            true, SystemClock.elapsedRealtime() - requestStart, combinedHeaders);
                }

                // 有些返回结果是没有内容的,如:204,所以我们必须进行检查
                InputStream inputStream = httpResponse.getContent();
                if (inputStream != null) {
                  responseContents =
                          inputStreamToBytes(inputStream, httpResponse.getContentLength());
                } else {
                  responseContents = new byte[0];
                }
                return new NetworkResponse(statusCode, responseContents, false,
                        SystemClock.elapsedRealtime() - requestStart, responseHeaders);
            } catch (Exception e) {
                // ...
            }
        }
    }

这个方法里面,基本上都是网络请求方面处理的细节,我们这篇文章,主要是梳理整体的流程,对细节方面先不深入。需要注意的是在我标注的第一个地方,调用了 Stack 的 executeRequest() 方法,这里的 Stack 就是之前调用 Volley.newRequestQueue() 所创建的实例,前面也说过了这个对象的内部是使用了 HttpURLConnection 或 HttpClient(已弃用)来进行网络请求。网络请求结束后将返回的数据封装成一个 NetworkResponse 对象进行返回。

在 NetworkDispatcher 接收到了这个 NetworkResponse 对象之后,又会调用 Request 的 parseNetworkResponse() 方法来对结果进行解析,然后将数据写入到缓存,最后调用 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(一个 Executor 的实例对象) 的 execute() 方法中传入了一个 ResponseDeliveryRunnable 对象,execute() 方法默认是在主线程中执行的,这样就保证了 ResponseDeliveryRunnable 的 run() 方法也是在主线程当中运行的,我们看下 run() 方法里面的逻辑:

        @SuppressWarnings("unchecked")
        @Override
        public void run() {
            // 如果 Request 被取消了,调用 finish() 方法,结束该请求,不进行传递
            if (mRequest.isCanceled()) {
                mRequest.finish("canceled-at-delivery");
                return;
            }

            // 根据响应的结果来进行不同的分发
            if (mResponse.isSuccess()) {
                mRequest.deliverResponse(mResponse.result);
            } else {
                mRequest.deliverError(mResponse.error);
            }

            // 如果传入的 mRunnable 不为 null,则运行
            if (mRunnable != null) {
                mRunnable.run();
            }
       }

可以看到当 Response.isSuccess() 为 true 的话,调用 Resquest 的 deliverResponse() 方法,对结果进行回调,deliverResponse() 方法是每一个具体的 Request 子类都必须实现的抽象类,来看下我们最熟悉的 StringRequest 中的 deliverResponse() 方法

    @Override
    protected void deliverResponse(String response) {
        Response.Listener<String> listener;
        synchronized (mLock) {
            listener = mListener;
        }
        if (listener != null) {
            listener.onResponse(response);
        }
    }

看到这里应该就很明白了,在 deliverResponse() 方法中,调用 listener.onResponse() 方法进行回调,这个 listener 正是我们构建 StringRequest 时传入的 Listener,也就是说将返回的结果回调到我们在外部调用的地方。

   StringRequest stringRequest = new StringRequest(url
           , new Response.Listener<String>() {
       @Override
       public void onResponse(String s) {
           // TODO:
       }
   }, new Response.ErrorListener() {
       @Override
       public void onErrorResponse(VolleyError error) {
          // TODO:
       }
   });

到这里,终于把 Volley 的完整执行流程全部都梳理了一遍,最后我们来看一下 Volley 官方提供的流程图:



参考

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

推荐阅读更多精彩内容