Volley框架之三 疑難實現

有了前面兩章做基礎,這篇文章主要分析一下,從Volley中我學到的知識點

1 .Requset是在哪裏處理的?主線程還是子線程?

子線程處理Request,只要new了一個RequestQueue那麼就會開啓1個緩存線程,4個網絡請求線程,CacheDispatcher/NetworkDispatcher繼承自Thread。
這就意味着一次最多隻能併發5個線程,如果緩存線程沒有命中,那麼最多併發4個網絡請求線程。

    /**
     * Starts the dispatchers in this queue.
     */
    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();
        }
    }

從這裏可以看出,如果有這麼一個需求,需要同時併發4個以上下載任務,那麼就需要重寫Volley的newRequestQueue靜態方法去調用RequestQueue的重載構造函數了
public RequestQueue(Cache cache, Network network, int threadPoolSize)
//threadPoolSize就是最大併發的網絡請求線程數了

但是另一方面,緩存線程的個數始終是1個。

這裏其實挺奇怪的,爲什麼不用線程池來管理緩存線程/網絡請求線程。畢竟new一個RequestQueue就會生成5個線程,很耗資源的,同時如果忘記調用RequestQueue的stop/callall方法,那麼這幾個線程就一直在運行,,,,

2.緩存線程/網絡請求線程怎麼通信?

線程之間通信,第一想到的就是handler機制,看看是不是
緩存線程/網絡請求線程解析完響應之後,執行mDelivery.postResponse(request, response);分發到main線程中去

    /** 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);
            }
        };
    }
    @Override
    public void postResponse(Request<?> request, Response<?> response, Runnable runnable) {
        request.markDelivered();
        request.addMarker("post-response");
        mResponsePoster.execute(new ResponseDeliveryRunnable(request, response, runnable));
    }
    private class ResponseDeliveryRunnable implements Runnable {
        .........
        @SuppressWarnings("unchecked")
        @Override
        public void run() {
            .........
            if (mResponse.isSuccess()) {
                mRequest.deliverResponse(mResponse.result);
            } else {
                mRequest.deliverError(mResponse.error);
            }
            .......
        }
    }
    public RequestQueue(Cache cache, Network network, int threadPoolSize) {
        this(cache, network, threadPoolSize,
                new ExecutorDelivery(new Handler(Looper.getMainLooper())));
    }

mResponsePoster是個Executor對象,匿名實現run方法,run方法內部通過handler機制把ResponseDeliveryRunnable發送到main線程中。handler的初始化new Handler(Looper.getMainLooper())拿的是main線程的looper。這樣的處理方式是不是很熟悉,Executor在AsynaTask中就使用過,這裏爲什麼不直接使用Handler呢?而是通過ExecutorDelivery把handler封裝了起來,覺得是代碼設計的考慮吧,
ExecutorDelivery一看就是用來分發用的,如果直接使用handler代碼結構明顯不夠優雅,這就爲以後我寫代碼提供了一種良好的思路習慣。

3.對同一個Request的重複請求怎麼處理的?

        for (int i = 0; i < 3; i++) {
            mQueue.add(stringRequest);
        }

比如我此時對同一個stringRequest同一時間提交了3次,難道是網絡請求3次嗎?這樣肯定是不合理的,應該是這樣的,第一次add的時候交給緩存線程處理,此時肯定是直接交給網絡請求線程發起網絡請求的,之後add的時候,並不直接添加到mCacheQueue隊列中,而是先等第一個add處理完畢之後,然後再把之後的stringRequest交給緩存線程處理。這個時候緩存線程再去判斷是否是從硬盤緩存中讀取還是交給網絡請求線程處理。這樣就節省了資源啊,良好的設計,下面看看怎麼實現的。

    private final Map<String, Queue<Request<?>>> mWaitingRequests =
            new HashMap<String, Queue<Request<?>>>();
    private final Set<Request<?>> mCurrentRequests = new HashSet<Request<?>>();
    private final PriorityBlockingQueue<Request<?>> mCacheQueue =
            new PriorityBlockingQueue<Request<?>>();
    private final PriorityBlockingQueue<Request<?>> mNetworkQueue =
            new PriorityBlockingQueue<Request<?>>();

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

        // Insert request into stage if there's already a request with the same cache key in flight.
        synchronized (mWaitingRequests) {
            String cacheKey = request.getCacheKey();
            if (mWaitingRequests.containsKey(cacheKey)) {
                // There is already a request in flight. Queue up.
                Queue<Request<?>> stagedRequests = mWaitingRequests.get(cacheKey);
                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 {
                // Insert 'null' queue for this cacheKey, indicating there is now a request in
                // flight.
                mWaitingRequests.put(cacheKey, null);
                mCacheQueue.add(request);
            }
            return request;
        }
    }

    /**
     * Called from {@link Request#finish(String)}, indicating that processing of the given request
     * has finished.
     * <p>
     * <p>Releases waiting requests for <code>request.getCacheKey()</code> if
     * <code>request.shouldCache()</code>.</p>
     */
    <T> void finish(Request<T> request) {
        // Remove from the set of requests currently being processed.
        synchronized (mCurrentRequests) {
            mCurrentRequests.remove(request);
        }
        synchronized (mFinishedListeners) {
            for (RequestFinishedListener<T> listener : mFinishedListeners) {
                listener.onRequestFinished(request);
            }
        }

        if (request.shouldCache()) {
            synchronized (mWaitingRequests) {
                String cacheKey = request.getCacheKey();
                Queue<Request<?>> waitingRequests = mWaitingRequests.remove(cacheKey);
                if (waitingRequests != null) {
                    if (VolleyLog.DEBUG) {
                        VolleyLog.v("Releasing %d waiting requests for cacheKey=%s.",
                                waitingRequests.size(), cacheKey);
                    }
                    // Process all queued up requests. They won't be considered as in flight, but
                    // that's not a problem as the cache has been primed by 'request'.
                    mCacheQueue.addAll(waitingRequests);
                }
            }
        }
    }   

