Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 我的Android網絡框架之旅(二)

我的Android網絡框架之旅(二)

編輯:關於Android編程

承接上一篇文章,今天我們來探討並發網絡的線程管理。眾所周知在網絡請求中,高並發的多線程網絡請求非常普遍,我們不能因為上一條網絡阻塞影響到其他的網絡請求,然而過多的線程又會耗盡移動端上有限的CPU資源。如何處理多並發操作上,各家的網絡框架多少都有些差異,今天我們就來看一看應該如何選擇。

隊列的選擇方案

網絡請求一般都是采用FIFO的方式進行調度,所以采用隊列來存儲請求任務最合適不過了,在JAVA中比較常用的隊列有以下幾種
1.ArrayBlockingQueue
2.LinkedBlockingQueue
3.PriorityBlockingQueue

讓我們先來普及以下BlockingQueue的特點

多線程環境中,通過隊列可以很容易實現數據共享,比如經典的“生產者”和“消費者”模型中,通過隊列可以很便利地實現兩者之間的數據共享。假設我們有若干生產者線程,另外又有若干個消費者線程。如果生產者線程需要把准備好的數據共享給消費者線程,利用隊列的方式來傳遞數據,就可以很方便地解決他們之間的數據共享問題。但如果生產者和消費者在某個時間段內,萬一發生數據處理速度不匹配的情況呢?理想情況下,如果生產者產出數據的速度大於消費者消費的速度,並且當生產出來的數據累積到一定程度的時候,那麼生產者必須暫停等待一下(阻塞生產者線程),以便等待消費者線程把累積的數據處理完畢,反之亦然。然而,在concurrent包發布以前,在多線程環境下,我們每個程序員都必須去自己控制這些細節,尤其還要兼顧效率和線程安全,而這會給我們的程序帶來不小的復雜度。好在此時,強大的concurrent包橫空出世了,而他也給我們帶來了強大的BlockingQueue。(在多線程領域:所謂阻塞,在某些情況下會掛起線程(即阻塞),一旦條件滿足,被掛起的線程又會自動被喚醒)

接著我們來看看它的三個常用實現子類

ArrayBlockingQueue
基於數組的阻塞隊列實現,在ArrayBlockingQueue內部,維護了一個定長數組,以便緩存隊列中的數據對象。ArrayBlockingQueue在生產者放入數據和消費者獲取數據,都是共用同一個鎖對象,由此也意味著兩者無法真正並行運行。

LinkedBlockingQueue
基於鏈表的阻塞隊列,同ArrayListBlockingQueue類似,在未指定長度的情況下,默認是最大值,這樣的話,如果生產者的速度一旦大於消費者的速度,也許還沒有等到隊列滿阻塞產生,系統內存就有可能已被消耗殆盡了。而LinkedBlockingQueue之所以能夠高效的處理並發數據,還因為其對於生產者端和消費者端分別采用了獨立的鎖來控制數據同步,這也意味著在高並發的情況下生產者和消費者可以並行地操作隊列中的數據,以此來提高整個隊列的並發性能。

PriorityBlockingQueue
基於優先級的阻塞隊列(優先級的判斷通過構造函數傳入的Compator對象來決定),但需要注意的是PriorityBlockingQueue並不會阻塞數據生產者,而只會在沒有可消費的數據時,阻塞數據的消費者。因此使用的時候要特別注意,生產者生產數據的速度絕對不能快於消費者消費數據的速度,否則時間一長,會最終耗盡所有的可用堆內存空間。在實現PriorityBlockingQueue時,內部控制線程同步的鎖采用的是公平鎖。

由上可知,傳統的隊列是線程阻塞的隊列,這就意味著當我們的網絡端上生產者端或消費者端不平衡的時候,就很容易產生線程阻塞。舉個例子,如果用戶在短時間內進行了大量的網絡操作,則消費者端的執行速度會遠遠的大於生產者端的速度,如果使用ArrayBlockingQueue的話,執行效率就會卡在同步鎖進行pull()和take()操作的上面,這樣的線程阻塞是不被接受的。這個時候LinkedBlockingQueue的優勢就遠遠顯示出來了,同一個元素在隊列中使用分離鎖的選擇可以讓LinkedBlockingQueue能夠高效的處理並發數據。
而優先級隊列的使用則體現在另一個場景裡,假設現在用戶進入了一個主頁面需要獲取動態數據,同時後台提交了更新用戶狀態的操作,那麼如果更新狀態被阻塞的話,用戶的主界面數據就會遲遲刷新不出來,這樣的用戶體驗就會變得很差。所以在進行網絡請求的隊列選擇上,綜合考慮到移動端的並發效率和網絡請求的優先級,PriorityBlockingQueue和LinkedBlockingQueue的結合體才是最完美的解決方案。

