Volley的使用以及源碼解析

Volley簡介

Volley 是 Google I/O 2013上發(fā)布的網(wǎng)絡(luò)通信庫,使網(wǎng)絡(luò)通信更快、更簡單、更健壯。Volley特別適合數(shù)據(jù)量不大但是通信頻繁的場景,而對于大數(shù)據(jù)量的網(wǎng)絡(luò)操作,比如說下載文件等,Volley的表現(xiàn)就會非常糟糕。Volley提供的功能有:

  • JSON,圖像等的異步下載;
  • 網(wǎng)絡(luò)請求的排序(scheduling)
  • 網(wǎng)絡(luò)請求的優(yōu)先級處理
  • 緩存
  • 多級別取消請求
  • 和Activity和生命周期的聯(lián)動(Activity結(jié)束時同時取消所有網(wǎng)絡(luò)請求)

Volley的優(yōu)點

  • 體積小,使用Volley可以使用Volley.jar或者通過gradle導(dǎo)入依賴,全部只有42個類,只有100多k大小。
  • 非常適合進行數(shù)據(jù)量不大,但通信頻繁的網(wǎng)絡(luò)操作。
  • 可直接在主線程調(diào)用服務(wù)端并處理返回結(jié)果
  • 可以取消請求,容易擴展,面向接口編程。
  • 網(wǎng)絡(luò)請求線程NetworkDispatcher默認開啟了4個,可以優(yōu)化,通過手機CPU數(shù)量。
  • 通過使用標準的HTTP緩存機制保持磁盤和內(nèi)存響應(yīng)的一致。

Volley的缺點

  • 使用的是HttpClient、HttpURLConnection
  • Android6.0不支持HttpClient了,如果想支持得添加org.apache.http.legacy.jar或者在app module中的build.gradle中加入useLibrary 'org.apache.http.legacy'
  • 對大文件下載Volley的表現(xiàn)非常糟糕
  • 只支持Http請求
  • 圖片加載性能一般

Volley的基本用法

關(guān)于Volley的使用,官方文檔的地址是https://developer.android.com/training/volley/index.html

Volley的用法非常簡單,發(fā)起一條HTTP請求,然后接收HTTP響應(yīng)。首先需要獲取到一個RequestQueue對象,可以調(diào)用如下方法獲取到:

RequestQueue queue = Volley.newRequestQueue(context);

RequestQueue是一個所有請求的隊列對象,它可以緩存所有HTTP請求,它的內(nèi)部有兩個隊列:

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

緩存請求的隊列和處理請求的隊列都是優(yōu)先級阻塞隊列,RequestQueue是按照一定的算法并發(fā)地發(fā)出這些請求。RequestQueue內(nèi)部的設(shè)計就是非常合適高并發(fā)的,因此我們不必為每一次HTTP請求都創(chuàng)建一個RequestQueue對象,這是非常浪費資源的,基本上在每一個需要和網(wǎng)絡(luò)交互的Activity中創(chuàng)建一個RequestQueue對象就足夠了。

接下來為了要發(fā)出一條HTTP請求,我們還需要創(chuàng)建一個StringRequest對象,如下所示:

String url ="http://shenhuniurou.com";
// Request a string response from the provided URL.
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
    new Response.Listener<String>() {
        @Override
        public void onResponse(String response) {
            
        }
    },
    new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
        
        }
    }
);

StringRequest封裝了一個請求,包括請求地址,請求方式,請求結(jié)果監(jiān)聽等。它有兩個構(gòu)造方法

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

    
public StringRequest(String url, Listener<String> listener, ErrorListener errorListener) {
    this(Method.GET, url, listener, errorListener);
}

如果沒傳請求方式,那么默認是GET方式。最后把這個請求添加到RequestQueue中去。

// Add the request to the RequestQueue.
queue.add(stringRequest);

另外還需要在AndroidManifest.xml中添加用戶權(quán)限

<uses-permission android:name="android.permission.INTERNET"/>

如果我們要使用POST方式并向服務(wù)器傳遞參數(shù),那就必須在StringRequest的匿名類中重寫getParams方法

