重溫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閱讀 230,622評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,716評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,746評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,991評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,706評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,036評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,029評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,203評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,725評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,451評論 3 361
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,677評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,161評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,857評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,266評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,606評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,407評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,643評論 2 380

推薦閱讀更多精彩內容