遲到的Volley原始碼解析

許佳佳233發表於2017-04-28

#前言
筆者其實之前其實早就想對Volley原始碼解析,但是由於筆者基礎不好,理解原始碼有一定困難,加上不夠重視,於是便不了了之了。
在近期筆試面試的經歷中,發現網路實踐這一塊實在重要,於是打算亡羊補牢。也是祭奠下失去的騰訊實習offer(三面+加面,掛在加面),我們無法責怪運氣,能做的只是提高自我。

參考文章:

Volley 原始碼解析
Android Volley完全解析(四),帶你從原始碼的角度理解Volley

#簡單使用
關於Volley的簡單使用主要以下三步:
1、獲取RequestQueue物件,如下

RequestQueue mQueue = Volley.newRequestQueue(context);  

2、定義Request,需要傳入URL,伺服器響應成功的回撥和伺服器響應失敗的回撥等。具體的有StringRequest,JsonRequest等,如下:

StringRequest stringRequest = new StringRequest("http://www.baidu.com",  
                        new Response.Listener<String>() {  
                            @Override  
                            public void onResponse(String response) {  
                                Log.d("TAG", response);  
                            }  
                        }, new Response.ErrorListener() {  
                            @Override  
                            public void onErrorResponse(VolleyError error) {  
                                Log.e("TAG", error.getMessage(), error);  
                            }  
                        });  

3、將Request物件新增到RequestQueue裡面,如下:

mQueue.add(stringRequest); 

在此就不多做累述,如果是初學Volley的讀者,推薦看下郭霖前輩的部落格:
Android Volley完全解析(一),初識Volley的基本用法

#初始化:newRequestQueue()
每次使用Volley第一步我們就是定義一個RequestQueue 物件,如下:

RequestQueue mQueue = Volley.newRequestQueue(context);  

我們看一下這個方法裡面做了什麼:

 public static RequestQueue newRequestQueue(Context context) {
        return newRequestQueue(context, null);
    }
 public static RequestQueue newRequestQueue(Context context, HttpStack stack) {
        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 = new RequestQueue(new DiskBasedCache(cacheDir), network);
        queue.start();

        return queue;
    }

首先了解下什麼是HttpStack。
**HttpStack:**處理 Http 請求,返回請求結果。目前 Volley 中有基於 HttpURLConnection 的HurlStack和 基於 Apache HttpClient 的HttpClientStack。
在原始碼中我們可以看到,如果手機系統版本號是大於9的,則建立一個HurlStack的例項,否則就建立一個HttpClientStack的例項。而HurlStack的內部就是使用HttpURLConnection進行網路通訊的,HttpClientStack的內部則是使用HttpClient進行網路通訊的。

然後,通過HttpStack生成BasicNetwork物件,接著把BasicNetwork物件傳遞到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();
        }
    }

此處主要是mCacheDispatcher和NetworkDispatcher,他們都是繼承的Thread類。mCacheDispatcher是快取排程的執行緒,NetworkDispatcher是網路排程的執行緒。他們的具體實現等會再說,我們先看RequestQueue.start()中的邏輯。

首先,我們可以看到新建了一個mCacheDispatcher物件,並且呼叫了start()方法。
然後根據mDispatchers的長度新建了幾個NetworkDispatcher物件並且開啟。具體的mDispatchers的長度,我們可以在原始碼中找到:

 public RequestQueue(Cache cache, Network network, int threadPoolSize,
            ResponseDelivery delivery) {
        mCache = cache;
        mNetwork = network;
        mDispatchers = new NetworkDispatcher[threadPoolSize];
        mDelivery = delivery;
    }
 public RequestQueue(Cache cache, Network network) {
        this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
    }
    /** Number of network request dispatcher threads to start. */
    private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;

可以看到在預設情況下,NetworkDispatcher也就是網路排程執行緒會開啟4個,當然我們也可以自定義開啟的個數。

所以預設是開啟5個執行緒,即5個執行緒併發,1個快取排程執行緒,4個網路排程執行緒。

然後我們就看一下NetworkDispatcher和CacheDispatcher分別幹了什麼吧。
#網路排程執行緒:NetworkDispatcher

@Override
    public void run() {
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
        while (true) {
            long startTimeMs = SystemClock.elapsedRealtime();
            Request<?> request;
            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);
            }
        }
    }

主要邏輯:
1、讓執行緒優先順序低於一般的,可以減少對使用者介面的影響。
2、啟動後會不斷從網路請求佇列中取請求處理,佇列為空則等待
3、通過performRequest(),取到請求後會執行,並返回執行結果,這裡的請求就是我們之前定義的StringRequest,JsonRequest等等。
4、通過parseNetworkResponse(),會解析返回的請求結果,並且返回解析後的結果。
5、解析結束後,檢視結果是否需要快取並且是否已經快取,如果需要快取,並且沒有快取的,那麼就快取。
6、如果是針對資料量大,並且訪問不頻繁甚至一次的請求,我們可以設定不需要快取,在這裡就不會快取。
7、最終把結果傳遞給ResponseDelivery去執行後續處理。