StringRequest stringRequest = new StringRequest(Request.Method.POST, url, 
    new Response.Listener<String>() {
        @Override
        public void onResponse(String response) {
            
        }
    },
    new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
            
        }
    }) {
    @Override protected Map<String, String> getParams() throws AuthFailureError {
        HashMap<String, String> params = new HashMap<>();
        params.put("key1", "value1");
        params.put("keyn", "keyn");
        return params;
    }
};

以上是使用StringRequest類來提交參數(shù)發(fā)起請求,StringRequest是繼承自Request<String>的,Request<T>是一個泛型抽象類,除了StringRequest,Request還有一個直接子類JsonRequest,不過JsonRequest也是一個泛型抽象類,它有兩個子類,JsonArrayRequest和JsonObjectRequest,用于請求JSON類型的數(shù)據(jù),用法如下:

JsonObjectRequest jsonObjectRequest = new JsonObjectRequest(
    Request.Method.GET,
    "http://www.weather.com.cn/data/sk/101110101.html",
    null,
    new Response.Listener<JSONObject>() {
        @Override public void onResponse(JSONObject response) {
            mTextView.setText("Response is: "+ response.toString());
        }
    },
    new Response.ErrorListener() {
        @Override public void onErrorResponse(VolleyError error) {

        }
    });

queue.add(jsonObjectRequest);

JsonArrayRequest jsonArrayRequest = new JsonArrayRequest(
    Request.Method.POST, 
    url, 
    null,
    new Response.Listener<JSONArray>() {
        @Override public void onResponse(JSONArray response) {

        }
    }, 
    new Response.ErrorListener() {
    @Override public void onErrorResponse(VolleyError error) {
        
    }
});

queue.add(jsonArrayRequest);

注意,并不是所有格式的數(shù)據(jù)都可以使用這兩個類來發(fā)起請求的,只有JSON格式的才可以,它提交的參數(shù)類型也是json格式的。

使用Volley加載網(wǎng)絡(luò)圖片

使用Volley加載網(wǎng)絡(luò)圖片使用的是Volley中的ImageRequest類來發(fā)起請求,ImageRequest同樣是Request類,不過它的泛型是Bitmap,和StringRequest、JsonRequest用法類似,先創(chuàng)建一個RequestQueue對象,創(chuàng)建一個Request對象,然后將Request對象添加到RequestQueue中即可。

final ImageView imageView = (ImageView) findViewById(R.id.imageView);

ImageRequest imageRequest = new ImageRequest(
    "http://tuku.chengdu.cn/CHN_CommendationPic/20120319105921966875741.jpg",
    new Response.Listener<Bitmap>() {
        @Override public void onResponse(Bitmap response) {
            imageView.setImageBitmap(response);
        }
    },
    0,
    0,
    ImageView.ScaleType.FIT_XY,
    Bitmap.Config.RGB_565,
    new Response.ErrorListener() {
        @Override public void onErrorResponse(VolleyError error) {

        }
    }
);

queue.add(imageRequest);

ImageRequest的構(gòu)造方法中有兩個,其中有一個已經(jīng)棄用了,我們看到一共有七個參數(shù),第一個url是圖片的地址,第二個參數(shù)是加載圖片成功后的回調(diào),里面返回的Bitmap位圖,第三個maxWidth和第四個maxHeight是用于指定允許圖片最大的寬度和高度,如果指定的網(wǎng)絡(luò)圖片的寬度或高度大于這里的最大值,則會對圖片進行壓縮,指定成0的話就表示不管圖片有多大,都不會進行壓縮;第五個參數(shù)scaleType表示圖片顯示的縮放類型,被棄用的那個構(gòu)造方法是沒有這個參數(shù)的,它默認使用ScaleType.CENTER_INSIDE,第六個參數(shù)Config是常量,用于指定顏色的屬性,其中ARGB_8888可以展示最好的顏色屬性,每個圖片像素占據(jù)4個字節(jié)的大小,而RGB_565則表示每個圖片像素占據(jù)2個字節(jié)大小。第七個參數(shù)是加載圖片失敗后的回調(diào)。

小結(jié):Request類的子類發(fā)送請求的步驟基本是一樣的,可分為三步:

  • 創(chuàng)建RequestQueue對象
  • 創(chuàng)建Request類的子類對象(StringRequest/JsonRequest/ImageRequest)
  • 將Request的子類對象添加到RequestQueue對象中

