Android網絡訪問Volley源碼解析

lutz8325 8年前發布 | 18K 次閱讀 Java開發

來自: http://blog.csdn.net/u014486880/article/details/50703666


很早之前就想寫下關于Volley的源碼解析。一開始學android網絡訪問都是使用HttpClient,剛接觸么Volley的時候就瞬間愛不釋手,雖說現在項目中使用OkHttp多些(Volley更新慢),但是作為google自家推出的網絡框架,Volley還是有很多值得學習的地方。這篇博客是我對Volley源碼分析后的一個總結。

Volley的使用

Volley的使用非常簡單,相信大家都很熟悉。首先需要獲取到一個RequestQueue對象。

RequestQueue mQueue = Volley.newRequestQueue(context); 

如果想通過網絡獲取json,如下:

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

只要在onResponse中處理返回的response即可。如果訪問出錯,則會調用onErrorResonse方法。 注意Volley是異步,是在子線程中進行網絡訪問,而onResponse里的代碼是在主線程中執行。所以使用Volley的地方切記不要把它當成單線程,這是初學者經常犯錯的地方。最后,將這個StringRequest對象添加到RequestQueue里面就可以了。

mQueue.add(stringRequest); 

如果要加載圖片,則首先要定義一個ImageCache,用于定義圖片的緩存。通過ImageLoader來加載圖片,ImageListener則用于指定ImageView以及加載失敗和加載過程中默認圖片 :

            private final LruCache<String, Bitmap> mLruCache = new LruCache<String, Bitmap>(
                    (int) (Runtime.getRuntime().maxMemory() / 10))
            {
                @Override
                protected int sizeOf(String key, Bitmap value)
                {
                    return value.getRowBytes() * value.getHeight();
                }
            };

        @Override
        public void putBitmap(String url, Bitmap bitmap)
        {
            mLruCache.put(url, bitmap);
        }

        @Override
        public Bitmap getBitmap(String url)
        {
            return mLruCache.get(url);
        }
    });

ImageListener listener = ImageLoader.getImageListener(imageView,
R.drawable.default, R.drawable.failed); imageLoader.get(imageurl, listener); </pre>

介紹完簡單用法之后,就來分析源代碼了。

Volley源碼分析

先看下官網給出的介紹圖:
這里寫圖片描述
這里我們先有個大概的介紹,藍色是主線程,綠色是CacheDispatcher(硬盤緩存)線程,紅色是NetworkDispatcher(網絡請求線程)。我們在主線程中調用RequestQueue的add()方法來添加一條網絡請求,這條請求會先被加入到緩存隊列當中,如果發現可以找到相應的緩存結果就直接讀取緩存并解析,然后回調給主線程。如果在緩存中沒有找到結果,則將這條請求加入到網絡請求隊列中,然后處理發送HTTP請求,解析響應結果,寫入緩存,并回調主線程。接下來詳細的進行分析。
不用說,入口肯定是Volley.newRequestQueue(context)。先看下newRequestQueue的代碼:

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;
}</pre> <p>首先封裝得到userAgent,User-Agent 字段設置為 App 的packageName/{versionCode},如果異常則使用 “volley/0”。上面代碼主要是實例化stack ,如果SDK版本大于9,使用HurlStack,否則使用HttpClientStack。實際上HurlStack的內部就是使用HttpURLConnection進行網絡通訊的,而HttpClientStack的內部則是使用HttpClient進行網絡通訊的。也就是說android2.2以上的都是使用HttpURLConnection,否則使用HttpClient。接著new了一個RequestQueue,并調用它的start方法。來看下它的RequestQueue構造方法:</p>

/* Number of network request dispatcher threads to start. /
    private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;

/** Cache interface for retrieving and storing responses. */
private final Cache mCache;

/** Network interface for performing requests. */
private final Network mNetwork;

/** Response delivery mechanism. */
private final ResponseDelivery mDelivery;

/** The network dispatchers. */
private NetworkDispatcher[] mDispatchers;

public RequestQueue(Cache cache, Network network, int threadPoolSize, ResponseDelivery delivery) { mCache = cache; mNetwork = network; mDispatchers = new NetworkDispatcher[threadPoolSize]; mDelivery = delivery; }</pre>

初始化主要就是4個參數:mCache、mNetwork、mDispatchers、mDelivery。第一個是硬盤緩存;第二個主要用于Http相關操作;第三個用于轉發請求的;第四個參數用于把結果轉發到UI線程,通過它來對外聲明接口。接下來看下start方法。

 private static final int DEFAULT_NETWORK_THREAD_POOL_SIZE = 4;

/** Cache interface for retrieving and storing responses. */
private final Cache mCache;

/** Network interface for performing requests. */
private final Network mNetwork;

/** Response delivery mechanism. */
private final ResponseDelivery mDelivery;

/** The network dispatchers. */
private NetworkDispatcher[] mDispatchers;

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

/** * Stops the cache and network dispatchers. */
public void stop() {
    if (mCacheDispatcher != null) {
        mCacheDispatcher.quit();
    }
    for (int i = 0; i < mDispatchers.length; i++) {
        if (mDispatchers[i] != null) {
            mDispatchers[i].quit();
        }
    }
}</pre> <p>首先調用stop()方法,確保此時所有轉發器都處于停止狀態。接下來就new了一個CacheDispatcher轉發器,它其實就是一個線程,用于硬盤緩存。再new了四個NetworkDispatcher轉發器,用于網絡請求。并分別調用這些線程的start()方法。如果是加載圖片,我們還需定義一個imageLoader,來看看Volley中為我們定義的ImageLoader,主要看它的get方法:</p>

 public ImageContainer get(String requestUrl, ImageListener imageListener,
            int maxWidth, int maxHeight) {
        // only fulfill requests that were initiated from the main thread.
        throwIfNotOnMainThread();

    final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight);

    // Try to look up the request in the cache of remote images.
    Bitmap cachedBitmap = mCache.getBitmap(cacheKey);
    if (cachedBitmap != null) {
        // Return the cached bitmap.
        ImageContainer container = new ImageContainer(cachedBitmap, requestUrl, null, null);
        imageListener.onResponse(container, true);
        return container;
    }

    // The bitmap did not exist in the cache, fetch it!
    ImageContainer imageContainer =
            new ImageContainer(null, requestUrl, cacheKey, imageListener);

    // Update the caller to let them know that they should use the default bitmap.
    imageListener.onResponse(imageContainer, true);

    // Check to see if a request is already in-flight.
    BatchedImageRequest request = mInFlightRequests.get(cacheKey);
    if (request != null) {
        // If it is, add this request to the list of listeners.
        request.addContainer(imageContainer);
        return imageContainer;
    }

    // The request is not already in flight. Send the new request to the network and
    // track it.
    Request<?> newRequest =
        new ImageRequest(requestUrl, new Listener<Bitmap>() {
            @Override
            public void onResponse(Bitmap response) {
                onGetImageSuccess(cacheKey, response);
            }
        }, maxWidth, maxHeight,
        Config.RGB_565, new ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                onGetImageError(cacheKey, error);
            }
        });

    mRequestQueue.add(newRequest);
    mInFlightRequests.put(cacheKey,
            new BatchedImageRequest(newRequest, imageContainer));
    return imageContainer;
}</pre> <p>上面代碼具體流程是這樣,首先通過throwIfNotOnMainThread()方法限制必須在UI線程調用;然后根據傳入的參數計算cacheKey,獲取緩存;如果存在cache,直接將返回結果封裝為一個ImageContainer,然后直接回調imageListener.onResponse(container, true);這時我們就可以設置圖片了。如果不存在,那就初始化一個ImageContainer,然后直接回調imageListener.onResponse(imageContainer, true),這里是為了讓我們設置默認圖片。所以,在實現listener的時候,要先判斷resp.getBitmap()是否為null;接下來檢查該url是否早已加入了請求對了,如果已加入,則將剛初始化的ImageContainer加入BatchedImageRequest。這就是加載圖片時的內存緩存。 <br />

然后調用RequestQueue的add()方法將Request傳入就可以完成網絡請求操作了,讓我們來看看add方法中到底做了什么事。</p>

 private final Map<String, Queue< Request<?>>> mWaitingRequests =
            new HashMap<String, Queue< Request<?>>>();

/** * The set of all requests currently being processed by this RequestQueue. A Request * will be in this set if it is waiting in any queue or currently being processed by * any dispatcher. */
private final Set<Request <?>> mCurrentRequests = new HashSet<Request<?>>();

/** The cache triage queue. */
private final PriorityBlockingQueue< Request<?>> mCacheQueue =
    new PriorityBlockingQueue< Request<?>>();

/** The queue of requests that are actually going out to the network. */
private final PriorityBlockingQueue< Request<?>> mNetworkQueue =
    new PriorityBlockingQueue< Request<?>>();

/* Adds a Request to the dispatch queue. @param request The request to service @return The passed -in 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;
    }
}</pre> <p>這里首先將請求加入mCurrentRequests,這個mCurrentRequests是一個HashSet,它保存了所有需要處理的Request,主要為了提供cancel的入口。如果該請求不應該被緩存則直接加入mNetworkQueue,然后返回。request.shouldCache()在默認情況下,每條請求都是可以緩存的,當然我們也可以調用Request的setShouldCache(false)方法來改變這一默認行為。 <br />

接下來判斷該請求是否有相同的請求正在被處理,如果有則加入mWaitingRequests;如果沒有,則加入mWaitingRequests.put(cacheKey, null),并將request加入到CacheQueue中。
有了隊列,我們就來看看線程是如何執行的。先看CacheDispatcher。</p>

public class CacheDispatcher extends Thread {

private static final boolean DEBUG = VolleyLog.DEBUG;

/** The queue of requests coming in for triage. */
private final BlockingQueue<Request<?>> mCacheQueue;

/** The queue of requests going out to the network. */
private final BlockingQueue<Request<?>> mNetworkQueue;

/** The cache to read from. */
private final Cache mCache;

/** For posting responses. */
private final ResponseDelivery mDelivery;

/** Used for telling us to die. */
private volatile boolean mQuit = false;

/** * Creates a new cache triage dispatcher thread. You must call {@link #start()} * in order to begin processing. * * @param cacheQueue Queue of incoming requests for triage * @param networkQueue Queue to post requests that require network to * @param cache Cache interface to use for resolution * @param delivery Delivery interface to use for posting responses */
public CacheDispatcher(
        BlockingQueue<Request<?>> cacheQueue, BlockingQueue<Request<?>> networkQueue,
        Cache cache, ResponseDelivery delivery) {
    mCacheQueue = cacheQueue;
    mNetworkQueue = networkQueue;
    mCache = cache;
    mDelivery = delivery;
}

/** * Forces this dispatcher to quit immediately. If any requests are still in * the queue, they are not guaranteed to be processed. */
public void quit() {
    mQuit = true;
    interrupt();
}

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

}</pre>

我們要知道CacheDispatcher是硬盤緩存,到此可知Volley也是有二級緩存的。重點看它的run方法。看到while(true)時,我們就知道,它是在不斷的執行的。首先從mCacheQueue中取出緩存,如果沒有取到,就把它加入mNetworkQueue中,再判斷緩存是否過期,如果過期,也放入mNetworkQueue中。否則就取到了可用的緩存了,再調用request.parseNetworkResponse解析從緩存中取出的data和responseHeaders通過mDelivery.postResponse轉發,然后回調到UI線程;我們看下mDelivery.postResponse方法:

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

主要看ResponseDeliveryRunnable。

 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();
        }
   }</pre> <p>在它的run方法中,如果訪問成功會調用mRequest.deliverResponse(mResponse.result)方法,到這里就很明了了,因為每個request子類中都要重寫deliverResponse,最后我們再在這個方法中將響應的數據回調到Response.Listener的onResponse()方法中就可以了。以StringRequest為例:</p>

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

分析完緩存,我們來看下網絡加載。它是在NetworkDispatcher線程中實現的。

public class NetworkDispatcher extends Thread {
    / The queue of requests to service. */
    private final BlockingQueue<Request<?>> mQueue;
    / The network interface for processing requests. /
    private final Network mNetwork;
    /** The cache to write to. /
    private final Cache mCache;
    / For posting responses and errors. */
    private final ResponseDelivery mDelivery;
    / Used for telling us to die. */
    private volatile boolean mQuit = false;

/** * Creates a new network dispatcher thread. You must call {@link #start()} * in order to begin processing. * * @param queue Queue of incoming requests for triage * @param network Network interface to use for performing requests * @param cache Cache interface to use for writing responses to cache * @param delivery Delivery interface to use for posting responses */
public NetworkDispatcher(BlockingQueue<Request<?>> queue,
        Network network, Cache cache,
        ResponseDelivery delivery) {
    mQueue = queue;
    mNetwork = network;
    mCache = cache;
    mDelivery = delivery;
}

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

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

}</pre>

首先取出請求;然后通過mNetwork.performRequest(request)處理我們的請求,拿到NetworkResponse。看下performRequest方法:

 public NetworkResponse performRequest(Request<?> request) throws VolleyError {  
        long requestStart = SystemClock.elapsedRealtime();  
        while (true) {  
            HttpResponse httpResponse = null;  
            byte[] responseContents = null;  
            Map<String, String> responseHeaders = new HashMap<String, String>();  
            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) {  
                    return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,  
                            request.getCacheEntry() == null ? null : request.getCacheEntry().data,  
                            responseHeaders, true);  
                }  
                // 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);  
                if (statusCode < 200 || statusCode > 299) {  
                    throw new IOException();  
                }  
                return new NetworkResponse(statusCode, responseContents, responseHeaders, false);  
            } catch (Exception e) {  
                ……  
            }  
        }  
    }  

上面方法主要是網絡請求的一些細節,所以如果要修改請求的細節就要到此處修改(后面會講到)。
在這里服務器會返回的數據組裝成一個NetworkResponse對象進行返回。在NetworkDispatcher中收到了NetworkResponse這個返回值后又會調用Request的parseNetworkResponse()方法來解析NetworkResponse中的數據,再將數據寫入到緩存。parseNetworkResponse的實現是交給Request的子類來完成的,不同種類的Request解析的方式不同。如json與gson就有區別。最后與CacheDispatcher一樣調用mDelivery.postResponse(request, response)返回回調,這里就不再分析了。
到這里volley的源碼就分析完了,總結一下:

  • 首先初始化RequestQueue,主要就是開啟CacheDispatcher和NetworkDispatcher線程,線程會不斷讀取請求,沒有消息則阻塞。
  • 當我們發出請求以后,會根據url,ImageView屬性等,構造出一個cacheKey,然后首先從LruCache中獲取,這個緩存我們自己構建的,這就是內存緩存;如果沒有取到,則判斷是否存在硬盤緩存,這一步是從getCacheDir里面獲取(默認5M);如果沒有取到,則從網絡請求;
  • </ul>

    Volley的擴展

    添加cookie頭

    volley跟httpClient不一樣,它是不會自動添加cookie頭的。但是cookie在應用中卻很重要,它會保證登陸后的操作都處于一個會話中,有效的增加了安全性。那么如何在volley中自動添加cookie呢。
    首先在新建Appliaction,當成全局的Application,然后在里面編寫在http頭參數中識別出cookie和添加cookie到Http頭代碼。

    /*  Checks the response headers for session cookie and saves it  if it finds it.  @param headers Response Headers. */
        public static final void checkSessionCookie(Map<String, String> headers) {
            Log.e("TAG", "checkSessionCookie->headers:" + headers);

        if (headers.containsKey(GlobalParams.SET_COOKIE_KEY) && headers.get(GlobalParams.SET_COOKIE_KEY).startsWith(GlobalParams.SESSION_COOKIE)) {
            String cookie = headers.get(GlobalParams.SET_COOKIE_KEY);
            if (cookie.length() > 0) {
                //形如Set-Cookie:JSESSIONID=18D6BCC01453C6EB39BB0C4208F389EE; Path=/smdb
                //進行解析,取出JSESSIONID的value
                String[] splitCookie = cookie.split(";");
                String[] splitSessionId = splitCookie[0].split("=");
                cookie = splitSessionId[1];
                Editor prefEditor = preferences.edit();
                prefEditor.putString(GlobalParams.SESSION_COOKIE, cookie);
                prefEditor.commit();
            }
        }else {
            if (null != httpclient.getCookieStore()) {
                List<Cookie> cookies = httpclient.getCookieStore().getCookies();
                for (Cookie cookie : cookies) {
                    if ("JSESSIONID".equals(cookie.getName())) {//取得session的value
                        String sessionId = cookie.getValue();
                        Editor prefEditor = preferences.edit();
                        prefEditor.putString(GlobalParams.SESSION_COOKIE, sessionId);
                        prefEditor.commit();
                        break;
                    }
                }
                if (!cookies.isEmpty()) {
                    for (int i = 0; i < cookies.size(); i++) {
                        cookie = cookies.get(i);//保存cookie的信息使得HttpClient和WebView共享同一個cookie
                    }
                }
            }
        }
    }</pre> <p>接著就要在Request的子類中合適地方添加頭信息,哪個地方合適。我們來看下HurlStack的performRequest方法。</p>
    

    @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());
            map.putAll(additionalHeaders);
            if (mUrlRewriter != null) {
                String rewritten = mUrlRewriter.rewriteUrl(url);
                if (rewritten == null) {
                    throw new IOException("URL blocked by rewriter: " + url);
                }
                url = rewritten;
            }
            URL parsedUrl = new URL(url);
            HttpURLConnection connection = openConnection(parsedUrl, request);
            for (String headerName : map.keySet()) {
                connection.addRequestProperty(headerName, map.get(headerName));
            }
            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);
            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;
        }

    重點看到map.putAll(request.getHeaders());所以我們考慮到如果要給它添加頭信息可以在request的getHeaders()方法中添加。至此我們以StringRequest為例,重寫一個類叫MyStringRequest:

    public class MyStringRequest extends StringRequest {

    private final Map<String, String> mParams;
    /** * @param method * @param url * @param params * A {@link HashMap} to post with the request. Null is allowed * and indicates no parameters will be posted along with request. * @param listener * @param errorListener */
    public MyStringRequest(int method, String url, Map<String, String> params, Listener<String> listener,
            ErrorListener errorListener) {
        super(method, url, listener, errorListener);
        mParams = params;
    }
    
    @Override
    protected Map<String, String> getParams() {
        return mParams;
    }
    
    /* (non-Javadoc) * @see com.android.volley.toolbox.StringRequest#parseNetworkResponse(com.android.volley.NetworkResponse) */
    @Override
    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        // since we don't know which of the two underlying network vehicles
        // will Volley use, we have to handle and store session cookies manually
        Log.e("TAG", "parseNetworkResponse->response.headers:" + response.headers);
        GlobalApplication.checkSessionCookie(response.headers);
        return super.parseNetworkResponse(response);
    }
    
    /* (non-Javadoc) * @see com.android.volley.Request#getHeaders() */
    @Override
    public Map<String, String> getHeaders() throws AuthFailureError {
        Map<String, String> headers = super.getHeaders();
            if (headers == null || headers.equals(Collections.emptyMap())) {
                headers = new HashMap<String, String>();
            }
            GlobalApplication.addSessionCookie(headers);
        return headers;
    }
    
    

    }</pre>

    在parseNetworkResponse中調用checkSessionCookie解析頭信息中的cookie,然后重寫getHeaders方法,調用addSessionCookie添加cookie。

    添加重定向功能

    網絡訪問經常要用到重定向,雖說在客戶端中用得比較少。那Volley能不能進行自動重定向,答案是可以的,重要修改下源碼。既然要重定向,那就要在請求返回的進行判斷,毫無疑問要在BasicNetwork的performRequest中修改,先看下修改后的代碼:

     @Override
        public NetworkResponse performRequest(Request<?> request) throws VolleyError {
            long requestStart = SystemClock.elapsedRealtime();
            while (true) {
                HttpResponse httpResponse = null;
                byte[] responseContents = null;
                Map<String, String> responseHeaders = new HashMap<String, String>();
                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) {
                    return new NetworkResponse(HttpStatus.SC_NOT_MODIFIED,
                            request.getCacheEntry() == null ? null : request.getCacheEntry().data,
                            responseHeaders, true);
                }
    
                // Handle moved resources
                //Line143-148為解決301/302重定向問題增加的代碼。
                //參考見https://github.com/elbuild/volley-plus/commit/4a65a4099d2b1d942f4d51a6df8734cf272564eb#diff-b4935f77d9f815bb7e0dba85e55dc707R150
                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);
    
                if (statusCode < 200 || statusCode > 299) {
                    throw new IOException();
                }
                return new NetworkResponse(statusCode, responseContents, responseHeaders, false);
            } catch (SocketTimeoutException e) {
                attemptRetryOnException("socket", request, new TimeoutError());
            } catch (ConnectTimeoutException e) {
                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) {
                    statusCode = httpResponse.getStatusLine().getStatusCode();
                } else {
                    throw new NoConnectionError(e);
                }
                //Line143-148為解決301/302重定向問題增加的代碼。
                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);
                    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) {
                        //else if語句為解決301/302重定向問題增加的代碼。設置重連請求。 
                        attemptRetryOnException("redirect",
                                request, new AuthFailureError(networkResponse));
                    } else {
                        // TODO: Only throw ServerError for 5xx status codes.
                        throw new ServerError(networkResponse);
                    }
                } else {
                    throw new NetworkError(networkResponse);
                }
            }
        }
    }</pre> <p>其實重點添加了以下的代碼:</p>
    

     if (statusCode == HttpStatus.SC_MOVED_PERMANENTLY || statusCode == HttpStatus.SC_MOVED_TEMPORARILY) {
                        String newUrl = responseHeaders.get("Location");
                        request.setRedirectUrl(newUrl);
                    }

    上面的代碼就是判斷返回code是否是301或302,如果是就獲取重定向的Url,再設置重定向,很簡單。到此Volley常見的擴展功能就講完了。
    源碼解析的文章都會有點長,寫完也要有耐心。繼續堅持。

    </div>

 本文由用戶 lutz8325 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!