多線程的選擇方案

在資源匮乏的移動端,性能優化是一個瓶頸。說到多線程並發操作,大家第一時間想到的一定是線程池。使用線程池可以減少在創建和銷毀線程上所花的時間以及系統資源的開銷 。固定數量的線程池可以有效的防止內存資源被過度消耗殆盡,為UI線程爭取更多的資源。在Android的源碼中,已經有了最佳實踐的模板,我也就不在這裡多贅述,上AsynckTask的源碼!

 private static final String LOG_TAG = "AsyncTask";

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final int KEEP_ALIVE = 1;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);

        public Thread newThread(Runnable r) {
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };

    private static final BlockingQueue sPoolWorkQueue =
            new LinkedBlockingQueue(128);

    /**
     * An {@link Executor} that can be used to execute tasks in parallel.
     */
    public static final Executor THREAD_POOL_EXECUTOR
            = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
                    TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory);

高並發操作的場景下使用線程池來管理線程的確是不錯的實踐方案,並且我本人是這樣做的。但是在我們查看Volley的源碼時候會發現,Volley其實並沒有用到線程池,而是自己維護了一個長度為4的數組進行多線程的管理。

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

我們可以看到,在進行網絡請求分配的時候,從隊列裡取出的網絡任務並沒有被交給線程池去分配資源,而是被交給一個初始化好的線程數組進行控制,那麼networkDispatcher裡面到底做了什麼呢?

public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        Request request;
        while (true) {
            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;
                }

                // Tag the request (if API >= 14)
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
                    TrafficStats.setThreadStatsTag(request.getTrafficStatsTag());
                }

                // 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) {
                parseAndDeliverNetworkError(request, volleyError);
            } catch (Exception e) {
                VolleyLog.e(e, Unhandled exception %s, e.toString());
                mDelivery.postError(request, new VolleyError(e));
            }
        }
}

在上面我們提到過,BlockingQueue 是一個線程阻塞的隊列,所以當隊列為空時,消費者線程會一直阻塞等待網絡任務的提交,所以在 while (true) {}中,request = mQueue.take();這段代碼就是一個線程阻塞的操作,mQuit用來控制線程結束釋放資源,如果網絡請求沒有被Cancel,最後會執行mDelivery.postResponse(request, response)進行將結果回調傳遞到主線程。整個過程中我們都並沒有看見ThreadPoolExecutor的身影。為什麼要使用數組而不是線程池呢?
我們先來看一看線程池的調度規則ThreadPoolExecutor使用介紹

當一個任務通過execute(Runnable)方法欲添加到線程池時:

如果此時線程池中的數量小於corePoolSize,即使線程池中的線程都處於空閒狀態,也要創建新的線程來處理被添加的任務。

如果此時線程池中的數量等於 corePoolSize,但是緩沖隊列 workQueue未滿,那麼任務被放入緩沖隊列。

如果此時線程池中的數量大於corePoolSize,緩沖隊列workQueue滿,並且線程池中的數量小於maximumPoolSize,建新的線程來處理被添加的任務。

如果此時線程池中的數量大於corePoolSize,緩沖隊列workQueue滿,並且線程池中的數量等於maximumPoolSize,那麼通過handler所指定的策略來處理此任務。也就是:處理任務的優先級為:核心線程corePoolSize、任務隊列workQueue、最大線程maximumPoolSize,如果三者都滿了,使用handler處理被拒絕的任務。

當線程池中的線程數量大於
corePoolSize時,如果某線程空閒時間超過keepAliveTime,線程將被終止。這樣,線程池可以動態的調整池中的線程數。

在線程池裡,如果線程池中的數量小於corePoolSize,即使線程池中的線程都處於空閒狀態,也要創建新的線程來處理被添加的任務,這就意味著當corePoolSize未滿時,會出現進程資源被浪費的情況。相比於運行時申請線程資源,初始化時分配線程資源反而可以節省創建開支。所以這樣看來,Volley使用一個 mDispatchers[i] 來管理網絡請求的多線程,也不乏是一種優化方案。

在這裡單開一篇文章講述了多線程的處理,總結一下在編寫網絡框架的並發處理時我們需要考慮到的情況有以下三點:
1.網絡請求的高並發線程執行效率
2.請求隊列的優先級處理和取消隊列
3.多線程的資源分配與釋放

下一篇我將詳細描述如何使用httpUrlConnection進行請求頭的封裝,數據報文的拼接,帶你了解RFC文檔下定義的幾種常見網絡請求協議。

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved