Android每週一輪子:Volley

Jensen95發表於2019-03-04

Volley

序言

2018年談Volley,可以說是too yong, too simple了,對於網路庫,現在使用最多的莫過於OkHttp了,接觸使用Volley應該還是大二的時候了。之後也看過其原始碼,但是在不久前面試的時候,被問到一個Volley庫的問題,就是Volley中請求的優先順序是如何排程的,卻卡住了,當時對於原始碼的閱讀大多隻是停留在對於其實現的流程和專案的結構上,而對於其具體的特性和其如何實現了這些特性卻沒有去了解,相比於功能實現的流程,特性的實現細節也是不可忽略的,甚至可以說這才是一個庫的精華之所在,同時對於該網路庫的缺陷在於那裡,通過了解其優勢和缺陷,我們可以更好的揚長避短,充分利用該庫。本著該原則,準備對於之前閱讀的程式碼進行一個重新的回顧,暫定的計劃為一週拆一個輪子。

將以其實現流程,特性實現和其缺陷作為主要切入點,進行程式碼分析。

Volley 基礎使用

final TextView mTextView = (TextView) findViewById(R.id.text);
...

// Instantiate the RequestQueue.
RequestQueue queue = Volley.newRequestQueue(this);
String url ="http://www.google.com";

// Request a string response from the provided URL.
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
            new Response.Listener<String>() {
    @Override
    public void onResponse(String response) {
        // Display the first 500 characters of the response string.
        mTextView.setText("Response is: "+ response.substring(0,500));
    }
}, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {
        mTextView.setText("That didn't work!");
    }
});
// Add the request to the RequestQueue.
queue.add(stringRequest);
複製程式碼
  • 建立RequestQueue 請求佇列。
  • 建立Request,Volley提供了String,JsonObject等型別,使用者可自己繼承Reqeust實現自己定義的返回結果型別。
  • 將請求新增到請求佇列中。

經過以上三步,我們就完成了一次網路請求,在註冊的監聽器的onResponse方法中我們可以拿到請求成功的返回結果和在onErrorResponse方法中得到出錯的資訊。

Volley實現

Volley實現結構圖

  • 請求佇列的建立
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;
}
複製程式碼
  • 請求新增到佇列中
public <T> Request<T> add(Request<T> request) {
    //請求新增到mCurrentRequests中
    request.setRequestQueue(this);
    synchronized (mCurrentRequests) {
        mCurrentRequests.add(request);
    }
    //設定請求的Sequence,後期用來比較請求的優先順序
    request.setSequence(getSequenceNumber());
    request.addMarker("add-to-queue");

    //如果請求不需要快取,直接加入網路請求佇列
    if (!request.shouldCache()) {
        mNetworkQueue.add(request);
        return request;
    }
    //如果需要快取加入到快取佇列中
    mCacheQueue.add(request);
    return request;
 }
複製程式碼
  • 呼叫佇列的開啟
public void start() {
    stop(); 
    //建立快取Dispatcher,並啟動
    mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
    mCacheDispatcher.start();

    // 根據執行緒池設定的數目,建立網路請求Dispatcher,並啟動
    for (int i = 0; i < mDispatchers.length; i++) {
        NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                mCache, mDelivery);
        mDispatchers[i] = networkDispatcher;
        networkDispatcher.start();
    }
}
複製程式碼

這裡首先會關掉之前的Dispatcher,然後重新建立並開啟Dispatcher

  • 建立並開啟CacheDispatcher
public void run() {
    mCache.initialize();
    while (true) {
             //取出請求
            final Request<?> request = mCacheQueue.take();
            if (request.isCanceled()) {
                request.finish("cache-discard-canceled");
                continue;
            }
            //判斷Cache未命中,這加入到網路請求佇列中
            Cache.Entry entry = mCache.get(request.getCacheKey());
            if (entry == null) {
                request.addMarker("cache-miss");
                // Cache miss; send off to the network dispatcher.
                if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
                    mNetworkQueue.put(request);
                }
                continue;
            }
            //快取過期了,拿到快取之後,再將該請求放置到網路請求佇列中
            if (entry.isExpired()) {
                request.addMarker("cache-hit-expired");
                request.setCacheEntry(entry);
            //判斷是否需要加入到等待佇列,如果需要則不加入網路請求佇列
                if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
                    mNetworkQueue.put(request);
                }
                continue;
            }
          //Cache 未過期,Cache命中,這將其包裝成NetworkResponse
            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;

                if (!mWaitingRequestManager.maybeAddToWaitingRequests(request)) {
        
                    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 {
                    mDelivery.postResponse(request, response);
                }
            }
    }
}