除了使用ImageRequest可以來加載圖片之外,Volley中還可以使用ImageLoader來加載圖片,ImageLoader內(nèi)部其實也是用ImageRequest來實現(xiàn)的,不過ImageLoader明顯要比ImageRequest更加高效,因為它不僅可以幫我們對圖片進行緩存,還可以過濾掉重復(fù)的鏈接,避免重復(fù)發(fā)送請求,使用方法:

RequestQueue queue = Volley.newRequestQueue(this);
        
ImageLoader imageLoader = new ImageLoader(queue, new ImageLoader.ImageCache() {
    @Override public Bitmap getBitmap(String url) {
        return null;
    }

    @Override public void putBitmap(String url, Bitmap bitmap) {

    }
});

ImageLoader.ImageListener listener = ImageLoader.getImageListener(imageView, R.mipmap.ic_launcher, R.mipmap.ic_launcher_round);
imageLoader.get("http://tuku.chengdu.cn/CHN_CommendationPic/20120319105921966875741.jpg", listener);

因為ImageLoader不是繼承自Request類,所以它加載圖片的用法和之前那些不一樣了,大致步驟為:

  • 創(chuàng)建RequestQueue對象
  • 創(chuàng)建ImageLoader對象
  • 創(chuàng)建ImageListener對象
  • 調(diào)用ImageLoader的get方法

在創(chuàng)建ImageLoader對象時,我們傳了兩個參數(shù),RequestQueue和ImageCache,ImageCache是實現(xiàn)圖片緩存的的接口,當圖片加載成功后,會將圖片緩存起來,下次再加載同樣一張圖片時,就不用去網(wǎng)絡(luò)上加載而是直接在緩存中加載即可,這樣既節(jié)省了資源也節(jié)省了請求時間。

我們看到ImageCache是一個接口:

public interface ImageCache {
    public Bitmap getBitmap(String url);
    public void putBitmap(String url, Bitmap bitmap);
}

我們要自己實現(xiàn)圖片的緩存,只要實現(xiàn)這個接口就行了:

public class BitmapCache implements ImageLoader.ImageCache {

    private LruCache<String, Bitmap> mCache;

    public BitmapCache() {
        int maxSize = 2 * 1024 * 1024;
        mCache = new LruCache<String, Bitmap>(maxSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight();
            }
        };
    }

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

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

}

所以當我們創(chuàng)建ImageLoader對象時,new一個BitmapCache對象傳進去即可:

ImageLoader imageLoader = new ImageLoader(queue, new BitmapCache());

除了以上兩種加載圖片的方式之外,Volley還提供了另外一種,使用NetworkImageView,不同于以上兩種方式,NetworkImageView是一個自定義View,它是繼承自ImageView,具備ImageView控件的所有功能,并且在原生的基礎(chǔ)之上加入了加載網(wǎng)絡(luò)圖片的功能。NetworkImageView控件的用法要比前兩種方式更加簡單,大致可以分為以下幾步:

  • 創(chuàng)建一個RequestQueue對象。
  • 創(chuàng)建一個ImageLoader對象。
  • 在布局文件中添加一個NetworkImageView控件。
  • 在代碼中獲取該控件的實例。
  • 設(shè)置要加載的圖片地址。
RequestQueue mQueue = Volley.newRequestQueue(this);
ImageLoader imageLoader = new ImageLoader(mQueue, new BitmapCache());
NetworkImageView networkImageView = (NetworkImageView) findViewById(R.id.networkImageView);
networkImageView.setDefaultImageResId(R.mipmap.ic_launcher);
networkImageView.setErrorImageResId(R.mipmap.ic_launcher);
networkImageView.setImageUrl("http://tuku.chengdu.cn/CHN_CommendationPic/20120319105921966875741.jpg", imageLoader);