#快取排程執行緒:CacheDispatcher

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

        // Make a blocking call to initialize the cache.
        mCache.initialize();

        while (true) {
            try {
                // Get a request from the cache triage queue, blocking until
                // at least one is available.
                final Request<?> request = mCacheQueue.take();
                request.addMarker("cache-queue-take");

                // If the request has been canceled, don't bother dispatching it.
                if (request.isCanceled()) {
                    request.finish("cache-discard-canceled");
                    continue;
                }

                // Attempt to retrieve this item from cache.
                Cache.Entry entry = mCache.get(request.getCacheKey());
                if (entry == null) {
                    request.addMarker("cache-miss");
                    // Cache miss; send off to the network dispatcher.
                    mNetworkQueue.put(request);
                    continue;
                }

                // If it is completely expired, just send it to the network.
                if (entry.isExpired()) {
                    request.addMarker("cache-hit-expired");
                    request.setCacheEntry(entry);
                    mNetworkQueue.put(request);
                    continue;
                }

                // We have a cache hit; parse its data for delivery back to the request.
                request.addMarker("cache-hit");
                Response<?> response = request.parseNetworkResponse(
                        new NetworkResponse(entry.data, entry.responseHeaders));
                request.addMarker("cache-hit-parsed");

                if (!entry.refreshNeeded()) {
                    // Completely unexpired cache hit. Just deliver the response.
                    mDelivery.postResponse(request, response);
                } else {
                    // Soft-expired cache hit. We can deliver the cached response,
                    // but we need to also send the request to the network for
                    // refreshing.
                    request.addMarker("cache-hit-refresh-needed");
                    request.setCacheEntry(entry);

                    // Mark the response as intermediate.
                    response.intermediate = true;

                    // 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) {
                                // Not much we can do about this.
                            }
                        }
                    });
                }

            } catch (InterruptedException e) {
                // We may have been interrupted because it was time to quit.
                if (mQuit) {
                    return;
                }
                continue;
            }
        }
    }

主要邏輯:
1、不斷從快取請求佇列中取請求處理,佇列為空則等待
3、當結果未快取過、快取失效或快取需要重新整理的情況下,就將該請求放入網路請求佇列,在NetworkDispatcher中進行排程處理。
2、如果獲取到了快取,快取請求處理結果傳遞給ResponseDelivery去執行後續處理。
#快取資料結構
在上面兩個排程執行緒中,我們可以看到快取物件是mCache,一層一層向上找,我們最終可以在newRequestQueue看到這行程式碼:

RequestQueue queue = new RequestQueue(new DiskBasedCache(cacheDir), network);

DiskBasedCache就是Volley中該快取的資料結構:

public class DiskBasedCache implements Cache {

    /** Map of the Key, CacheHeader pairs */
    private final Map<String, CacheHeader> mEntries =
            new LinkedHashMap<String, CacheHeader>(16, .75f, true);

僅僅看前幾行,我們就可以知道它是通過LinkedHashMap這個雙向連結串列進行快取的。初始長度為16,負載因子為0.75。

#在主執行緒響應:ResponseDelivery
NetworkDispatcher和CacheDispatcher最終都將解析後的資料交給了mDelivery處理,那麼這是什麼呢?最終我們可以發現是在RequestQueue這個方法中定義的,如下:

 public RequestQueue(Cache cache, Network network, int threadPoolSize) {
        this(cache, network, threadPoolSize,
                new ExecutorDelivery(new Handler(Looper.getMainLooper())));
    }
    public RequestQueue(Cache cache, Network network) {
        this(cache, network, DEFAULT_NETWORK_THREAD_POOL_SIZE);
    }

我們呼叫的時候使用的是後一個方法,但是會跳轉到第一個,於是我們知道mDelivery其實就是一個ExecutorDelivery物件,於是我們來看下它的原始碼:

public class ExecutorDelivery implements ResponseDelivery {
    /** Used for posting responses, typically to the main thread. */
    private final Executor mResponsePoster;