複製程式碼

當快取沒有命中的時候,需要發起網路請求,這個時候,通過WaitingRequestManager來進行管理,其維護了一個Map來放置響應的請求,鍵為請求CacheKey,值為請求。如果Map中不包含該CacheKey,這將其加入,並將值置為Null,如果有,則直接將其加入。第一次置為Null的原因是為了防止在請求歸來時,在NetworkDispatcher中執行了一次資料的非同步傳遞,在快取請求佇列處理時再次被處理,通過這種方式也保證了對於同一個請求,只有可能被HttpStack執行一次,而每一個請求設定的成功失敗回撥都會被用到。

private synchronized boolean maybeAddToWaitingRequests(Request<?> request) {
    String cacheKey = request.getCacheKey();
      //如果請求佇列包含該Cachekey,表示已經執行過網路請求
    if (mWaitingRequests.containsKey(cacheKey)) {
        // There is already a request in flight. Queue up.
        List<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
        if (stagedRequests == null) {
            stagedRequests = new ArrayList<Request<?>>();
        }
        request.addMarker("waiting-for-response");
        stagedRequests.add(request);
        mWaitingRequests.put(cacheKey, stagedRequests);
        if (VolleyLog.DEBUG) {
            VolleyLog.d("Request for cacheKey=%s is in flight, putting on hold.", cacheKey);
        }
        return true;
    } else {
        //將Value置為null,因為其回撥在NetworkDispatcher中被執行了
        mWaitingRequests.put(cacheKey, null);
        request.setNetworkRequestCompleteListener(this);
        if (VolleyLog.DEBUG) {
        return false;
    }
}
複製程式碼

通過為該Request設定setNetworkRequestCompleteListener,當網路請求執行完成的時候,其onResponseReceived函式會被回撥到。

public void onResponseReceived(Request<?> request, Response<?> response) {
    if (response.cacheEntry == null || response.cacheEntry.isExpired()) {
        onNoUsableResponseReceived(request);
        return;
    }
    String cacheKey = request.getCacheKey();
    List<Request<?>> waitingRequests;
    synchronized (this) {
        waitingRequests = mWaitingRequests.remove(cacheKey);
    }
    if (waitingRequests != null) {
        // 將等待的請求結果進行傳遞
        for (Request<?> waiting : waitingRequests) {
            mCacheDispatcher.mDelivery.postResponse(waiting, response);
        }
    }
}
複製程式碼

這個時候,排隊的請求的回撥則會被執行。

  • 建立並開啟NetworkDispatcher
public void run() {
    while (true) {
      //取出請求
      Request<?> request = mQueue.take();
        //判斷請求是否需要取消
        if (request.isCanceled()) {
             request.finish("network-discard-cancelled");
             request.notifyListenerResponseNotUsable();
              continue;
         }
        //執行網路請求
         NetworkResponse networkResponse = mNetwork.performRequest(request);
        //解析網路請求結果
         Response<?> response = request.parseNetworkResponse(networkResponse);
        //判斷是否需要加入快取
        if (request.shouldCache() && response.cacheEntry != null) {
             mCache.put(request.getCacheKey(), response.cacheEntry);
             request.addMarker("network-cache-written");
         }
        //通過Delivery將響應結果傳遞出去
        mDelivery.postResponse(request, response);
        request.notifyListenerResponseReceived(response);

  }
}
複製程式碼
  • 請求處理到產生響應

Volley請求處理過程

Volley 特性和缺陷

特性

  • 自動排程網路請求,支援多併發網路請求
  • 透明的磁碟記憶體響應結果快取
  • 支援請求優先順序調整
  • 支援取消請求,並提供相應API
  • 高度可擴充
  • 網路請求結果非同步回撥

缺陷

  • 不適合做大的網路下載請求

接下來,針對Volley的特性和缺陷,分別展開,從原始碼進行分析。

  1. 自動排程網路請求,支援多併發網路請求

在RequestQueue方法中,開啟了多個NetworkDispatcher,而每一個Dispatcher都是一個執行緒。

NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                mCache, mDelivery);
 mDispatchers[i] = networkDispatcher;