mCurrentRequest首先保存的是當前正在進行的Request,是個HashSet類型,Set不能有重複元素,所以上面add了三次,最後其實mCurrentRequest中只有一個StringRequest
mWaitingRequests看看註釋Staging area for requests that already have a duplicate request in flight.就是說如果有重複的Ruquest正在進行,那麼把之後的Request保存在這個Map中, HashMap

4. 爲什麼add就開始處理Request了?

現在考慮一般情況,先添加到mCacheQueue

    private final PriorityBlockingQueue<Request<?>> mCacheQueue =
            new PriorityBlockingQueue<Request<?>>();

mCacheQueue是個PriorityBlockingQueue類型對象,AsyncTask中還記得嗎?使用的是LinkedBlockingQueue。。他們都是阻塞隊列,他們都有一個特性:
take沒有拿到數據項的話,線程就會一直會阻塞,直到有數據項被add或者put進來。同時take方法內部實現也有鎖,所以多線程take不會出現問題

java.util.concurrent.BlockingQueue的特性是:
1. 當隊列是空的時,從隊列中獲取或刪除元素的操作將會被阻塞,或者當隊列是滿時,往隊列裏添加元素的操作會被阻塞。
2. 阻塞隊列不接受空值,當你嘗試向隊列中添加空值的時候,它會拋出NullPointerException。
3. 阻塞隊列的實現都是線程安全的,所有的查詢方法都是原子的並且使用了內部鎖或者其他形式的併發控制。
4. BlockingQueue 接口是java collections框架的一部分,它主要用於實現生產者-消費者問題

現在來看看PriorityBlockingQueue類的獨有特性,顧名思義優先級隊列,優先級高的先被take出來,PriorityBlockingQueue裏面存儲的對象必須是實現Comparable接口。優先級隊列通過這個接口的compare方法確定對象的priority
Request.java

    public enum Priority {
        LOW,
        NORMAL,
        HIGH,
        IMMEDIATE
    }
    /**
     * Returns the {@link Priority} of this request; {@link Priority#NORMAL} by default.
     */
    public Priority getPriority() {
        return Priority.NORMAL;
    }
    /**
     * Our comparator sorts from high to low priority, and secondarily by
     * sequence number to provide FIFO ordering.
     */
    @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();
    }

getPriority默認是Priority.NORMAL,如果getPriority相同,則比較mSequence字段,這個字段每次add一次,就新+1一次,此時FIFO,先進先出,先處理先add的那個request。
ImageRequest.java

    @Override
    public Priority getPriority() {
        return Priority.LOW;
    }

ImageRequest的優先級比默認的還低,此時說明如果有其它的Request和圖片加載的Request,那麼優先處理其它的Request

CacheDispatcher.java

    @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();

        Request<?> request;
        //無限循環
        while (true) {
            // release previous request object to avoid leaking request object when mQueue is drained.
            request = null;
            try {
                // Take a request from the queue.
                request = mCacheQueue.take();
            } catch (InterruptedException e) {
                // We may have been interrupted because it was time to quit.
                if (mQuit) {
                    //調用quit方法才退出while循環,緩存線程終止
                    return;
                }
                continue;
            }
            try {
                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()) { //緩存是新鮮的,那麼分發給main線程
                    // 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;

                    //如果不是新鮮的,那麼首先把從緩存中讀取的response分發出去給main線程,同時還要交給網絡請求線程發起網絡請求,這樣做是爲了符合http協議規定吧
                    // Post the intermediate response back to the user and have
                    // the delivery then forward the request along to the network.
                    final Request<?> finalRequest = request;
                    mDelivery.postResponse(request, response, new Runnable() {
                        @Override
                        public void run() {
                            try {
                                mNetworkQueue.put(finalRequest);
                            } catch (InterruptedException e) {
                                // Not much we can do about this.
                            }
                        }
                    });
                }
            } catch (Exception e) {
                VolleyLog.e(e, "Unhandled exception %s", e.toString());
            }
        }

