Volley的原理解析

BaiHongHua發表於2019-03-01

前言

在 Android 開發程式設計的過程當中,無可避免地需要涉及到網路程式設計,這就需要我們開發者比較熟練地掌握網路程式設計。哈哈,今天就先從解析 Volley 開始吧!

1.0 Volley是什麼

Volley 是在 2013 年的 Googel I/O 大會上面推出的一個 HTTP 庫,它可以幫助 Android 應用更加方便地執行網路請求。它既可以訪問網路取得資料,同時還可以訪問網路取得圖片。

2.0 Volley的優缺點

Volley 有以下的優點:
  • 自動排程網路請求
  • 高併發網路連線
  • 通過標準的 HTTP cache coherence(快取記憶體一致性)快取磁碟的記憶體透明的響應
  • 支援指定請求的優先順序
  • 撤銷請求 API,或者指定取消請求佇列中的一個區域
  • 框架容易被定製。例如,定製重試或者回撥功能
  • 強大的指令(Strong ordering)可以使得非同步載入網路資料並正確地顯示到 UI 的操作更加簡單
  • 包含了除錯與追蹤工具
Volley 的缺點:
  • 不適合用來下載大的資料檔案

3.0 Volley的網路請求佇列

使用 Volley 的流程是,建立一個 RequestQueue(請求佇列)物件,然後把 Request(請求)提交給它。

// 程式碼[1]

final TextView textView = (TextView) MainActivity.this.findViewById(R.id.tv);

//[1.0]建立 RequestQueue 的例項
RequestQueue requestQueue = Volley.newRequestQueue(MainActivity.this); // 1

String url = "http://gank.io/api/data/Android/10/1";

//[2.0]構造一個 request(請求)
StringRequest request = new StringRequest(Request.Method.GET, url, new Response.Listener<String>() {
    @Override
    public void onResponse(String response) {
        textView.setText("Response is: " + response.toString());
    }
}, new Response.ErrorListener() {

    @Override
    public void onErrorResponse(VolleyError error) {
        textView.setText("Error is happenning");
    }
});

//[3.0]把 request 新增進請求佇列 RequestQueue 裡面
requestQueue.add(request);複製程式碼

上面程式碼邏輯主要是通過構造一個 StringRequest 的例項,然後把這個例項新增進請求佇列 RequestQueue 裡面。我們來檢視註釋 1 的 Volley.newRequestQueue(Context) 方法的原始碼:

// 原始碼[2]

public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
        File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);  // 1

        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) {  // 2
                stack = new HurlStack();
            } else {
                stack = new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
            }
        }

        Network network = new BasicNetwork(stack);

        RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network); // 3
        queue.start(); // 4

        return queue;
    }

public static RequestQueue newRequestQueue(Context context) {
    return newRequestQueue(context, null);
}複製程式碼

從上面的原始碼可以看出,newRequestQueue(Context)newRequestQueue(Context, HttpStack) 的過載方法,下面我們主要來看 newRequestQueue(Context, HttpStack) 方法,在註釋 1 處通過 new File(File, String) 初始化構建一個 cacheDir 的快取(這是一個名詞),在註釋 3 處,new DiskBasedCache(cacheDir) 為這個快取分配了 5M 的儲存空間;
回到註釋 2, 當 SDK 的版本大於或等於 9 的時候,也就是 Android 的版本號大於或等於 2.3,則建立基於 HttpURLConnectionHurlStack,否則就建立基於執行請求的 HttpClientHttpClientStack,然後在註釋 3 處,通過 new RequestQueue(Cache, Network) 建立請求佇列,我們來檢視下 new RequestQueue(Cache, Network) 的原始碼:

// 原始碼[3]

    ...

private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;

    ...

public RequestQueue(Cache cache, Network network) {
        this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
    }
    ...複製程式碼

構造方法 new RequestQueue(Cache, Network) 為網路請求分配了 4 條執行緒進行請求。對於 RequestQueue 這個類,主要的作用是作為一個執行緒池用於排程佇列中的請求:當呼叫 add(Request) 將會把傳進的請求(Request)在快取佇列(cache)或者網路佇列(network)中解析,然後傳遞迴主執行緒,也就是 程式碼[1] 裡面的回撥函式 onResponse(String)onErrorResponse(VolleyError) 拿到回撥的請求內容;
在 程式碼[1] 的註釋 4 處,呼叫了 add(Request) 方法:

// 原始碼[4]

public <T> Request<T> add(Request<T> request) {
        request.setRequestQueue(this);
        //同步程式碼塊,保證 mCurrentRequests.add(request) 在一個程式裡面的所有執行緒裡面,有且只能在
        //一個執行緒裡面執行
        synchronized (mCurrentRequests) { 
            mCurrentRequests.add(request);
        }

        request.setSequence(getSequenceNumber());
        request.addMarker("add-to-queue");

        //如果不可以儲存,就把請求(request)新增進網路排程佇列裡面
        if (!request.shouldCache()) {
            mNetworkQueue.add(request);  
            return request;
        }

        //如果可以儲存:
        //同步程式碼塊
        synchronized (mWaitingRequests) {  
            String cacheKey = request.getCacheKey();
            //如果之前有相同的請求並且還沒有返回結果的,就把此請求加入到 mWaitingRequests 裡面
            if (mWaitingRequests.containsKey(cacheKey)) {   
                Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
                //如果沒有請求在進行中,重新初始化 Queue<Request<?>> 的例項
                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 {
                //如果沒有的話,就把請求新增到 mCacheQueue 佇列裡面
                mWaitingRequests.put(cacheKey, null);
                mCacheQueue.add(request);  
            }
            return request;
        }
    }
}複製程式碼