使用ImageRequest和ImageLoader這兩種方式來加載網(wǎng)絡(luò)圖片,都可以傳入一個最大寬度和高度的參數(shù)來對圖片進行壓縮,但是由于NetworkImageView是一個控件,在加載圖片的時候它會自動獲取自身的寬高,然后對比網(wǎng)絡(luò)圖片的寬度,再決定是否需要對圖片進行壓縮。也就是說,壓縮過程是在內(nèi)部完全自動化的,并不需要我們關(guān)心,所以我們加載的時候不需要手動傳入最大寬高,NetworkImageView會始終呈現(xiàn)給我們一張大小剛剛好的網(wǎng)絡(luò)圖片,不會多占用任何一點內(nèi)存,如果你不想對圖片進行壓縮,只需要在布局文件中把NetworkImageView的layout_width和layout_height都設(shè)置成wrap_content就可以了,這樣NetworkImageView就會將該圖片的原始大小展示出來,不會進行任何壓縮。

實現(xiàn)自定義Request

一般如果請求的數(shù)據(jù)是字符串、圖片、JSON類型的數(shù)據(jù)時,我們都不需要自定義Request,但是如果這些還不滿足我們的需求時,就要自定義Request了,需要做的事有下面兩步:

  • 繼承Request<T>泛型類,其中的泛型表示我們請求期望解析響應(yīng)的類型。
  • 實現(xiàn)抽象方法parseNetworkResponse()deliverResponse()

Gson是使用反射將Java對象轉(zhuǎn)換為JSON或從JSON轉(zhuǎn)換的庫。你可以定義與其對應(yīng)的JSON鍵具有相同名稱的Java實體對象,將Gson傳遞給類對象,Gson就可以將這個Java對象的各個字段自動填充值。以下是使用Gson進行解析的Volley請求的完整實現(xiàn):

public class GsonRequest<T> extends Request<T> {

    private Gson mGson;
    private Class<T> clazz;
    private Map<String, String> headers;
    private Response.Listener<T> listener;
    
    public GsonRequest(int method, String url, Class<T> clazz, Map<String, String> headers, Response.Listener<T> listener, Response.ErrorListener errorListener) {
        super(method, url, errorListener);  
        mGson = new Gson();  
        mClass = clazz;  
        mListener = listener;  
    }
    
    public GsonRequest(String url, Class<T> clazz, Listener<T> listener,  
            ErrorListener errorListener) {  
        this(Method.GET, url, clazz, listener, errorListener);  
    }  

    @Override
    public Map<String, String> getHeaders() throws AuthFailureError {
        return headers != null ? headers : super.getHeaders();
    }

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

    @Override
    protected Response<T> parseNetworkResponse(NetworkResponse response) {
        try {
            String json = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
            return Response.success(mGson.fromJson(json, clazz), HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            return Response.error(new ParseError(e));
        } catch (JsonSyntaxException e) {
            return Response.error(new ParseError(e));
        }
    }
}

使用的時候我們縣創(chuàng)建一個對象實體類,定義其屬性和getter/setter方法,然后使用這個GsonRequest類解析時應(yīng)該這樣:

GsonRequest<JavaBean> gsonRequest = new GsonRequest<JavaBean>(
    url, JavaBean.class,
    new Response.Listener<JavaBean>() {
        @Override
        public void onResponse(JavaBean javaBean) {
            
        }
    }, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {
        Log.e("TAG", error.getMessage(), error);
    }
});
mQueue.add(gsonRequest);

Volley源碼解析

先來看下面這張圖,是Volley官方文檔主頁上摳下來的,它解釋了Volley的工作流程和整體結(jié)構(gòu)

volley-request

一般我們要分析一個框架的源碼,首先我們要找到它的入口,從Volley的使用方法來看,我們第一步是先通過Volley類的newRequestQueue(this)創(chuàng)建一個RequestQueue對象,那就從這里開始吧。Volley類只有兩個方法,都是newRequestQueue,一個是一個參數(shù),另一個是兩個參數(shù),我們直接看最終調(diào)用的那個:

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是null,所以這里會先判斷sdk版本,如果是9以上就創(chuàng)建HurlStack類對象,否則創(chuàng)建HttpClientStack類型對象,至于HurlStack和HttpClientStack這兩個類是什么?我們點進去可以看到這個類的說明:

/**
 * An {@link HttpStack} based on {@link HttpURLConnection}.
 */
