Volley源碼分析

目錄:
一、前言
二、基礎層
   2.1 緩存模塊
        2.1.1 Http緩存協議
        2.1.2 存儲設計
        2.1.3 緩存總結
   2.2日志記錄
        2.2.1 概念
        2.2.2 實例
        2.2.3 詳細分析
           2.2.3.1日志開關
           2.2.3.2 格式化日志信息
   2.3異常模塊
         2.3.1 類繼承結構
   2.4 重試機制
三、網絡層
   3.1.Http請求流程簡介
   3.2.網絡請求和響應對象封裝
      3.2.1.Request
      3.2.2 FIFO優先級隊列,請求排序
      3.2.3 Request中的抽象方法
      3.2.4 Response
   3.3網絡請求引擎 
       3.3.1 HttpStack里的HttpURLConnection、HttpClient
       3.3.2.OkHttp
   3.4網絡請求代理者
       3.4.1 NetWork的performRequest()方法工作流程圖
四、控制層
    4.1 Volley內部請求的工作流程
       4.1.1 如何調度網絡請求隊列
       4.1.2 如何取消網絡請求
    4.2 類的詳細解析
        4.2.1 線程類CacheDispatcher的內部工作流程圖
        4.2.2 線程類NetworkDispatcher的內部工作流程圖
        4.2.3 ExecutorDelivery的內部工作原理
五、應用層
      5.1 使用StringRequest發起一次請求
六、總結

   
前言

本文是一篇日常學習總結性的文章,筆者通過分析經典網絡框架Volley的源碼,望以鞏固Android網絡框架中常見的多線程、IO、設計模式、SDK開發等方面的技術。

我相信當前80%安卓開發者使用的網絡框架是Retrofit,但是我可以說99%開發者都沒有沒聽過Volley,因為它的設計思想是史詩級的、經典的、值得反復閱讀的。

在標準的網絡協議中,前人把網絡分不同層次進行開發,每一層分別負責不同的通信功能,同時每層之間能簡單交互,業內比較規范的說法叫做“高內聚,低耦合”。比如 TCP/IP,是一組不同層次上的多個協議的組合。如下圖:

TCP/IP協議簇的四個層次

所以,從上得到啟示,按照分層的原則,逐層撥開Volley源碼的面紗。

分層解耦

筆者認為Volley網絡框架有如下層次結構:

volley架構.png

二、基礎層

我們先看基礎層,它上層都會需要依賴它這一層。它又好比一個建筑的地基,保證了整個框架的健壯性。

在基礎層,我們能學習到:
1.緩存數據模塊:通用的緩存策略。
2.日志模塊:靈活地對事件打點。
3.異常模塊:統一的異常處理。
4.重試機制:網絡條件差情況下能提供重新請求的功能。

2.1 緩存模塊
2.1.1 Volley的緩存機制

遵循HTTP緩存協議需自備梯子:)
概括的說http緩存協議利用http請求頭中的cache-control和eTag字段進行是否緩存,緩存時間的判斷。
主要過程如下圖:

最佳 Cache-Control 策略.png

詳細流程請看上面提到的鏈接。

2.1.2 存儲設計
  • Cache接口:
    Cache接口,它制定了緩存的基本數據結構,即一個Key-Value鍵值對。其中Entry類作為值,它封裝了一個HTTP響應數據,對應下圖:
HTTP請求得到的響應頭.png

其中兩個重要的方法:

public Entry get(String key);//得到一個緩存對象
public void put(String key, Entry entry);//保存一個緩存對象
  • DiskBasedCache
    它是一個基礎磁盤緩存類,其數據結構表如下:
DiskBasedCache屬性.png

從表可知:
1.我們可以配置緩存的根目錄,用于獲取緩存文件對象和文件路徑。
2.字段最大可用緩存和可用緩存容量來判斷手機sdcard是否可以繼續保存緩存。

  • CacheHeader :
    CacheHeader的一個內部類,用來讀取和寫入文件大小和緩存時間的一個頭信息幫助類,除了文件實體內容。