CacheDispatcher的run方法可以說是整個Volley最重要的一個方法了
從優先級隊列中拿到Request,request = mCacheQueue.take();/request = mQueue.take(); 如果mCacheQueue沒有Request,那麼就一直被阻塞在這裏了。
request.parseNetworkResponse 解析緩存/網絡數據 封裝成response。
mDelivery.postResponse 分發到main線程。

5.怎麼實現緩存機制?

寫如硬盤緩存

                //NetworkDispatcher的run方法
                // 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);

此時通過把request.getCacheKey()當作key,生成文件名。entry.data是一個字節數組byte[]寫入文件

    //DiskBasedCache的put方法
    @Override
    public synchronized void put(String key, Entry entry) {
        pruneIfNeeded(entry.data.length);
        File file = getFileForKey(key);
        try {
            BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
            CacheHeader e = new CacheHeader(key, entry);
            boolean success = e.writeHeader(fos);
            if (!success) {
                fos.close();
                VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
                throw new IOException();
            }
            fos.write(entry.data);
            fos.close();
            putEntry(key, e);
            return;
        } catch (IOException e) {
        }
        boolean deleted = file.delete();
        if (!deleted) {
            VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
        }
    }

BufferedOutputStream緩存輸出流包裝FileOutputStream寫入文件

讀硬盤緩存

                Cache.Entry entry = mCache.get(request.getCacheKey());

                // 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");

查找緩存目錄是否有request.getCacheKey()該文件,有的話直接用entry.data(字節數組)和entry.responseHeaders(Map對象)做參數包裝成NetworkResponse分發到不同的Request去解析

//parseNetworkResponse方法的實現
//StringRequest
    @Override
    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        String parsed;
        try {
            parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
        } catch (UnsupportedEncodingException e) {
            parsed = new String(response.data);
        }
        return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
    }
//ImageRequest
    @Override
    protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
        // Serialize all decode on a global lock to reduce concurrent heap usage.
        synchronized (sDecodeLock) {
            try {
                return doParse(response);
            } catch (OutOfMemoryError e) {
                VolleyLog.e("Caught OOM for %d byte image, url=%s", response.data.length, getUrl());
                return Response.error(new ParseError(e));
            }
        }
    }
Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);

NetworkResponse.java
BasicNetwork中方法 performRequest 的返回值,Request的 parseNetworkResponse(…) 方法入參,是 Volley 中用於內部 Response 轉換的一級。
封裝了網絡請求響應的 StatusCode,Headers 和 Body 等。
(1). 成員變量
int statusCode Http 響應狀態碼
byte[] data Body 數據
Map

       /** True if the entry is expired. */
        public boolean isExpired() {
            return this.ttl < System.currentTimeMillis();
        }

        /** True if a refresh is needed from the original data source. */
        public boolean refreshNeeded() {
            return this.softTtl < System.currentTimeMillis();
        }

parseCacheHeaders方法返回Cache.Entry對象,ttl,softTtl都會設置

DiskBasedCache繼承 Cache 類,基於 Disk 的緩存實現類
public synchronized void initialize() 初始化,掃描緩存目錄得到所有緩存數據摘要信息放入內存。
public synchronized Entry get(String key) 從緩存中得到數據。先從摘要信息中得到摘要信息,然後讀取緩存數據文件得到內容。
public synchronized void put(String key, Entry entry) 將數據存入緩存內。先檢查緩存是否會滿,會則先刪除緩存中部分數據,然後再新建緩存文件。
private void pruneIfNeeded(int neededSpace) 檢查是否能再分配 neededSpace 字節的空間,如果不能則刪除緩存中部分數據。
public synchronized void clear() 清空緩存。 public synchronized void remove(String key) 刪除緩存中某個元素。

回頭再去看CacheDispatcher的run方法,是不是很清楚了。
entry.isExpired()/entry.refreshNeeded() 都是根據服務器響應頭中獲取設置的,並不是我們本地寫死的,符合Http規範

關鍵來看HttpHeaderParser.parseCacheHeaders方法
通過網絡響應中的緩存控制Header和Body內容,構建緩存實體。如果Header的 Cache-Control 字段含有no-cache或no-store表示不緩存,返回 null,那麼不加入硬盤緩存,下次直接進行網絡請求。
如果不包含,那麼就會根據Header中的其他字段去計算ttl和softTtl,具體計算方法,見parseCacheHeaders方法。這是符合Http規範來做的

試一下,add同一個StringRequest(www.baidu.com)三次
可以發現結果:

所以雖然是同一個StringRequest但是還是會請求網絡三次,而不是用緩存

總結一下:
根據進行請求時服務器返回的緩存控制Header對請求結果進行緩存,下次請求時判斷如果沒有過期就直接使用緩存加快響應速度,如果需要會再次請求服務器進行刷新,如果服務器返回了304,表示請求的資源自上次請求緩存後還沒有改變,這種情況就直接用緩存不用再次刷新頁面,不過這要服務器支持了。

6.怎麼斷線重連,怎麼設置超時時間

    public static final int DEFAULT_TIMEOUT_MS = 2500;
    public static final int DEFAULT_MAX_RETRIES = 0;
    public static final float DEFAULT_BACKOFF_MULT = 1f;
    myRequest.setRetryPolicy(new DefaultRetryPolicy(
                MY_SOCKET_TIMEOUT_MS, 
                DefaultRetryPolicy.DEFAULT_MAX_RETRIES, 
                DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));

    public DefaultRetryPolicy() {
        this(DEFAULT_TIMEOUT_MS, DEFAULT_MAX_RETRIES, DEFAULT_BACKOFF_MULT);
    }
    /**
     * Constructs a new retry policy.
     * @param initialTimeoutMs The initial timeout for the policy.
     * @param maxNumRetries The maximum number of retries.
     * @param backoffMultiplier Backoff multiplier for the policy.
     */
    public DefaultRetryPolicy(int initialTimeoutMs, int maxNumRetries, float backoffMultiplier) {
        mCurrentTimeoutMs = initialTimeoutMs;
        mMaxNumRetries = maxNumRetries;
        mBackoffMultiplier = backoffMultiplier;
    }
    @Override
    public void retry(VolleyError error) throws VolleyError {
        mCurrentRetryCount++;
        mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier); //超時時間計算方法
        if (!hasAttemptRemaining()) {
            throw error; //沒有重試機會則拋出VolleyError異常
        }
    }

可以看出默認超時時間2.5s 不會去重試
myRequest.setRetryPolicy(new DefaultRetryPolicy(3000,2,2));
第一次網絡請求超時時間3s,第二次重試超時時間:3+3*2=9s,第三次重試超時時間:9+9*2=27s
重試發起時期,超時之後立即發起

怎麼實現的超時重連
NetworkDispatcher.run->BasicNetwork.performRequest->HurlStack/HttpClientStack(api<9).performRequest
BasicNetwork的performRequest方法,是個true循環

    @Override
    public NetworkResponse performRequest(Request<?> request) throws VolleyError {
        long requestStart = SystemClock.elapsedRealtime();
        while (true) {
            HttpResponse httpResponse = null;
            byte[] responseContents = null;
            Map<String, String> responseHeaders = Collections.emptyMap();
            try {
                // Gather headers.
                Map<String, String> headers = new HashMap<String, String>();
                addCacheHeaders(headers, request.getCacheEntry());
                httpResponse = mHttpStack.performRequest(request, headers);
                StatusLine statusLine = httpResponse.getStatusLine();
                int statusCode = statusLine.getStatusCode();

                responseHeaders = convertHeaders(httpResponse.getAllHeaders());
                // Handle cache validation.
                if (statusCode == HttpStatus.SC_NOT_MODIFIED) { //304狀態碼,說明資源沒有被修改

                    Entry entry = request.getCacheEntry();
                    if (entry == null) { //如果緩存中沒有,用網絡請求的,否則用緩存中的
                        return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, null,
                                responseHeaders, true,
                                SystemClock.elapsedRealtime() - requestStart);
                    }

                    // A HTTP 304 response does not have all header fields. We
                    // have to use the header fields from the cache entry plus
                    // the new ones from the response.
                    // http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
                    entry.responseHeaders.putAll(responseHeaders);
                    return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED, entry.data,
                            entry.responseHeaders, true,
                            SystemClock.elapsedRealtime() - requestStart);
                }
                //301,302 表示資源被臨時移動或永久移動了,所以需要重定向,會重新發起請求
                // Handle moved resources
                if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
                    String newUrl = responseHeaders.get("Location");
                    request.setRedirectUrl(newUrl);
                }

                // Some responses such as 204s do not have content.  We must check.
                if (httpResponse.getEntity() != null) {
                  responseContents = entityToBytes(httpResponse.getEntity());
                } else {
                  // Add 0 byte response as a way of honestly representing a
                  // no-content request.
                  responseContents = new byte[0];
                }

                // if the request is slow, log it.
                long requestLifetime = SystemClock.elapsedRealtime() - requestStart;
                logSlowRequests(requestLifetime, request, responseContents, statusLine);
                //如果不在200-299這個範圍那麼拋出IOException,並在隨後的catch中被捕獲
                if (statusCode < 200 || statusCode > 299) {
                    throw new IOException();
                }
                //200-299說明網絡請求正常,直接返回,最後回調Listener的onResponse方法
                return new NetworkResponse(statusCode, responseContents, responseHeaders, false,
                        SystemClock.elapsedRealtime() - requestStart);
            } catch (SocketTimeoutException e) { //捕獲到套接字連接異常,檢查是否需要重試,如果有重試機會那麼繼續while循環否則往上拋異常
                attemptRetryOnException("socket", request, new TimeoutError());
            } catch (ConnectTimeoutException e) { //捕獲到連接超時異常,檢查是否需要重試,如果有重試機會那麼繼續while循環否則往上拋異常
                attemptRetryOnException("connection", request, new TimeoutError());
            } catch (MalformedURLException e) {
                throw new RuntimeException("Bad URL " + request.getUrl(), e);
            } catch (IOException e) {
                int statusCode = 0;
                NetworkResponse networkResponse = null;
                if (httpResponse != null) { //httpResponse不爲空嘛,說明網絡請求走通了,但是響應碼不是200到299之間,那麼走該分支
                    statusCode = httpResponse.getStatusLine().getStatusCode();
                } else { //沒有網絡連接,此時httpResponse爲null
                    throw new NoConnectionError(e); //拋NoConnectionError異常,是VolleyError子類,就不走後面的邏輯了
                }
                if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || 
                        statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
                    VolleyLog.e("Request at %s has been redirected to %s", request.getOriginUrl(), request.getUrl());
                } else {
                    VolleyLog.e("Unexpected response code %d for %s", statusCode, request.getUrl());
                }
                if (responseContents != null) {
                    networkResponse = new NetworkResponse(statusCode, responseContents,
                            responseHeaders, false, SystemClock.elapsedRealtime() - requestStart);
                    if (statusCode == HttpStatus.SC_UNAUTHORIZED ||
                            statusCode == HttpStatus.SC_FORBIDDEN) {
                        attemptRetryOnException("auth",
                                request, new AuthFailureError(networkResponse)); //檢查是否需要重試
                    } else if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || 
                                statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
                        attemptRetryOnException("redirect",
                                request, new RedirectError(networkResponse)); //檢查是否需要重試
                    } else {
                        // TODO: Only throw ServerError for 5xx status codes.
                        throw new ServerError(networkResponse); //ServerError也是VolleyError子類
                    }
                } else {
                    throw new NetworkError(e); //NetworkError也是VolleyError子類
                }
            }
        }
    }

    private static void attemptRetryOnException(String logPrefix, Request<?> request,
            VolleyError exception) throws VolleyError {
        RetryPolicy retryPolicy = request.getRetryPolicy();
        int oldTimeout = request.getTimeoutMs();

        try {
            retryPolicy.retry(exception);
        } catch (VolleyError e) {
            request.addMarker(
                    String.format("%s-timeout-giveup [timeout=%s]", logPrefix, oldTimeout));
            throw e; //繼續往上拋VolleyError異常
        }
        request.addMarker(String.format("%s-retry [timeout=%s]", logPrefix, oldTimeout));
    }
    @Override
    public void retry(VolleyError error) throws VolleyError {
        mCurrentRetryCount++;
        mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
        if (!hasAttemptRemaining()) {
            throw error; //沒有重試機會則拋出VolleyError異常
        }
    }
    protected boolean hasAttemptRemaining() {
        return mCurrentRetryCount <= mMaxNumRetries;
    }

如果正常直接return響應,否則捕獲SocketTimeoutException,ConnectTimeoutException異常,attemptRetryOnException方法判斷如果有重連機會,就不拋出異常,那麼while(true)繼續,所以重新發起網絡請求。否則如果沒有重連機會,那麼拋出VolleyError異常,BasicNetwork.performRequest繼續往上拋,networkdiapatcher.run方法捕獲VolleyError異常,調用parseAndDeliverNetworkError方法,回調給主線程。。..當網絡請求正常時,沒socket連接超時,也沒有connect連接超時,但是當網絡請求的response碼是SC_UNAUTHORIZED,SC_FORBIDDEN...時也會發起重試

可以看到這裏的實現是尊從Http協議的標準來實現的,實現了的不同的狀態碼的不同表現

沒有網絡的時候,肯定是拋出UnknownHostException異常哦,UnknownHostException extends IOException 但是代碼中捕獲到IOException,然後拋出throw new NoConnectionError(e); NoConnectionError間接繼承自VolleyError異常,所以還是會調用parseAndDeliverNetworkError方法,所以最後回調

所以不管是超時異常,還是沒有網絡連接,或者是網絡返回碼不是200到299,同時如果有重試,失敗,那麼最後都走以下的流程,networkdiapatcher.run方法捕獲VolleyError/Exception異常,調用parseAndDeliverNetworkError方法,然後調用mErrorListener的

    //networkdiapatcher的run方法
    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);
    }
    private void parseAndDeliverNetworkError(Request<?> request, VolleyError error) {
        error = request.parseNetworkError(error);
        mDelivery.postError(request, error);
    }

    mErrorListener.onErrorResponse(error);
    public StringRequest(int method, String url, Listener<String> listener, ErrorListener errorListener) {

只有當網絡請求的狀態碼爲200-299纔會調用Listener的onResponse方法,否則其它情況都是調用ErrorListener的onErrorResponse方法

另外超時時間是在哪裏設置獲取的呢?肯定是在>HurlStack/HttpClientStack相應方法裏面設置超時參數了。。HurlStack設置如下

    private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException {
        HttpURLConnection connection = createConnection(url);

        int timeoutMs = request.getTimeoutMs();
        connection.setConnectTimeout(timeoutMs);
        connection.setReadTimeout(timeoutMs);
        connection.setUseCaches(false);
        connection.setDoInput(true);

        // use caller-provided custom SslSocketFactory, if any, for HTTPS
        if ("https".equals(url.getProtocol()) && mSslSocketFactory != null) {
            ((HttpsURLConnection)connection).setSSLSocketFactory(mSslSocketFactory);
        }

        return connection;
    }

7. Post請求方式,怎麼傳遞post參數,怎麼給http請求添加Head?

前面說過只需要重寫getParams方法就能添加post請求參數,自定義請求頭的話,需要重寫getHeaders方法
看以下Request的方法

    //如果需要自定義Http請求頭,複寫這個方法
    public Map<String, String> getHeaders() throws AuthFailureError {
        return Collections.emptyMap();
    }
    //如果需要添加post請求參數,複寫這個方法
    protected Map<String, String> getParams() throws AuthFailureError {
        return null;
    }
    public byte[] getBody() throws AuthFailureError {
        Map<String, String> params = getParams();
        if (params != null && params.size() > 0) {
            return encodeParameters(params, getParamsEncoding());
        }
        return null;
    }
    private static final String DEFAULT_PARAMS_ENCODING = "UTF-8";
    protected String getParamsEncoding() {
        return DEFAULT_PARAMS_ENCODING;
    }
    /**
     * Returns the content type of the POST or PUT body.
     */
    public String getBodyContentType() { //Post方法Content-Type請求頭的值
        return "application/x-www-form-urlencoded; charset=" + getParamsEncoding();
    }
    private byte[] encodeParameters(Map<String, String> params, String paramsEncoding) {
        StringBuilder encodedParams = new StringBuilder();
        try {
            for (Map.Entry<String, String> entry : params.entrySet()) {
                encodedParams.append(URLEncoder.encode(entry.getKey(), paramsEncoding));
                encodedParams.append('=');
                encodedParams.append(URLEncoder.encode(entry.getValue(), paramsEncoding));
                encodedParams.append('&');
            }
            return encodedParams.toString().getBytes(paramsEncoding);
        } catch (UnsupportedEncodingException uee) {
            throw new RuntimeException("Encoding not supported: " + paramsEncoding, uee);
        }
    }

encodedParams就是 param1=value1&param2=value2這類字符串,然後轉換爲byte[]字節數組

getBody()和getHeaders()在什麼地方調用呢?
這裏只分析HurlStack內部使用HttpURLConnection,HttpClientStack內部使用HttpClient這個太簡單就不看了
NetworkDispatcher.run->BasicNetwork.performRequest->HurlStack/HttpClientStack(api<9).performRequest 這條鏈
HurlStack方法
addCacheHeaders(headers, request.getCacheEntry());
httpResponse = mHttpStack.performRequest(request, headers);

   @Override
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
            throws IOException, AuthFailureError {
        String url = request.getUrl();
        HashMap<String, String> map = new HashMap<String, String>();
        map.putAll(request.getHeaders()); //取出request設置的head
        map.putAll(additionalHeaders); //取出緩存中的Header,如果該request前面發起過網絡請求,那麼就會緩存下來,同時會把http的head也緩存
        ........
        URL parsedUrl = new URL(url);
        HttpURLConnection connection = openConnection(parsedUrl, request);
        for (String headerName : map.keySet()) {
            connection.addRequestProperty(headerName, map.get(headerName)); //設置Http請求的head
        }
        setConnectionParametersForRequest(connection, request);
        // Initialize HttpResponse with data from the HttpURLConnection.
        ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
        int responseCode = connection.getResponseCode();
        if (responseCode == -1) {
            // -1 is returned by getResponseCode() if the response code could not be retrieved.
            // Signal to the caller that something was wrong with the connection.
            throw new IOException("Could not retrieve response code from HttpUrlConnection.");
        }
        StatusLine responseStatus = new BasicStatusLine(protocolVersion,
                connection.getResponseCode(), connection.getResponseMessage());
        BasicHttpResponse response = new BasicHttpResponse(responseStatus);
        if (hasResponseBody(request.getMethod(), responseStatus.getStatusCode())) {
            response.setEntity(entityFromConnection(connection));
        }
        for (Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
            if (header.getKey() != null) {
                Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
                response.addHeader(h);
            }
        }
        return response;
    }
    //可以看到volley支持get,post,PUT等都支持的,符合http規範
    static void setConnectionParametersForRequest(HttpURLConnection connection,
            Request<?> request) throws IOException, AuthFailureError {
        switch (request.getMethod()) {
            case Method.GET:
                // Not necessary to set the request method because connection defaults to GET but
                // being explicit here.
                connection.setRequestMethod("GET");
                break;
            case Method.POST:
                connection.setRequestMethod("POST");
                addBodyIfExists(connection, request);
            ...........
        }
    }
    private static void addBodyIfExists(HttpURLConnection connection, Request<?> request)
            throws IOException, AuthFailureError {
        byte[] body = request.getBody();
        if (body != null) {
            connection.setDoOutput(true);
            connection.addRequestProperty(HEADER_CONTENT_TYPE, request.getBodyContentType()); //request的getBodyContentType()方法可以設置Content-Type請求頭
            DataOutputStream out = new DataOutputStream(connection.getOutputStream());
            out.write(body); //把getBody()返回的byte[]數組寫入輸入流,,
            out.close();
        }
    }

另外一方面:爲請求添加緩存頭
performRequest方法的Map<String, String> additionalHeaders參數是從緩存中讀取的,到底添加了那些請求頭呢?可以看到,如果有的話只會添加If-None-Match,If-Modified-Since這兩個頭。。
CacheDispatcher的run方法

                if (entry.isExpired()) {
                    request.addMarker("cache-hit-expired");
                    request.setCacheEntry(entry); //緩存跟request綁定
                    mNetworkQueue.put(request);
                    continue;
                }

Request方法

    public Request<?> setCacheEntry(Cache.Entry entry) {
        mCacheEntry = entry;
        return this;
    }

    /**
     * Returns the annotated cache entry, or null if there isn't one.
     */
    public Cache.Entry getCacheEntry() {
        return mCacheEntry;
    }

BasicNetwork.performRequest

    addCacheHeaders(headers, request.getCacheEntry());
    httpResponse = mHttpStack.performRequest(request, headers); //HttpClientStack或者HurlStack的performRequest方法

    private void addCacheHeaders(Map<String, String> headers, Cache.Entry entry) {
        // If there's no cache entry, we're done.
        if (entry == null) {
            return;
        }

        if (entry.etag != null) {
            headers.put("If-None-Match", entry.etag); //If-None-Match頭從緩存中取出來,如果有
        }

        if (entry.lastModified > 0) {
            Date refTime = new Date(entry.lastModified);
            headers.put("If-Modified-Since", DateUtils.formatDate(refTime)); //If-Modified-Since頭從緩存中取出來,如果有
        }
    }

7.怎麼防止多線程併發問題?

HttpClientStack使用的是new HttpClientStack(AndroidHttpClient.newInstance(userAgent));
AndroidHttpClient繼承HttpClient,內部使用ThreadSafeClientConnManager線程安全連接管理器
HurlStack內部使用的是HttpURLConnection,在最新的版本上HttpURLConnection底層是通過Okhttp來實現的,Okhttp是線程安全的
DDMS可以看到多了一個OKhttp ConnectionPool,這個連接池解決了多線程問題,類似上面的ThreadSafeClientConnManager線程安全連接管理器

這樣就解決了多個線程共享同一個HurlStack/HttpClientStack,造成的多線程問題

另外一方面,如果多個線程使用的是同一個Request呢? 這裏必然會出現多線程問題,代碼實例如下

    for (int i = 0; i < 3; i++) {
       mQueue.add(stringRequest);
       new MyThread(stringRequest, this).start();
    }
    class MyThread extends Thread {
        private StringRequest stringRequest;
        private Context mContext;

        public MyThread(StringRequest stringRequest, Context context) {
            this.stringRequest = stringRequest;
            this.mContext = context;
        }

        @Override
        public void run() {
            RequestQueue mQueue = Volley.newRequestQueue(mContext);
            mQueue.add(stringRequest);
        }
    }

8.爲什麼不適合大數據量的網絡操作

HurlStack中

    response.setEntity(entityFromConnection(connection)); //response是HttpResponse類型
    private static HttpEntity entityFromConnection(HttpURLConnection connection) {
        BasicHttpEntity entity = new BasicHttpEntity();
        InputStream inputStream;
        try {
            inputStream = connection.getInputStream();
        } catch (IOException ioe) {
            inputStream = connection.getErrorStream();
        }
        entity.setContent(inputStream);
        entity.setContentLength(connection.getContentLength());
        entity.setContentEncoding(connection.getContentEncoding());
        entity.setContentType(connection.getContentType());
        return entity;
    }
return mClient.execute(httpRequest); //返回HttpResponse類型

看到木有,都是直接把先寫入內存,然後寫入硬盤緩存的,並沒有針對大數量的操作直接寫入文件處理,所以當然不合適,很可能出現OOM異常啊

9.爲什麼不適合大文件下載,而適合小文件

BasicNetwork的performRequest方法中

.........
byte[] responseContents = null;
.........
if (httpResponse.getEntity() != null) {
    responseContents = entityToBytes(httpResponse.getEntity());
}
.........
return new NetworkResponse(statusCode, responseContents, responseHeaders, false, SystemClock.elapsedRealtime() - requestStart);
    //entityToBytes方法
    /** Reads the contents of HttpEntity into a byte[]. */
    private byte[] entityToBytes(HttpEntity entity) throws IOException, ServerError {
        PoolingByteArrayOutputStream bytes =
                new PoolingByteArrayOutputStream(mPool, (int) entity.getContentLength());
        byte[] buffer = null;
        try {
            InputStream in = entity.getContent();
            if (in == null) {
                throw new ServerError();
            }
            buffer = mPool.getBuf(1024);
            int count;
            while ((count = in.read(buffer)) != -1) {
                bytes.write(buffer, 0, count);
            }
            return bytes.toByteArray();
        } finally {
            try {
                // Close the InputStream and release the resources by "consuming the content".
                entity.consumeContent();
            } catch (IOException e) {
                // This can happen if there was an exception above that left the entity in
                // an invalid state.
                VolleyLog.v("Error occured when calling consumingContent");
            }
            mPool.returnBuf(buffer);
            bytes.close();
        }
    }

看到了吧,不管什麼文件都會通過entityToBytes方法把InputStream寫入byte[]數組InputStream in = entity.getContent()拿到輸入流,如果文件非常大,所以byte[]數組非常大,那麼極可能肯定會OOM。另一方面,因爲直接寫入內存byte[],所以不需要額外IO讀操作(但還是有一次IO寫操作,寫入硬盤緩存),所以很快速度更快。。看了UIL的源碼,對大圖片的下載,網絡請求得到的輸入流-輸出流先寫入硬盤緩存,然後再從硬盤緩存中根據圖片的大小對decodingOptions進行設置(而不是整張大圖直接讀取),讀取輸入流到內存decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);這樣就經歷了兩次IO操作,速度慢一些,但是不容易OOM

10. Volley框架設計好在哪裏,缺點?

優點
泛型的使用,良好的代碼設計,我們的項目中就幾乎沒看到過泛型的身影

public abstract class Request<T> implements Comparable<Request<T>>
    public class StringRequest extends Request<String>
    public abstract class JsonRequest<T> extends Request<T>
        public class JsonArrayRequest extends JsonRequest<JSONArray>
        public class JsonObjectRequest extends JsonRequest<JSONObject>
  1. 擴展性強。Volley 中大多是基於接口的設計,可配置性強。
  2. 一定程度符合 Http 規範,包括返回 ResponseCode(2xx、3xx、4xx、5xx)的處理,請求頭的處理,緩存機制的支持等。並支持重試及優先級定義。
  3. 默認 Android2.3 及以上基於 HttpURLConnection,2.3 以下基於 HttpClient 實現。
  4. 提供簡便的圖片加載工具。
  5. 適合小文件下載,因爲如果文件從是網絡請求中加載,那麼請求得到的輸入流直接寫入內存流,不需要額外的IO讀操作(只需要一次IO寫操作)

缺點
1. 不適合大文件下載,比如下載一本書的內容,但是下載整本書都加載到byte[]內存,肯定是不合適的,應該是把輸出流寫入硬盤緩存,然後再從硬盤緩存中根據需要讀取文件輸入流到byte[],比如讀取某個章節的內容。這樣byte[]就小了很多;大圖片下載其實也是,輸入流byte[]數組全部寫入內存,然後再對圖片壓縮,然並苒,還是可能出現內存溢出。
2. 沒有針對listview,gridview做優化,比如listview圖片錯亂,滑動暫停加載,都沒有處理,據說Universal-Image-Loader優化了這方面,下一步研究下這個開源項目,
3. 沒有使用線程池管理線程啊,一不小心如果沒對requestqueue stop/cancel,線程就一直得不到釋放,資源就耗盡。android-async-http聽說使用了線程池管理線程,但是不推薦使用這個了,內部httpclient實現,落後了。
4. 沒有對文件上傳做處理,增加api會更好多了,這個容易實現,另一方面也體現了volley良好的拓展性
5. 沒有對持久化cookie,增加api會更好多了,這個也容易實現,另一方面也體現了volley良好的拓展性
6. 硬盤緩存默認緩存在內部存儲中,沒有提供api使緩存存放在外部存儲中File cacheDir = new File(context.getCacheDir(), DEFAULT_CACHE_DIR);private static final String DEFAULT_CACHE_DIR = "volley"; 默認在/data/data/…應用包名/cache/volley中

有了這些知識做基礎,如果現在需要我寫一個網絡通信框架,那將得心應手

發佈了78 篇原創文章 · 獲贊 9 · 訪問量 14萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章