public class HurlStack implements HttpStack {
/**
 * An HttpStack that performs request over an {@link HttpClient}.
 */
public class HttpClientStack implements HttpStack {

也就是說HurlStack內(nèi)部是基于HttpURLConnection實現(xiàn)的,而HttpClientStack是基于HttpClient來實現(xiàn)的,這里為什么要根據(jù)sdk版本來分別選擇這兩種不同的類呢?

我們根據(jù)上面源碼中提示的文章地址http://android-developers.blogspot.com/2011/09/androids-http-clients.html可以知道,大多數(shù)的Android應(yīng)用程序都會使用HTTP協(xié)議來發(fā)送和接收網(wǎng)絡(luò)數(shù)據(jù),而Android中主要提供了兩種方式來進行HTTP操作,HttpURLConnection和HttpClient。這兩種方式都支持HTTPS協(xié)議、以流的形式進行上傳和下載、配置超時時間、IPv6、以及連接池等功能。但是在Android2.3之前,HttpURLConnection是不可靠,bug太多,而HttpClient比較穩(wěn)定,bug較少,API很多,基本上已經(jīng)滿足了開發(fā)者們的需求,因此很難在不破壞其兼容性的情況下進行拓展。

在Android2.2版本之前,HttpURLConnection一直存在著一些令人厭煩的bug。比如說對一個可讀的InputStream調(diào)用close()方法時,就有可能會導(dǎo)致連接池失效了。那么我們通常的解決辦法就是直接禁用掉連接池的功能。

在Android2.3版本的時候,加入了更加透明化的響應(yīng)壓縮。HttpURLConnection會自動在每個發(fā)出的請求中加入如下消息頭:Accept-Encoding: gzip,并處理相應(yīng)的返回結(jié)果。但是如果啟動了響應(yīng)壓縮的功能,HTTP響應(yīng)頭里的Content-Length就會代表著壓縮后的長度,這時再使用getContentLength()方法來取出解壓后的數(shù)據(jù)就是錯誤的了。正確的做法應(yīng)該是一直調(diào)用InputStream.read()方法來讀取響應(yīng)數(shù)據(jù),一直到出現(xiàn)-1為止。

在Android 2.3版本中還增加了一些HTTPS方面的改進,現(xiàn)在HttpsURLConnection會使用SNI(Server Name Indication)的方式進行連接,使得多個HTTPS主機可以共享同一個IP地址。除此之外,還增加了一些壓縮和會話的機制。如果連接失敗,它會自動去嘗試重新進行連接。這使得HttpsURLConnection可以在不破壞老版本兼容性的前提下,更加高效地連接最新的服務(wù)器。

在Android4.0版本中,我們又添加了一些響應(yīng)的緩存機制。當緩存被安裝后(調(diào)用HttpResponseCache的install()方法),所有的HTTP請求都會滿足以下三種情況:

  • 所有的緩存響應(yīng)都由本地存儲來提供。因為沒有必要去發(fā)起任務(wù)的網(wǎng)絡(luò)連接請求,所有的響應(yīng)都可以立刻獲取到。

  • 有條件的緩存響應(yīng)必須要有服務(wù)器來進行更新檢查。比如說客戶端發(fā)起了一條類似于“如果a.png這張圖片發(fā)生了改變,就將它發(fā)送給我” 這樣的請求,服務(wù)器需要將更新后的數(shù)據(jù)進行返回,或者返回一個304(Not Modified)狀態(tài)。如果請求的內(nèi)容沒有發(fā)生,客戶端就不會下載任何數(shù)據(jù)。

  • 沒有緩存的響應(yīng)都是由服務(wù)器直接提供的。這部分響應(yīng)會在稍后存儲到響應(yīng)緩存中。

在Android2.2版本之前,HttpClient擁有較少的bug,因此使用它是最好的選擇。而在Android2.3版本及以后,HttpURLConnection則是最佳的選擇。它的API簡單,體積較小,因而非常適用于Android項目。壓縮和緩存機制可以有效地減少網(wǎng)絡(luò)訪問的流量,在提升速度和省電方面也起到了較大的作用。對于新的應(yīng)用程序應(yīng)該更加偏向于使用HttpURLConnection,因為在以后的工作當中我們也會將更多的時間放在優(yōu)化HttpURLConnection上面。

以上解釋了在sdk為9以上使用HttpURLConnection的原因。下面還是回到Volley的源碼解析中來,使用HttpStack對象創(chuàng)建好Network之后,再根據(jù)Network和Cache來構(gòu)建RequestQueue對象,調(diào)用其start方法啟動,然后將RequestQueue對象返回,newRequestQueue方法結(jié)束。

接著來看看這個start方法方法內(nèi)部都做了什么。

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

首先要知道這個CacheDispatcher和這個NetworkDispatcher都是繼承自Thread,一開始停止所有線程,然后創(chuàng)建一個新的CacheDispatcher線程,啟動,然后創(chuàng)建默認4個NetworkDispatcher,啟動,當start方法執(zhí)行完后,Volley已經(jīng)啟動了5個線程等待網(wǎng)絡(luò)請求任務(wù)來處理了。CacheDispatcher是緩存線程,NetworkDispatcher是網(wǎng)絡(luò)請求線程。

在拿到RequestQueue對象后,我們會把我們的請求add到RequestQueue對象中去,其中add的方法如下:

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

先是給這個請求設(shè)置了一些屬性,然后根據(jù)該請求是否可以緩存,將其加入到不同的請求隊列中,在默認情況下,每條請求都是可以緩存的,當然我們也可以調(diào)用Request的setShouldCache(false)方法來改變這一默認行為。

這里補充一點,請求默認是緩存的,然后通過拿到請求的cacheKey,以鍵值對形式將請求緩存到緩存隊列中去,那么這里請求request的cacheKey是怎么來的?

看Request中這兩個方法:

public String getUrl() {
    return mUrl;
}

public String getCacheKey() {
    return getUrl();
}

其實cacheKey就是請求的url,因為這個請求肯定是唯一的。

既然默認每條請求都是可以緩存的,自然就被添加到了緩存隊列中,于是一直在后臺等待的緩存線程就開始處理請求了,我們再看看緩存線程CacheDispatcher的run方法:

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

可以看到有一個while(true)循環(huán),說明緩存線程始終是在運行的,接著會嘗試從緩存當中取出響應(yīng)結(jié)果,如果為空的話,則把這條請求加入到網(wǎng)絡(luò)請求隊列中,如果不為空的話,再判斷該緩存是否已過期,如果已經(jīng)過期了則同樣把這條請求加入到網(wǎng)絡(luò)請求隊列中,否則就認為不需要重發(fā)網(wǎng)絡(luò)請求,直接使用緩存中的數(shù)據(jù)即可。之后會調(diào)用Request的parseNetworkResponse()方法來對數(shù)據(jù)進行解析,再往后就是將解析出來的數(shù)據(jù)進行回調(diào)了,這部分代碼我們先跳過,因為它的邏輯和NetworkDispatcher后半部分的邏輯是基本相同的,等會再看,先來看一下NetworkDispatcher中是怎么處理網(wǎng)絡(luò)請求隊列的,同樣是NetworkDispatcher的run方法:

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

真正執(zhí)行網(wǎng)絡(luò)請求是調(diào)用Network的performRequest方法,而Network是一個接口,它的實現(xiàn)類是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 = 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();
            .
            .
            .
        } catch (SocketTimeoutException e) {
            attemptRetryOnException("socket", request, new TimeoutError());
        } 
        .
        .
        .
    }
}

我們看到實際上是調(diào)用了HttpStack的performRequest方法,而HttpStack前面已經(jīng)說過,就是HttpURLConnecttion或者HttpClient,請求到數(shù)據(jù)后封裝成NetworkResponse對象返回。在NetworkDispatcher的run方法中發(fā)送請求后返回NetworkResponse對象,接著調(diào)用了Request的parseNetworkResponse方法來解析響應(yīng)數(shù)據(jù)和把響應(yīng)數(shù)據(jù)存入緩存中,不同的Request的子類其parseNetworkResponse方法都不一樣,當我們自定義Request時也必需要重寫parseNetworkResponse方法的。