networkDispatcher.start();
複製程式碼

對於網路請求通過一個優先順序阻塞佇列存放,每一個執行緒可以從佇列中獲取請求,然後執行,最後將響應結果返回。Volley內部管理了這些執行緒,無需開發者關心。

  1. 透明的磁碟記憶體響應結果快取

對於快取,Volley支援使用者制定自己的快取規則,通過實現Cache介面,實現自己的快取。同時提供了一個預設的快取實現DiskBasedCache。其是如何實現透明的磁碟記憶體響應的呢?

首先對於所有的請求,在判斷為設定了快取的,將會加入到快取佇列中,然後從快取中去取,如果快取中有則返回,如果沒有或者過期則將其加入到網路請求佇列中。

在CacheDispatcher中,我們首先呼叫了Cache的初始化方法

mCache.initialize();
複製程式碼

之後通過請求的CacheKey從Cache的get方法中獲取快取。其內部實現是記憶體中維護了一個LinkedHashMap,以請求的CacheKey作為Key,CacheHeader作為值,對於每一個請求內容通過檔案的形式存放在磁碟檔案中,初始化的時候,讀取快取檔案目錄,將其載入到記憶體中,查詢的時候,根據記憶體中的快取進行判斷,從磁碟中載入資料。

  1. 優先順序實現

Volley支援優先順序的調整,通過一個PriorityblockingQueue佇列,進行排程,將請求加入進來,該佇列會根據其存放的Obejct的compare方法對其進行優先順序的比較,來確定其先後順序。

@Override
public int compareTo(Request<T> other) {
    Priority left = this.getPriority();
    Priority right = other.getPriority();

    // High-priority requests are "lesser" so they are sorted to the front.
    // Equal priorities are sorted by sequence number to provide FIFO ordering.
    return left == right ?
            this.mSequence - other.mSequence :
            right.ordinal() - left.ordinal();
}
複製程式碼

首先根據請求設定的優先順序,然後根據其Sequence值,這個值是在時間順序上遞增的。

  1. 任意的取消網路請求

通過為請求設定cacel欄位,在請求執行的時候,對其進行判斷,來確定是否為取消了,然後再次執行。

  1. 高度可擴充

Volley支援對於Cache的擴充,HttpStack的擴充,也就是對於網路請求具體執行類的擴充,這裡提供了HttpClient和HttpUrlConnection。支援對於網路請求回撥的自定義。支援網路請求解析的自定義。使用者可以根據自己的需求,進行響應的插樁擴充。

  1. 網路請求結果非同步回撥
  • 建立排程器
new ExecutorDelivery(new Handler(Looper.getMainLooper()));
複製程式碼
public ExecutorDelivery(final Handler handler) {
    // Make an Executor that just wraps the handler.
    mResponsePoster = new Executor() {
        @Override
        public void execute(Runnable command) {
            handler.post(command);
        }
    };
}
複製程式碼
  • 傳遞資料
@Override
public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
    request.markDelivered();
    request.addMarker("post-response");
    mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
}
複製程式碼

排程器持有了主執行緒的Handler,通過Handler的post將我們的資料透傳到主執行緒之中。通過這種方式從而實現非同步回撥。

  1. 不適合大資料量傳遞

在返回的結果,通過byte[]欄位中,然後存放在記憶體之中,然後獲取相應的編碼方式,將其轉化為字串,這樣就導致當網路請求數目增加,返回內容增加的時候,導致記憶體中的資料量增加,因此Volley不適合進行大資料量的傳遞,而實現小而頻繁的請求。

new NetworkResponse(statusCode, responseContents, responseHeaders, false,
                        SystemClock.elapsedRealtime() - requestStart);
複製程式碼

這裡的responseContents為一個byte型別陣列。

總結

Volley的原始碼實現比較簡單,程式碼量也不是很大,比較容易閱讀和理解,所以Volley作為成了新年以來拆的第一個輪子。拆輪子的過程可以幫助我們學習到功能的實現,同時也可以從中學習到一些設計思想。接下來的計劃是每週拆一個輪子,可能是Android也可能是其它方面的。時間充裕,可能會找一些程式碼量大的,時間少的話,可能就找一些簡單輪子來拆。拆分思路為其基礎使用,實現原理梳理,特性和缺陷分析。通過這三方面來徹底瞭解一個輪子的實現。

相關文章