Android網絡編程(七) 之 Volley原理分析

1 前言

Volley是Goole在2013年Google I/O大會上推出的開源網絡通信框架。Volley 的特點是使用簡單而且適合數據量小,通信頻繁的網絡操作,而對於大數據量的網絡操作,比如說下載文件等,它的表現就會非常糟糕,因爲Volley在解析期間將所有響應保存在內存中。Volley帶給我們開發者的是便利,它把Http所有通信細節、線程處理全部封裝在內部,我們只需要簡單調用就可以完成通信操作。關於如何使用Volley我們在前面文章《Android網絡編程(五) 之 Volley框架的使用》已經有所介紹,今天我們主要是爲了搞清楚Volley內部是實現原理,揭開它爲什麼只適用於數據量不大且通信頻繁的網絡操作以及它內部線程是怎樣一個工作原理。

2 使用回顧

Volley的使用是非常的簡單,實際上就是5個步聚:

1. 在Gradle中進行com.android.volley:volley:1.1.1 的依賴

2. 在AndroidManifest.xml中添加訪問網絡權限<uses-permissionandroid:name="android.permission.INTERNET" />

3. 創建請求隊列RequestQueue對象

4. 創建請求Request對象

5. 將Request對象add到RequestQueue對象中

示例:

RequestQueue requestQueue = Volley.newRequestQueue(getApplicationContext());
Request stringRequest = new StringRequest(Request.Method.GET, "http://www.xxx.com",
        new Response.Listener<String>() {
            @Override
            public void onResponse(String s) {
                // 請求成功
            }
        },
        new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError volleyError) {
                // 請求失敗
            }
        });
requestQueue.add(stringRequest);

3 原理分析

3.1 工作流程

開始前,我們先來看看官網上關於Volley的工作流程圖:

 

1. 圖中可見三種顏色分別代表三種線程:藍色是主線程,綠色是緩存線程,而橙色是網絡線程。

2. 如上面回顧中代碼,我們在主線程中將請求的Request對象加入到請求隊列RequestQueue中,Volley就正式開始工作。其實請求隊列RequestQueue內部在創建後主要是做了兩件事情,創建並啓動一條緩存線程 CacheDispatcher和創建並啓動N條網絡請求線程NetWorkDIspatcher。

3. 結合上圖,主線程請求後首先會經過綠色部分,也就是緩存線程CacheDispatcher,該線程中會去做一些判斷來確定當前請求的數據是否在緩存中。如果存在,則將數據取出,然後根據傳入的Request對象類型進行加工,然後將其返回給主線程;如果不存在,則會將請求交由網絡請求線程NetWorkDIspatcher來處理。

4. 網絡請求線程NetWorkDIspatcher會有N條,默認是4條,主要是負責進行網絡的請求,同時會判斷下載到的數據能否進行緩存,當請求成功後,便如緩存線程般,根據傳入的Request對象類形進行加工,然後將其返回給主線程。

3.2 RequestQueue的創建和工作原理

我們在上面使用代碼可見,一切從Volley.newRequestQueue()創建RequestQueue對象開始,我們來看看該方法源代碼:

Volley.java

public static RequestQueue newRequestQueue(Context context) {
    return newRequestQueue(context, (BaseHttpStack) null);
}
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, /* flags= */ 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);
}

newRequestQueue方法是兩個靜態公開方法,一個參數的方法最後也會調用到兩個參數的方法,方法內主要是爲了創建一個BasicNetwork對象,並將其傳遞到newRequestQueue方法中。BasicNetwork的創建分三種情況,首先會對方法第二個參數BaseHttpStack進行判空,先看如果不爲空,則直接將其傳給BasicNetwork的構造函數進行實例化BasicNetwork對象;如果不爲空,即再進一步判斷當前Android SDK是否>=9,即是否>=Android 2.3。如果是則創建一個HurlStack對象然後實例化BasicNetwork對象,否則創建一個HttpClientStack對象然後再實例化BasicNetwork對象。

如果你進一步看HurlStack和HttpClientStack的源碼,會發現HurlStack是基於HttpURLConnection來實現的,而HttpClientStack是基於HttpClient來關現的。其中原因就是HttpURLConnection在Android2.3之前會非常不靠譜,存在一些Bug,比如在在讀取 InputStream時調用 close()方法,就有可能會導致連接池失效了,所以在以前如果在Android2.3之前使用的話,通常的解決辦法就是禁用掉連接池的功能,而在Android2.3起,由於已經不存在剛纔說的Bug,而且因爲HttpURLConnection其API簡單、壓縮和緩存機制可以有效地減少網絡訪問的流量,改進了網絡速度等優化,故一般情況下都是使用HttpURLConnection代替HttpClient,並且在Android6.0之後已經默認將HttpClient移除SDK中。

回到正題,所以BasicNetwork對象便就是用於網絡請求的,在創建出BasicNetwork對象後,下一步就是將其傳遞給同名的newRequestQueue靜態私有方法:

Volley.java

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

該方法中首先通過創建的DiskBasedCache對象和傳入的Network對象來實例化RequestQueue,並且調用了RequestQueue對象的start方法。先來看看RequestQueue類的構造方法:

RequestQueue.java

private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;
public RequestQueue(Cache cache, Network network) {
    this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
}
public RequestQueue(Cache cache, Network network, int threadPoolSize) {
    this(cache, network, threadPoolSize, new ExecutorDelivery(new Handler(Looper.getMainLooper())));
}
public RequestQueue(Cache cache, Network network, int threadPoolSize, ResponseDelivery delivery) {
    mCache = cache;
    mNetwork = network;
    mDispatchers = new NetworkDispatcher[threadPoolSize];
    mDelivery = delivery;
}

常規調用的RequestQueue類的構造方法最終會調到接收4個參數的重載方法中,方法中只做了全局變量的賦值,並沒有太多邏輯,那麼來看看這幾個參數的意思。

DiskBasedCache      用於保持對磁盤的響應的緩存

Network                     用於執行HTTP請求的網絡接口

threadPoolSize         要創建的網絡調度程序線程數,默認是4,所以說Volley是適合數據量小,通信頻繁的網絡操作

ResponseDelivery    綁定了UI線程Looper的結果分發器

因爲RequestQueue對象創建後會首先調用start方法,所以接下來繼續看看它的start方法源碼:

RequestQueue.java

public void start() {

    // Make sure any currently running dispatchers are stopped.
    stop();
    // 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();
    }
}

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

start方法做了三件事情,首先調用了stop方法進行mCacheDispatcher和mDispatchers的重置,其次是創建CacheDispatcher對象並啓動,最後是for循環創建出N個NetworkDispatcher對象並啓動。CacheDispatcher和NetworkDispatcher都是繼承Thread線程,從註釋中可知,CacheDispatcher是用於緩存調度線程,而NetworkDispatcher是用於網絡請求的線程,我們繼續來看看它們分別具體是幹什麼來着。

3.2.1 CacheDispatcher線程

CacheDispatcher構造方法接收mCacheQueue代表緩存隊列、mNetworkQueue代表網絡請求隊列、mCache和mDelivery是RequestQueue類構造方法傳入的用於保持對磁盤的響應的緩存對象和結果分發器對象。繼續看看該線程內部做了啥事情:

CacheDispatcher.java

@Override
public void run() {
    ……
    while (true) {
        try {
            processRequest();
        } catch (InterruptedException e) {
            ……
        }
    }
}
private void processRequest() throws InterruptedException {
    final Request<?> request = mCacheQueue.take();
    processRequest(request);
}
@VisibleForTesting
void processRequest(final Request<?> request) throws InterruptedException {
    request.addMarker("cache-queue-take");

    // 如果請求取消,則結束流程
    if (request.isCanceled()) {
        request.finish("cache-discard-canceled");
        return;
    }

    // 嘗試從緩存中檢索數據
    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;
    }

    // 命中緩存,解析緩存數據
    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);
        // Mark the response as intermediate.
        response.intermediate = true;

        if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
            // Post the intermediate response back to the user and have
            // the delivery then forward the request along to the network.
            mDelivery.postResponse(
                    request,
                    response,
                    new Runnable() {
                        @Override
                        public void run() {
                            try {
                                mNetworkQueue.put(request);
                            } catch (InterruptedException e) {
                                // Restore the interrupted status
                                Thread.currentThread().interrupt();
                            }
                        }
                    });
        } else {
            // request has been added to list of waiting requests
            // to receive the network response from the first request once it returns.
            mDelivery.postResponse(request, response);
        }
    }
}

CacheDispatcher線程內部是一個死循環,當從緩存隊列mCacheQueue中讀取到Request後,便開始對Request進行處理。從上面代碼註釋中可見,緩存經過了:是否存在、是否過期、新鮮度的判斷,如果緩存獲取失敗則會加入mNetworkQueue網絡請求隊列去,否則就調用ResponseDelivery結果分發器的postResponse分發結果。

3.2.2 NetworkDispatcher線程

NetworkDispatcher構造方法接收mNetworkQueue代表網絡請求隊列、mNetwork、mCache和mDelivery是RequestQueue類構造方法傳入的用於執行HTTP請求的網絡接口、用於保持對磁盤的響應的緩存對象和結果分發器對象。繼續看看該線程內部做了啥事情:

NetworkDispatcher.java

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

        }
    }
}

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

@VisibleForTesting
void processRequest(Request<?> request) {
    long startTimeMs = SystemClock.elapsedRealtime();
    try {
        request.addMarker("network-queue-take");

        // 如果請求取消,則直接返回結果,結束流程
        if (request.isCanceled()) {
            request.finish("network-discard-cancelled");
            request.notifyListenerResponseNotUsable();
            return;
        }

        addTrafficStatsTag(request);

        // 使用mNetwork執行網絡請求.
        NetworkResponse networkResponse = mNetwork.performRequest(request);
        request.addMarker("network-http-complete");

        // 如果服務器返回304,表示沒有改動過,則直接返回結果,結束流程

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

        // 根據傳入的Request子類(StringRequest/JsonRequest…)進行解析出相應的Response對象
        Response<?> response = request.parseNetworkResponse(networkResponse);
        request.addMarker("network-parse-complete");

        // 寫入緩存
        if (request.shouldCache() && response.cacheEntry != null) {
            mCache.put(request.getCacheKey(), response.cacheEntry);
            request.addMarker("network-cache-written");
        }

        // 將結果分發
        request.markDelivered();
        mDelivery.postResponse(request, response);
        request.notifyListenerResponseReceived(response);
    } catch (VolleyError volleyError) {
        volleyError.setNetworkTimeMs(SystemClock.elapsedRealtime() - startTimeMs);
        parseAndDeliverNetworkError(request, volleyError);
        request.notifyListenerResponseNotUsable();
    } 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);
        request.notifyListenerResponseNotUsable();
    }
}

同樣NetworkDispatcher線程內部也是一個死循環,當從網絡請求隊列mNetworkQueue中讀取到Request後,便 開始對Request進行處理。從上面代碼註釋中可見,使用了mNetwork進行了網絡請求、通過parseNetworkResponse方法返回了我們傳入的Request對應類型的結果,並進行緩存寫入,最後調用結果分發器的postResponse分發結果。

3.3 Request的創建

Request是一個抽象類,它的子類有:StringRequest、JsonRequest、ImageRequest等,我們也可以根據自己實際請求後返回的類型進行自定義Request對象。當我們進行自定義Request繼承時,不同的Request最大的差別在於重寫parseNetworkResponse方法,也就是上面NetworkDispatcher線程內部做完網格請求後調用的parseNetworkResponse方法返回對應類型結果。如StringRequest類中源碼:

StringRequest.java

@Override
@SuppressWarnings("DefaultCharset")
protected Response<String> parseNetworkResponse(NetworkResponse response) {
    String parsed;
    try {
        parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
    } catch (UnsupportedEncodingException e) {
        // Since minSdkVersion = 8, we can't call
        // new String(response.data, Charset.defaultCharset())
        // So suppress the warning instead.
        parsed = new String(response.data);
    }
    return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
}

3.4 將Request添加以RquestQueue

當RquestQueue和Request對象都創建好後,最後一步就是將Request對象通過RquestQueue的add方法添加到隊列中去,可見源碼:

RquestQueue.java

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;
    }
    mCacheQueue.add(request);
    return request;
}

當request被添加到mNetworkQueue和mCacheQueue兩個隊列後,便有了3.2.1和3.2.2中兩種線程在死循環讀取自身相應的隊列,然後進行相應的邏輯處理。

4 總結

關於Volley的使用和原理就寫到這裏,Volley的總體設計思路很簡單。雖然它使用場景有夠爲明顯的限制,而且目前比較流行的網絡請求框架還有Okhttp3,但是畢竟學習優秀的框架設計思想還是非常有必要的。更多關於Volley的介紹,請可以前往其官網

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章