解析完之后,接著會調(diào)用mDelivery.postResponse方法來回調(diào)解析出來的響應(yīng)數(shù)據(jù),這個mDelivery是一個接口ResponseDelivery,結(jié)果分發(fā)器,它的實現(xiàn)類是ExecutorDelivery,負責分發(fā)響應(yīng)和錯誤數(shù)據(jù),其中的postResponse方法如下:

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

可以看到在mResponsePoster的execute()方法中傳入了一個ResponseDeliveryRunnable對象,它的作用是保證該對象中的run()方法就是在主線程當中運行,也就是把解析后的數(shù)據(jù)從子線程中切換到主線程了。

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

可以看到請求成功后調(diào)用了Request的deliverResponse()方法,這個方法就是我們在自定義Request時需要重寫的另外一個方法,每一條網(wǎng)絡(luò)請求的響應(yīng)都是回調(diào)到這個方法中,最后我們再在這個方法中將響應(yīng)的數(shù)據(jù)回調(diào)到Response.Listener的onResponse()方法中就可以了。

至于這個從主線程切換到子線程再到主線程的過程到底是怎樣的呢?我再來梳理一遍:

當我們創(chuàng)建RequestQueue時,在start方法中,會創(chuàng)建五個子線程,其中一個CacheDispatcher,默認四個NetworkDispatcher,他們的構(gòu)造方法中都有傳遞一個對象,就是這個mDelivery,它是一個ResponseDelivery接口,實現(xiàn)類是ExecutorDelivery,我們看看RequestQueue的這兩個構(gòu)造方法就清除了:

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, int threadPoolSize) {
    this(cache, network, threadPoolSize, new ExecutorDelivery(new Handler(Looper.getMainLooper())));
}

所以RequestQueue是自己構(gòu)建了這個ExecutorDelivery對象,然后再傳遞到后面的子線程后的,ExecutorDelivery的構(gòu)造方法參數(shù)就是一個主線程的handler對象,所以這個mDelivery內(nèi)部是持有一個主線程的消息系統(tǒng)的。然后在請求完成,解析完響應(yīng)數(shù)據(jù)后,調(diào)用了mDelivery的postResponse方法,在postResponse方法最后調(diào)用了一個Executor對象的execute方法方法,參數(shù)就是一個Runnable,并且將解析后的響應(yīng)數(shù)據(jù)傳給Runnable。我們再看看ExecutorDelivery的構(gòu)造方法:

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

由于這個消息系統(tǒng)是在主線程構(gòu)造,也就是說Runnable會被發(fā)送到主線程去執(zhí)行,這樣就把解析后的數(shù)據(jù)傳遞到主線程了。

現(xiàn)在再回過頭看源碼解析開頭的那張流程圖就比較清晰了,我們在主線程中調(diào)用RequestQueue的add()方法來添加一條網(wǎng)絡(luò)請求,這條請求會先被加入到緩存隊列當中,如果發(fā)現(xiàn)可以找到相應(yīng)的緩存結(jié)果就直接讀取緩存并解析,然后回調(diào)給主線程。如果在緩存中沒有找到結(jié)果,則將這條請求加入到網(wǎng)絡(luò)請求隊列中,然后處理發(fā)送HTTP請求,解析響應(yīng)結(jié)果,寫入緩存,并回調(diào)主線程。

Volley的源碼解析就結(jié)束了,其實源碼解析沒有必要每個類每個方法都去看一遍,我們只需要將這個框架的工作流程相關(guān)的源碼即可,主要是掌握框架的優(yōu)秀的設(shè)計思路以及一些良好的編碼實現(xiàn)。好了,就到這里。

參考資料

手撕 Volley(一)
手撕 Volley(二)
手撕 Volley(三)
Android Volley完全解析(一),初識Volley的基本用法
Android Volley完全解析(二),使用Volley加載網(wǎng)絡(luò)圖片
Android Volley完全解析(三),定制自己的Request
Android Volley完全解析(四),帶你從源碼的角度理解Volley
HTTP協(xié)議詳解
HTTP協(xié)議詳解
HttpClient和HttpURLConnection的區(qū)別

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,238評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,430評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,134評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,893評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,653評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,136評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,212評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,372評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,888評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,738評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,482評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,179評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,588評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,829評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,610評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,916評論 2 372

推薦閱讀更多精彩內(nèi)容