    /**
     * Creates a new response delivery interface.
     * @param handler {@link Handler} to post responses on
     */
    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);
            }
        };
    }

    /**
     * Creates a new response delivery interface, mockable version
     * for testing.
     * @param executor For running delivery tasks
     */
    public ExecutorDelivery(Executor executor) {
        mResponsePoster = executor;
    }

    @Override
    public void postResponse(Request<?> request, Response<?> response) {
        postResponse(request, response, null);
    }

    @Override
    public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
        request.markDelivered();
        request.addMarker("post-response");
        mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
    }

    @Override
    public void postError(Request<?> request, VolleyError error) {
        request.addMarker("post-error");
        Response<?> response = Response.error(error);
        mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, null));
    }

    /**
     * A Runnable used for delivering network responses to a listener on the
     * main thread.
     */
    @SuppressWarnings("rawtypes")
    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();
            }
       }
    }
}

原始碼雖多,但是功能幾句話就可以描述:
它會在 Handler 對應執行緒中傳輸快取排程執行緒或者網路排程執行緒中產生的請求結果或請求錯誤,會在請求成功的情況下呼叫 Request.deliverResponse(…) 函式,失敗時呼叫 Request.deliverError(…) 函式。

當我們去檢視deliverResponse和deliverError的時候,可以發現他們都是Request中的方法,然後我們可以把JsonRequest當做例子看一下原始碼:

public abstract class JsonRequest<T> extends Request<T> {
    /** Default charset for JSON request. */
    protected static final String PROTOCOL_CHARSET = "utf-8";

    /** Content type for request. */
    private static final String PROTOCOL_CONTENT_TYPE =
        String.format("application/json; charset=%s", PROTOCOL_CHARSET);

    private final Listener<T> mListener;
    private final String mRequestBody;

    /**
     * Deprecated constructor for a JsonRequest which defaults to GET unless {@link #getPostBody()}
     * or {@link #getPostParams()} is overridden (which defaults to POST).
     *
     * @deprecated Use {@link #JsonRequest(int, String, String, Listener, ErrorListener)}.
     */
    public JsonRequest(String url, String requestBody, Listener<T> listener,
            ErrorListener errorListener) {
        this(Method.DEPRECATED_GET_OR_POST, url, requestBody, listener, errorListener);
    }

    public JsonRequest(int method, String url, String requestBody, Listener<T> listener,
            ErrorListener errorListener) {
        super(method, url, errorListener);
        mListener = listener;
        mRequestBody = requestBody;
    }

    @Override
    protected void deliverResponse(T response) {
        mListener.onResponse(response);
    }

    @Override
    abstract protected Response<T> parseNetworkResponse(NetworkResponse response);

    /**
     * @deprecated Use {@link #getBodyContentType()}.
     */
    @Override
    public String getPostBodyContentType() {
        return getBodyContentType();
    }

    /**
     * @deprecated Use {@link #getBody()}.
     */
    @Override
    public byte[] getPostBody() {
        return getBody();
    }

    @Override
    public String getBodyContentType() {
        return PROTOCOL_CONTENT_TYPE;
    }

    @Override
    public byte[] getBody() {
        try {
            return mRequestBody == null ? null : mRequestBody.getBytes(PROTOCOL_CHARSET);
        } catch (UnsupportedEncodingException uee) {
            VolleyLog.wtf("Unsupported Encoding while trying to get the bytes of %s using %s",
                    mRequestBody, PROTOCOL_CHARSET);
            return null;
        }
    }
}

原始碼雖多,重點就一個,如下:


    public JsonRequest(int method, String url, String requestBody, Listener<T> listener,
            ErrorListener errorListener) {
        super(method, url, errorListener);
        mListener = listener;
        mRequestBody = requestBody;
    }

    @Override
    protected void deliverResponse(T response) {
        mListener.onResponse(response);
    }

我們在ExecutorDelivery中,在請求成功時呼叫的deliverResponse()方法,其實就是我們開始定義Request(如JsonRequest、StringRequest等)中所定義的Listener.onResponse()方法。

如此其實Volley原始碼的解析就基本結束了。

#總結:

最終我們再根據Volley請求流程圖概括一下:
這裡寫圖片描述

##流程:
1、呼叫RequestQueue的add()方法來新增一條請求。
2、請求會先被加入到快取佇列當中,快取排程執行緒會從中獲取請求。如果獲取到了,就會解析並且返回主執行緒響應。如果獲取不到,就會將這條請求放入網路請求佇列。
3、網路排程執行緒會從網路請求佇列獲取到請求然後處理,傳送HTTP請求,解析響應結果,寫入快取,返回主執行緒響應。

##要點:
1、快取排程執行緒只有一個,不可自定義.
2、網路排程執行緒預設有4個,可以自定義個數。
3、快取排程執行緒和網路排程執行緒都是使用while(true)不斷從佇列中讀取請求,但是不是死迴圈,當佇列中沒有請求的時候,會等待。
4、Volley是預設所有請求都會快取的,但是針對資料量大,並且訪問不頻繁甚至一次的請求,我們可以設定不需要快取。
5、快取使用的資料結構是雙向連結串列LinkedHashmap。

相關文章