從上面的 add(Request) 方法的原始碼可以看出,主要的邏輯是:如果請求(request)不可以快取,就直接新增進快取佇列,否則新增進快取佇列; 當拿到 mNetworkQueuemCacheQueue 以後,就把請求返回並且呼叫 start() 方法(如 原始碼[2] 的註釋 4),當檢視 start() 方法的原始碼時候:

// 原始碼[5]

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();
        }
    }複製程式碼

裡面主要是建立 CacheDispatcher(BlockingQueue<Request<?>>, BlockingQueue<Request<?>>, Cache, ResponseDelivery) 的例項並通過 mCacheDispatcher.start() 開啟快取排程執行緒,和建立 NetworkDispatcher(BlockingQueue<Request<?>>,Network, Cache,ResponseDelivery) 的例項並通過 networkDispatcher.start() 開啟網路排程執行緒。

4.0 網路排程執行緒NetworkDispatcher

網路排程執行緒 NetworkDispatcher 是一個繼承於 Thread 的執行緒,通過檢視其任務方法 run() 的原始碼:

// 原始碼[6]

@Override
    public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        Request<?> request;
        while (true) {
            try {  
                // 從網路佇列中取出請求      
                request = mQueue.take();
            } catch (InterruptedException e) {           
                if (mQuit) {
                    return;
                }
                continue;
            }

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

                // 如果請求被取消
                if (request.isCanceled()) {
                    request.finish("network-discard-cancelled");
                    continue;
                }

                addTrafficStatsTag(request);

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

                if (networkResponse.notModified && request.hasHadResponseDelivered()) {
                    request.finish("not-modified");
                    continue;
                }

                Response<?> response = request.parseNetworkResponse(networkResponse);
                request.addMarker("network-parse-complete");

                // 把網路請求到的實體快取到 原始碼[2] 的註釋 1 處的本地快取檔案裡面
                if (request.shouldCache() && response.cacheEntry != null) {
                    mCache.put(request.getCacheKey(), response.cacheEntry);
                    request.addMarker("network-cache-written");
                }

                request.markDelivered();
                // 回撥請求到的響應給主執行緒
                mDelivery.postResponse(request, response);
            } catch (VolleyError volleyError) {
                ...

            } catch (Exception e) {
                ...

            }
        }
    }複製程式碼

網路排程的主要邏輯是,先判斷網路請求有沒有被取消,如果沒有被取消,就通過 mNetwork.performRequest(request) 執行請求網路響應,拿到響應以後,先快取在本地,然後再回撥給主執行緒。

5.0 快取排程執行緒CacheDispatcher

快取排程執行緒 CacheDispatcher 的原始碼如下所示:

@Override
    public void run() {
        if (DEBUG) VolleyLog.v("start new dispatcher");
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

        mCache.initialize();

        while (true) {
            try {            
                final Request<?> request = mCacheQueue.take();
                request.addMarker("cache-queue-take");

                //如果請求被取消
                if (request.isCanceled()) {
                    request.finish("cache-discard-canceled");
                    continue;
                }

                // 嘗試在本地快取中取回資料
                Cache.Entry entry = mCache.get(request.getCacheKey());
                if (entry == null) {
                    //本地快取丟失或者沒有
                    request.addMarker("cache-miss");                   
                    mNetworkQueue.put(request);
                    continue;
                }

                // 本地快取過期
                if (entry.isExpired()) {
                    request.addMarker("cache-hit-expired");
                    request.setCacheEntry(entry);
                    mNetworkQueue.put(request);
                    continue;
                }

                // 命中快取
                request.addMarker("cache-hit");
                Response<?> response = request.parseNetworkResponse(
                        new NetworkResponse(entry.data, entry.responseHeaders));
                request.addMarker("cache-hit-parsed");

                if (!entry.refreshNeeded()) {
                    // 回撥相應給主執行緒
                    mDelivery.postResponse(request, response);
                } else {

                    request.addMarker("cache-hit-refresh-needed");
                    request.setCacheEntry(entry);

                    response.intermediate = true;

                    mDelivery.postResponse(request, response, new Runnable() {
                        @Override
                        public void run() {
                            try {
                                mNetworkQueue.put(request);
                            } catch (InterruptedException e) {
                                ...
                            }
                        }
                    });
                }

            } catch (InterruptedException e) {
                ...
            }
        }
    }複製程式碼

同樣地,如果請求沒有被取消,就開始在取回本地的快取,當本地的快取不存在、丟失或者已經過期,就把請求新增到網路請求佇列,當命中本地快取,就把快取的響應回撥給主執行緒;

6.0 Volley原理解析

為了傳送一個請求,你只需要構造一個請求並通過 add() 方法新增到 RequestQueue 中。一旦新增了這個請求,它會通過佇列,通過一系列的排程,然後得到原始的響應資料並返回。

當執行 add()方法時,Volley觸發執行一個快取處理執行緒以及一系列網路處理執行緒。當新增一個請求到佇列中,它將被快取執行緒所捕獲並觸發: 如果這個請求可以被快取處理,那麼會在快取執行緒中執行響應資料的解析並返回到主執行緒。如果請求不能被快取所處理,它會被放到網路佇列中。網路執行緒池中的第一個可用的網路執行緒會從佇列中獲取到這個請求並執行HTTP操作,解析工作執行緒的響應資料,把資料寫到快取中並把解析之後的資料返回到主執行緒。

一個請求的生命週期

The lifecycle of a request
The lifecycle of a request

小結

到此,Volley 庫的分析就到先到此暫時結束了。這次,筆者主要是就當一個請求新增進請求佇列之後,到其返回響應的過程進行了分析,瞭解到了 Volley 是如何把一個請求進行排程,然後得到響應並返回的。哈哈,感謝你的閱讀...


原始碼下載 TestVolley

相關文章