DiskBasedCache的方法:

  • initialize():初始化DiskBasedCache,通過遍歷根目錄文件初始化DiskBasedCache。
    @Override
    public synchronized void initialize() {
        if (!mRootDirectory.exists()) {     -----1
            if (!mRootDirectory.mkdirs()) {
                VolleyLog.e("Unable to create cache dir %s", mRootDirectory.getAbsolutePath());
            }
            return;
        }
        File[] files = mRootDirectory.listFiles();  ------2
       ...
        for (File file : files) {  
            BufferedInputStream fis = null;
            try {
                fis = new BufferedInputStream(new FileInputStream(file));
                CacheHeader entry = CacheHeader.readHeader(fis);
                entry.size = file.length();
                putEntry(entry.key, entry);       --------3
            } catch (IOException e) {
             ...
    }

1.判斷緩存根目錄是否存在,不存在則新建。
2.遍歷緩存目錄。
3.依次讀取文件的頭信息,把所有已緩存的文件頭信息以key存入mEntries中(從磁盤讀到內存中)。

  • getFileNameForKey():得到文件名的hash值。把文件一分為二分別取hash值,后合并。

注意:這里得到唯一的文件名,并沒有采取常見的md5加密文件名的辦法,我認為這是考慮了字符串hash比md5加密哈希文件名時間效率更高。

    private String getFilenameForKey(String key) {
        int firstHalfLength = key.length() / 2;
        String localFilename = String.valueOf(key.substring(0, firstHalfLength).hashCode());
        localFilename += String.valueOf(key.substring(firstHalfLength).hashCode());
        return localFilename;  
    }
  • get(String key); 根據key獲取緩存對象。代碼片段如下:
/**
     * Returns the cache entry with the specified key if it exists, null otherwise.
     */
    @Override
    public synchronized Entry get(String key) {
        CacheHeader entry = mEntries.get(key);   ------1
        // if the entry does not exist, return.
        if (entry == null) {
            return null;
        }
        File file = getFileForKey(key);     ------2
        CountingInputStream cis = null;
        try {
            cis = new CountingInputStream(new BufferedInputStream(new FileInputStream(file)));
            CacheHeader.readHeader(cis); // eat header
            byte[] data = streamToBytes(cis, (int) (file.length() - cis.bytesRead));
            return entry.toCacheEntry(data);     ------3
        } catch (IOException e) {
         ...
         ...
        } finally {
                    cis.close();
    }

1.從mEntries這個map中匹配緩存在內存中的頭信息,沒有則返回null。
2.根據key查找磁盤中的緩存文件File。
3.將文件內容data字節解析到緩存對象Entry并返回。

總結:先讀取文件頭信息,然后讀取文件主體內容,最后兩者合并。

  • clear(); 在線程安全前提條件下,清除緩存文件:遍歷根目錄下面的所有文件,并刪除。代碼片段如下:
  public synchronized void clear() {
      File[] files = mRootDirectory.listFiles();
        if (files != null) {
            for (File file : files) {
                file.delete(); //刪除磁盤中的文件
            }
        }
        mEntries.clear(); //清理內存
        mTotalSize = 0;//重新初始化總大小
        VolleyLog.d("Cache cleared.");
}
  • put(String key, Entry entry); 在線程安全前提條件下,保存一個緩存文件。代碼片段如下:
 @Override
    public synchronized void put(String key, Entry entry) {
        pruneIfNeeded(entry.data.length);   //         ---------1
        File file = getFileForKey(key);
        try {
            BufferedOutputStream fos = new BufferedOutputStream(new FileOutputStream(file));
            CacheHeader e = new CacheHeader(key, entry);
            boolean success = e.writeHeader(fos);-----------2
            if (!success) {
                fos.close();
                VolleyLog.d("Failed to write header for %s", file.getAbsolutePath());
                throw new IOException();
            }
            fos.write(entry.data);  -------------3
            fos.close();
            putEntry(key, e);    ----------------4
            return;
        } catch (IOException e) {
        }
        boolean deleted = file.delete();
        if (!deleted) {
            VolleyLog.d("Could not clean up file %s", file.getAbsolutePath());
        }
    }

1.首先,判斷本地緩存容量是否足夠,不夠則遍歷刪除舊文件,直到容量足夠時停止。
2.把Entry的頭信息寫入本地文件中
3.把Entry的實體數據寫入本地文件中
4.把從Entry得到頭信息CacheHeader緩存到內存Map中。

2.1.3緩存總結

至此,緩存模塊分析完畢,其中有幾點技巧值得我們學習:
1.把文件保存分成內存和磁盤兩部分。內存保存頭信息(文件的大小、過期時間等),磁盤則保存文件的全部數據。這樣每次讀取緩存時可以先讀內存判斷頭信息是否合法,若合法則去本地讀取文件數據,否則放棄,這樣可以減少耗時的文件讀取時間,提升效率。
2.在文件信息安全的前提下,可以根據文件名字符串一分為二hash的辦法,取代md5加密辦法。

2.2日志記錄
2.2.1 概念

日志記錄主要用于程序運行期間發生的事件,以便于了解系統活動和診斷問題。它對于了解復雜系統的活動軌跡至關重要。
這段話摘自維基百科,可見日志記錄在SDK開發或組件開發中扮演著不可或缺的角色,它可便于調試,保證開發系統的穩定性。

2.2.2 實例

了解了日志記錄的概念后,那么我們看看Volley框架是如何記錄日志:
首先客戶端先發起一個網絡請求:

String url = "http://www.fulaan.com/forum/fPostActivityAll.do?sortType=2&page=1&classify=-1&cream=-1&gtTime=0&postSection=&pageSize=25&inSet=1";
            StringRequest stringRequest = new StringRequest(Request.Method.GET, url, new Response.Listener<String>() {
                @Override
                public void onResponse(String response) {
                    mResultView.setText(response);
                    stopProgress();
                }
            }, new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    stopProgress();
                    showToast(error.getMessage());
                }
            });
Volley.addQueue(stringRequest);

然后我們看控制臺logcat輸出的信息:

07-03 16:09:43.350 11447-11518/com.mani.volleydemo V/Volley: [155] CacheDispatcher.run: start new dispatcher
07-03 16:09:45.310 11447-11519/com.mani.volleydemo D/Volley: [156] BasicNetwork.logSlowRequests: HTTP response for request=<[ ] http://www.fulaan.com/forum/fPostActivityAll.do?sortType=2&page=1&classify=-1&cream=-1&gtTime=0&postSection=&pageSize=25&inSet=1 0xd41c847b NORMAL 1> [lifetime=793], [size=187392], [rc=200], [retryCount=0]
07-03 16:09:45.546 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (1032 ms) [ ] http://www.fulaan.com/forum/fPostActivityAll.do?sortType=2&page=1&classify=-1&cream=-1&gtTime=0&postSection=&pageSize=25&inSet=1 0xd41c847b NORMAL 1
07-03 16:09:45.547 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+0   ) [ 1] add-to-queue
07-03 16:09:45.547 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+1   ) [155] cache-queue-take
07-03 16:09:45.549 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+1   ) [155] cache-hit-expired
07-03 16:09:45.549 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+0   ) [156] network-queue-take
07-03 16:09:45.549 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+794 ) [156] network-http-complete
07-03 16:09:45.551 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+4   ) [156] network-parse-complete
07-03 16:09:45.551 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+2   ) [156] network-cache-written
07-03 16:09:45.551 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+0   ) [156] post-response
07-03 16:09:45.551 11447-11447/com.mani.volleydemo D/Volley: [1] MarkerLog.finish: (+230 ) [ 1] done
2.2.3 詳細代碼分析

我們可以看到在第一段代碼中并沒有使用android.util.log包中的Log.d(TAG,"")來打印信息,那這些日志是從哪里打印的呢?不難發現,這些日志記錄是由Volley框架內部類VolleyLog打印出來的。

另外,從控制臺的輸出信息,我們了解到日志打點了一些關鍵信息:事件名稱、事件發生的開始時間、線程ID、事件消耗總時間、事件內容。即“what 、where 、when”。這樣我們通過看控制臺日志,就能完整地跟蹤一個網絡請求,提升debug的效率。

先不急,我們看看內部代碼在哪里首先發起了打印的指令:
在Request類中:

                    ~~~~~
// Perform the network 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");
                ~~~~~
                if (request.shouldCache() && response.cacheEntry != null) {
                ~~~~~
                    request.addMarker("network-cache-written");
                }

上面的代碼的意思是:當一個request對象被調度時,使用request.addMarker("something")的方式用來進行“事件打點”,它記錄一個網絡請求整個生命周期。
其中request.addMarker("something")內部代碼是:

   /**
     * Adds an event to this request's event log; for debugging.
     */
    public void addMarker(String tag) {
        if (MarkerLog.ENABLED) {
            mEventLog.add(tag, Thread.currentThread().getId());
        }
    }

上述代碼中mEventLog是VolleyLog的一個內部類MarkerLog的實例。
到了這里大家可能會迷糊MarkerLog又是啥,VolleyLog是啥。為什么我們有了android.util.Log這個Log類后還需要自己定制VolleyLog呢?

  • VolleyLog
/**
 * Logging helper class.
 * <p/>
 * to see Volley logs call:<br/>
 * {@code <android-sdk>/platform-tools/adb shell setprop log.tag.Volley VERBOSE}
 */
public class VolleyLog {
    public static String TAG = "Volley";

    public static boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE);

    /**
     * Customize the log tag for your application, so that other apps
     * using Volley don't mix their logs with yours.
     * <br />
     * Enable the log property for your tag before starting your app:
     * <br />
     * {@code adb shell setprop log.tag.<tag>}
     */
    public static void setTag(String tag) {
        d("Changing log tag to %s", tag);
        TAG = tag;

        // Reinitialize the DEBUG "constant"
        DEBUG = Log.isLoggable(TAG, Log.VERBOSE);
    }  

    public static void e(String format, Object... args) {
        Log.e(TAG, buildMessage(format, args));
    }

    /**
     * Formats the caller's provided message and prepends useful info like
     * calling thread ID and method name.
     */
    private static String buildMessage(String format, Object... args) {
        String msg = (args == null) ? format : String.format(Locale.US, format, args);
        StackTraceElement[] trace = new Throwable().fillInStackTrace().getStackTrace();

        String caller = "<unknown>";
        // Walk up the stack looking for the first caller outside of VolleyLog.
        // It will be at least two frames up, so start there.
        for (int i = 2; i < trace.length; i++) {
            Class<?> clazz = trace[i].getClass();
            if (!clazz.equals(VolleyLog.class)) {
                String callingClass = trace[i].getClassName();
                callingClass = callingClass.substring(callingClass.lastIndexOf('.') + 1);
                callingClass = callingClass.substring(callingClass.lastIndexOf('$') + 1);

                caller = callingClass + "." + trace[i].getMethodName();
                break;
            }
        }
        return String.format(Locale.US, "[%d] %s: %s",
                Thread.currentThread().getId(), caller, msg);
    }

上述代碼中,通過對android.util.log類的封裝,VolleyLog類有兩個優勢地方值得我們學習:

2.2.3.1 日志開關

boolean DEBUG=Log.isLoggable(TAG,VERBOSE)這個是系統級別方法,Log.isLoggable()會去調用底層native代碼判斷當前日志系統是否能打印VERBOSE以上級別的日志,默認情況下是不可以的。所以如果Volley框架隨我們的app發布到了線上,默認我們看不到之前我們打印的日志信息,這樣可以減少日志輸出帶來的性能消耗。
那么怎么在調試階段返回為true呢? 很簡單,我們只需要在終端輸入命令:

adb shell setprop log.tag.Volley VERBOSE 

這樣再次運行app,我們就能看到Volley打印的日志信息了。

2.2.3.2 格式化日志信息

有經驗的同學應該知道控制臺有時候很多日志沒有規則,不利于閱讀,所以我們可以統一輸出的格式特別是在多線程中調試時,我們還可以打印出線程id,這樣更有利于我們跟蹤問題。那么我們看看是如何打印線程方法棧日志的:

 StackTraceElement[] trace = new Throwable().fillInStackTrace().getStackTrace();

上面一行代碼用來得到當前線程下方法調用的棧軌跡,并返回在一個數組中。
舉個例子:

 public void fetch(){
     VolleyLog.d("hello"); 
}

上述代碼的方法調用的棧軌跡軌跡應該是:
1.fetch();
2.VolleyLog.d();
3.buildmessage();

這樣我們就明白了 new Throwable().fillInStackTrace().getStackTrace();
的作用。至于為什么是從i=2的下標開始讀起,是因為會經歷d()、buildMessage()這兩個方法,而我們關注的是fetch()方法的線程id,和它的調用者className。
最后我們可以格式化字符串格式:

String.format(Locale.US, "[%d] %s: %s",Thread.currentThread().getId(), caller, msg);

這樣我們就能在控制臺得到如下的格式:

[1] MarkerLog.finish: (+1   ) [155] cache-hit-expired
2.3異常模塊

如果了解Http請求,那么應該知道請求網絡會碰到不同類型的錯誤響應,如請求密碼錯誤、請求超時、網絡地址解析錯誤、服務器錯誤等等,Volley為了統一處理網絡異常寫了一個基類VolleyError,它又拓展了如NetworkError、AuthFailureError等子類,下面是完整類圖:

異常繼承結構圖
異常繼承結構.png

這里就分析一個類別 AuthFailureError,代碼:

/**
 * Error indicating that there was an authentication failure when performing a Request.
 */
@SuppressWarnings("serial")
public class AuthFailureError extends VolleyError {
    /** An intent that can be used to resolve this exception. (Brings up the password dialog.) */
    private Intent mResolutionIntent;

    public AuthFailureError() { }

    public Intent getResolutionIntent() {
        return mResolutionIntent;
    }
    @Override
    public String getMessage() {
        if (mResolutionIntent != null) {
            return "User needs to (re)enter credentials.";
        }
        return super.getMessage();
    }

AuthFailureError是用來處理用戶認證請求時拋出的異常,它有個成員變量mResolutionIntent,當發生異常時,我們可以通過mResolutionIntent去顯示一個提示對話框,或者其他自定義方法來處理認證異常。
其他異常類同理,這里不做講解。

那么這么多異常類分別是在什么邏輯下拋出呢?這里賣個關子。在網絡層的時候我們將具體講解如何統一處理不同類別異常。

2.4 重試機制

重試機制是為了在網絡請求失敗或者超時的情況下,由網絡框架主動重新發起的網絡請求。

接口類RetryPolicy定義了重試主要協議:

volleyError.png

它定義了網絡重試機制的三個主要點:
1.超時時間(什么時候發起重試請求)
2.重試次數 (重試幾次后,如果失敗則停止重試)
3.發起重試命令,同時攜帶可能拋出的異常

根據上面三點要求,Volley內部提供了一個默認實現類:
DefaultRetryPolicy:

/**
 * Default retry policy for requests.
 */
public class DefaultRetryPolicy implements RetryPolicy {
    /** The current timeout in milliseconds. */
    private int mCurrentTimeoutMs;

    /** The current retry count. */
    private int mCurrentRetryCount;

    /** The maximum number of attempts. */
    private final int mMaxNumRetries;

    /** The backoff multiplier for the policy. */
    private final float mBackoffMultiplier;

    /** The default socket timeout in milliseconds */
    public static final int DEFAULT_TIMEOUT_MS = 2500;

    /** The default number of retries */
    public static final int DEFAULT_MAX_RETRIES = 1;

    /** The default backoff multiplier */
    public static final float DEFAULT_BACKOFF_MULT = 1f;

    /**
     * Constructs a new retry policy using the default timeouts.
     */
    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;
    }

    /**
     * Returns the current timeout.
     */
    @Override
    public int getCurrentTimeout() {
        return mCurrentTimeoutMs;
    }

    /**
     * Returns the current retry count.
     */
    @Override
    public int getCurrentRetryCount() {
        return mCurrentRetryCount;
    }

    /**
     * Returns the backoff multiplier for the policy.
     */
    public float getBackoffMultiplier() {
        return mBackoffMultiplier;
    }

    /**
     * Prepares for the next retry by applying a backoff to the timeout.
     * @param error The error code of the last attempt.
     */
    @Override
    public void retry(VolleyError error) throws VolleyError {
        mCurrentRetryCount++;
        mCurrentTimeoutMs += (mCurrentTimeoutMs * mBackoffMultiplier);
        if (!hasAttemptRemaining()) {
            throw error;
        }
    }

    /**
     * Returns true if this policy has attempts remaining, false otherwise.
     */
    protected boolean hasAttemptRemaining() {
        return mCurrentRetryCount <= mMaxNumRetries;
    }
}
  1. mCurrentTimeoutMs代表網絡請求時間連接范圍,超出范圍則重新請求
  2. mCurrentRetryCount已經完成的重試次數
  3. mMaxNumRetries最大允許的重試次數
    DefaultRetryPolicy的構造函數為我們默認初始化默認的超時次數和時間。
    我們重點看看retry(error)方法,
    mCurrentRetryCount++;每次重試請求則數量加1
    mCurrentTimeoutMs += 允許超時時間閥值也增加一定系數級別
    最后,hasAttemptRemaining如果超出最大重試次數,則拋出異常交給應用層開發者處理。
總結

除了系統提供的DefaultRetryPolicy,我們可以自己定制重試策略,只要是實現了RetryPlicy接口,mCurrentTimeoutMs值大小和重試次數可以自定義,例如,我們可以在電量充足的條件下重試次數更多,不管怎么樣,以達到“因地制宜”為最終目的。

三、網絡層

分析完基礎層后,我們看看網絡層,它將使用到基礎層中Cache類和日志類、異常類,而網絡層主要職能是執行網絡請求。

3.1Http請求流程簡介
http請求.png
3.2網絡請求和響應對象封裝
3.2.1 Request

Volley框架把Http請求需要的頭信息封裝到了Request對象中,另外,Request類還處理了請求排序、通知傳送器派發請求響應的數據、響應錯誤監聽器。
Request數據結構:

request基本屬性.png
3.2.2FIFO優先級隊列,請求排序

3.2.2.1 首先用枚舉值標記優先級:

  /**
     * Priority values.  Requests will be processed from higher priorities to
     * lower priorities, in FIFO order.
     */
    public enum Priority {
        LOW,
        NORMAL,
        HIGH,
        IMMEDIATE
    }

3.2.2.2重寫compareTo函數:

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

這樣,隨后我們在控制層中,就可以按照先進先出的順序執行請求。

3.2.3Request中的抽象方法
 /**
     * Subclasses must implement this to parse the raw network response
     * and return an appropriate response type. This method will be
     * called from a worker thread.  The response will not be delivered
     * if you return null.
     * @param response Response from the network
     * @return The parsed response, or null in the case of an error
     */
    abstract protected Response<T> parseNetworkResponse(NetworkResponse response);

 /**
     * Subclasses must implement this to perform delivery of the parsed
     * response to their listeners.  The given response is guaranteed to
     * be non-null; responses that fail to parse are not delivered.
     * @param response The parsed response returned by
     * {@link #parseNetworkResponse(NetworkResponse)}
     */
    abstract protected void deliverResponse(T response);

上面的兩個方法交給上層---應用層去實現,根據注釋可了解到它們分別是解析網絡響應數據為具體類型、分發響應數據給上層。

3.2.4 Response

上面分析完Request后,我們來看看網絡響應類Response。
Response的作用:
Response的并沒有封裝網絡響應的數據(NetworkResonse類封裝了網絡響應數據),它主要是用來把網絡響應解析完畢后的數據響應給上層監聽器,它扮演Control的角色。

/**
 * Encapsulates a parsed response for delivery.
 *
 * @param <T> Parsed type of this response
 */
public class Response<T> {

    /** Callback interface for delivering parsed responses. */
    public interface Listener<T> {
        /** Called when a response is received. */
        public void onResponse(T response);
    }

    /** Callback interface for delivering error responses. */
    public interface ErrorListener {
        /**
         * Callback method that an error has been occurred with the
         * provided error code and optional user-readable message.
         */
        public void onErrorResponse(VolleyError error);
    }

    /** Returns a successful response containing the parsed result. */
    public static <T> Response<T> success(T result, Cache.Entry cacheEntry) {
        return new Response<T>(result, cacheEntry);
    }

    /**
     * Returns a failed response containing the given error code and an optional
     * localized message displayed to the user.
     */
    public static <T> Response<T> error(VolleyError error) {
        return new Response<T>(error);
    }

    /** Parsed response, or null in the case of error. */
    public final T result;

    /** Cache metadata for this response, or null in the case of error. */
    public final Cache.Entry cacheEntry;

    /** Detailed error information if <code>errorCode != OK</code>. */
    public final VolleyError error;

    /** True if this response was a soft-expired one and a second one MAY be coming. */
    public boolean intermediate = false;

    /**
     * Returns whether this response is considered successful.
     */
    public boolean isSuccess() {
        return error == null;
    }


    private Response(T result, Cache.Entry cacheEntry) {
        this.result = result;
        this.cacheEntry = cacheEntry;
        this.error = null;
    }

    private Response(VolleyError error) {
        this.result = null;
        this.cacheEntry = null;
        this.error = error;
    }
}
3.3網絡請求引擎
3.3.1 HttpStack

它作為Volley最底層的網絡請求引擎,它封裝了一次http請求。

public interface HttpStack {
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
        throws IOException, AuthFailureError;
}

HttpStack的實現類有兩個:
1.HurlStack,內部使用的網絡引擎是HttpURLConnection
2.HttpClientStack,,內部使用的網絡引擎是 HttpClient
所以發起網絡請求的代碼就是這兩個實現類去處理的。至于選擇哪個網絡引擎則根據android版本來定。

3.3.2OkHttp

既然httpStack是網絡引擎,那么我們可以用OkHttp這個性能更好的網絡引擎取代它,具體內容可參見其官網。

3.4.網絡請求代理者
網絡引擎.png

我們可以想象到在執行請求網絡前后,我們需要一個處理請求頭協議,和處理響應狀態碼、處理響應異常,這個時候我們把httpstack交給了NetWork這一層去處理請求前后的業務邏輯。
NetWork的performRequest()方法工作流程圖:

network.png
  • BasicNetwork的關鍵代碼
public class BasicNetwork implements Network {
    protected final HttpStack mHttpStack;
 @Override
    public NetworkResponse performRequest(Request<?> request) throws VolleyError {
        while (true) {  //循環執行,這里是用來失敗重連
          addCacheHeaders(headers, request.getCacheEntry());
             ....
~~~~~~~~~ 執行請求前
          httpResponse = mHttpStack.performRequest(request, headers);
~~~~~~~~~   執行請求后    
           return new NetworkResponse();
                   }     
            }  catch (ConnectTimeoutException e) {
                attemptRetryOnException("connection", request, new TimeoutError());
            } catch (IOException e) {   
                throw new NetworkError(networkResponse);
             }
    }

從上述BasicNetwork的代碼來看,network經歷了四個步驟:

1.mHttpStack.preformRequest()執行網絡請求前,讀取請求頭信息,如緩存標志,請求參數等。

2.在執行網絡請求后,我們讀取了statusCode請求狀態碼,它用來判斷是否請求成功,是否需要更新緩存等。如果成功我們返回NetWorkResponse對象。

NetWorkResponse:它充當網絡數據載體的角色,用來包裹網絡請求得到的數據.

3.如果發生連接超時我們將重新連接網絡請求:

while(true){
~~~~~~~~~~~~~~~~~~~~~
 attemptRetryOnException("connection", request, new TimeoutError());
~~~~~~~~~~~~~~~~~~~~~
}

因為preformRequest()方法里是一個while循環,通過拋出異常或者重試請求已經超過最大次數,循環才會退出。

4.如果是發生網絡請求錯誤,那么我們則拋出對應的異常,中止循環,這里就對應了在第一部分基礎層的異常機制,我們定義的那些異常派生類現在就派上用場了:

 throw new ServerError(networkResponse);
 throw new NetworkError(networkResponse);

四、控制層(核心)

至此,我們已經把網絡請求需要做的基礎工作已經全部做完,但Volley設計思想的核心從這里開始。

在控制層中Volley設計者解決了下面幾個問題。
1.如何調度網絡請求隊列?
2.如何取消網絡請求?

4.1 Volley請求的工作流程

回答這些問題之前我們看看Volley的工作流程圖:

volley-request.png

上幅圖中 ,隊列的調度主要發生在RequestQueue類中。

4.1.1 如何調度網絡請求隊列?

當調用requestQueue.start()時,RequestQueue將會啟動一個用于處理緩存的線程和一個處理網絡請求的線程池。當你把request對象放入隊列,它首先會被一個緩存線程給接收處理,如果緩存命中了,那么對應原先緩存好的響應數據將在緩存線程解析,解析完的數據將會派發到主線程。

如果request沒有被緩存命中,那么它將會被網絡隊列(network queue)接管,然后線程池中第一個可用的網絡線程將從隊列中把request取出來,執行HTTP處理,在工作線程解析原始響應數據,并把響應存入緩存,然后把解析完畢的響應數據派發到主線程。

需要注意的是,你可以在任意線程發起一個請求,而其中一些耗時的IO操作或者解析數據操作將會在子線程(工作線程)執行,但是最后得到的網絡響應數據還是會發送到主線程。

4.1.2 如何取消網絡請求?

為了取消一個請求,我們可以調用Request對象的cancel()方法。一旦取消請求完成,Volley保證你一定不會收到的請求后的響應處理。這個功能意味著你可以在activity的onStop()回調方法中取消那些即將發送的(pending)請求,同時你不必在你的響應處理中檢查getActivity() == null,或者一些恢復場景中再去處理請求,如onSaveInstanceState()回調中。
為了更好的利用這個行為,你應該謹慎地跟蹤所有發起的請求,以便你可以在合適的時間取消它們。

Example:你可以給每個請求設置一個tag,這樣你就能取消所有設置了這個tag的請求。例如,你能把Activity的實例作為所有請求的tag,那么在onStop()回調中你可以調用requestQueue.cancelAll(this)取消所有請求。相似的,在viewPager頁面中,你能給“獲得縮略圖”請求設置一個viewPager的tab名稱,這樣后,在滑動viewPager改變tab的時候,你能單獨取消那些已經隱藏的view的請求,新tab頁面的請求則不會被停止。

4.2類的詳細解析
4.2.1線程類CacheDispatcher的內部工作流程圖:
cacheDispacher.png
4.2.2 線程類NetworkDispatcher的內部工作流程圖:
networkDispatcher.png
4.2.3 ExecutorDelivery的內部工作原理:

ExecutorDelivery內部是通過mResponsePoster對象調用一個handler把響應數據派發到主線程:

 mResponsePoster = new Executor() {
            @Override
            public void execute(Runnable command) {
                handler.post(command);
            }
        };

五、應用層

5.1 使用StringRequest發起一次請求

前面我們了解到Request有兩個需要上層重寫的抽象函數:

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

abstract protected void deliverResponse(T response);

我們看看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));
    }

把參數reponse.data用utf-8的編碼方式得到一個String,并返回一個包裝好的Resonse<String>對象。

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

這里是觸發響應回調。

如果我們想得到一個JsonOject,或者具體的實體類,那么我們重寫parseNetworkResponse()函數即可。這個函數會在netWork.proformRequest()里面調用。

如果我們想在UI線程得到響應數據之前做些其他操作(如顯示進度條),那么我們重寫deliverResponse()函數即可。這個函數會在ResponseDeliver里面調用。

至此我們看看在UI線程我們發起一個請求的完整過程:

 String url = "http://www.fulaan.com/forum/fPostActivityAll.do?sortType=2&page=1&classify=-1&cream=-1&gtTime=0&postSection=&pageSize=25&inSet=1";
    StringRequest stringRequest = new StringRequest(url, new Response.Listener<String>() {
      @Override public void onResponse(String response) {
        Log.d(TAG, "onResponse: " + response);
      }
    }, new Response.ErrorListener() {
      @Override public void onErrorResponse(VolleyError error) {
        Log.d(TAG, "VolleyError: " + error);
      }
    });
    stringRequest.setTag(this);
    VolleyUtil.getInstance(this).addRequestQueue(stringRequest);

簡單說明下步驟:
1.構造一個stringRequest,
2.實現錯誤監聽和成功響應回調函數
3.給request設置tag,用于取消。
4.把stringRquest交給控制器RequestQueue去自行調度執行。

六、總結

至此,分析完了Volley源碼框架,冥想片刻。
完成這篇博客,我學到了如何一步步構建一套網絡框架。后續計劃是對比分析retrofit網絡框架設計思想,另外我想如果做圖片框架、斷點文件多線程下載等,Voley給我的設計思想都有很好的參考價值。

不管怎么樣,還請讀者多多指教與共勉~多謝!

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容