Volley框架介紹與源碼解析

Volley框架介紹

在開發(fā)一些調用服務器端API以獲得數據的應用時赤嚼,需要編寫繁瑣的基于HttpClient或HttpURLConnection的代碼與處理異步請求的線程管理操作演侯,如Android Studio自帶的LoginActivity模板旬昭,就實現(xiàn)了一個繼承自AsyncTask的臃腫內部類來處理一個簡單的登錄請求眯漩。

應運而生的諸多網絡通信框架中旧噪,Volley的優(yōu)點包括(翻譯自google官方文檔):

  1. 自動調度網絡請求
  2. 可以并發(fā)進行多個網絡通信
  3. 可操作的標準HTTP緩存(磁盤吨娜、內存)
  4. 支持請求優(yōu)先級
  5. 提供可以取消一個或多個請求的接口
  6. 可以簡易地自定義一些組件
  7. 通過排序從網絡異步獲取數據,正確地填充UI組件
  8. 提供調試與跟蹤工具

總的來說淘钟,Volley框架在輕量級與可拓展性上的優(yōu)越表現(xiàn)讓我們可以利用它進行一些頻繁但數據量小的網絡通信宦赠,利用內置的HTTP緩存功能也可以實現(xiàn)圖片資源的加載與緩存。

配置

Volley的開源地址為https://github.com/google/volley 米母,一般可以通過在項目里導入module的方式引入clone到本地的Volley庫勾扭,或者在項目的build.gradle中加入

dependencies {
    ...
    compile 'com.android.volley:volley:1.0.0'
}

的方式添加對volley的依賴。

框架使用的基本方法

Volley的常規(guī)使用方法(來自Google官方文檔):

final TextView mTextView = (TextView) findViewById(R.id.text);
...

// Instantiate the RequestQueue.
RequestQueue queue = Volley.newRequestQueue(this);
String url ="http://www.google.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) {
       // Display the first 500 characters of the response string.
       mTextView.setText("Response is: "+ response.substring(0,500));
   }
}, new Response.ErrorListener() {
   @Override
   public void onErrorResponse(VolleyError error) {
       mTextView.setText("That didn't work!");
   }
});
// Add the request to the RequestQueue.
queue.add(stringRequest);

相對來說調用過程十分簡潔铁瞒,主要的組件包括:用于調度妙色、并發(fā)執(zhí)行多個HTTP請求的RequestQueue,volley庫中的Request對象慧耍,傳入Request對象的Listener用于處理成功請求后的response身辨,傳入Request對象的ErrorListener用于執(zhí)行請求錯誤/異常后的相關操作。

宏觀來看芍碧,Volley框架的總體運行與調用結構為(圖片轉載自http://blog.csdn.net/geolo/article/details/43966171 ):

整體調用結構

可以看到煌珊,傳入RequestQueue的對象可以是現(xiàn)有的,StringRequest(獲得String格式的response)泌豆,JsonObjectRequest(可以傳入一個json對象作為post請求的params定庵,并返回json格式的response),ImageRequest(前文提過volley框架因為緩存的存在對圖片資源的加載與管理有很好的表現(xiàn)),實現(xiàn)Request抽象類獲得的自定義請求對象洗贰,實現(xiàn)方式非常簡潔找岖,可以參考官方文檔https://developer.android.com/training/volley/request-custom.html 的示例,這里不做贅述敛滋。

而RequestQueue管理了兩種線程许布,NetworkDispatcher與CacheDispatcher,分別處理網絡請求與緩存管理绎晃,內部利用底層的HttpClient或HttpURLConnection實現(xiàn)網絡通信蜜唾,利用磁盤緩存(CacheDispathcer運行時讀取文件寫入HashMap中由內存管理)實現(xiàn)緩存數據的持久化。

request生命周期

上圖是來自Google官方文檔的庶艾,request對象的生命期示意圖袁余,這里進行了主線程、緩存線程咱揍、網絡線程的區(qū)分颖榜,首先檢查該請求是否命中緩存,若命中則返回緩存結果煤裙,若丟失則加入NetworkDispatcher線程池進行網絡通信掩完,返回結果、寫入緩存硼砰。

下面通過閱讀volley庫中的一些源碼來詳細解析這個過程的具體實現(xiàn)方式且蓬。

Volley源碼解析

首先我們一般通過調用Volley.newRequestQueue(Context context)靜態(tài)方法獲得RequestQueue對象的實例(當然可以直接傳入RequestQueue需要的一些構造參數對象new出來,自己實現(xiàn)一個單例工廠管理RequestQueue對象是一個不錯的選擇题翰,適用于經常需要網絡通信的應用)恶阴,方法具體實現(xiàn)為:

/**
 * Creates a default instance of the worker pool and calls {@link RequestQueue#start()} on it.
 *
 * @param context A {@link Context} to use for creating the cache dir.
 * @param stack An {@link HttpStack} to use for the network, or null for default.
 * @return A started {@link RequestQueue} instance.
 */
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;
}

對于RequestQueue需要的兩個參數,Cache接口與Network接口豹障,volley庫中自帶了BasicNetwork與DiskBasedCache作為一種具體實現(xiàn)類冯事。

首先判斷設備的版本號是否為9及以上,若是則使用HurlStack作為Network實現(xiàn)類的參數沼填,否則使用HttpClientStack桅咆,前者內部封裝的網絡通信組件為HttpURLConnection括授,后者為HttpClient坞笙,這兩個基本組件的使用場景與android的版本迭代過程有關,因此這里不再做細致介紹薛夜。

public interface Network {
    /**
     * Performs the specified request.
     * @param request Request to process
     * @return A {@link NetworkResponse} with data and caching metadata; will never be null
     * @throws VolleyError on errors
     */
    NetworkResponse performRequest(Request<?> request) throws VolleyError;
}

Network接口提供了performRequest方法梯澜,很直觀地傳入request獲得response的過程晚伙,在BasicNetwork中漓帚,利用作為參數傳入的HttpStack接口尝抖,調用底層的網絡通信組件(前段已有介紹)獲得response昧辽,并添加了加入至緩存(后文會解釋)搅荞、異常處理取具、記錄長時請求暇检、嘗試重請求等封裝好的功能块仆。

public interface Cache {
Entry get(String key);

    void put(String key, Entry entry);

    void initialize();

    void invalidate(String key, boolean fullExpire);

    void remove(String key);

    void clear();

    class Entry {

        public byte[] data;

        public String etag;

        public long serverDate;

        public long lastModified;

        public long ttl;

        public long softTtl;

        public Map<String, String> responseHeaders = Collections.emptyMap();

        boolean isExpired() {
            return this.ttl < System.currentTimeMillis();
        }

        boolean refreshNeeded() {
            return this.softTtl < System.currentTimeMillis();
        }
    }

}

Cache接口提供了一個內部類Entry作為緩存的元數據,其中包括一個存儲responseHeaders的Map科汗,事實上Volley框架提供的緩存功能便是根據response的headers信息來管理緩存的刷新周期、有效時長等信息坤检,實現(xiàn)HTTP標準緩存早歇。DiskBasedCache利用讀寫文件實現(xiàn)了基于磁盤的緩存持久化晨另,CacheDispatcher線程會調用intialize()方法加數據從文件加載到內存的HashMap中拯刁,并對每個請求進行cache是否命中的操作。

Volley框架的這種設計方法支持了開發(fā)者對Cache與Network接口的自定義實現(xiàn)與擴展帚桩,我在這只對庫中自帶的兩個實現(xiàn)類簡單介紹账嚎,底層的實現(xiàn)方式并非是框架運作流程的重點所在郭蕉,因此不再貼上繁瑣的代碼與分析以免浪費閱讀者的時間召锈。

回到之前在Volley中創(chuàng)建的RequestQueue對象,在創(chuàng)建它的實例后梢薪,首先調用其start()方法:

public void start() {
    stop();  // Make sure any currently running dispatchers are stopped.
    // Create the cache dispatcher and start it.
    mCacheDispatcher = new CacheDispatcher(mCacheQueue, mNetworkQueue, mCache, mDelivery);
    mCacheDispatcher.start();

    // Create network dispatchers (and corresponding threads) up to the pool size.
    for (int i = 0; i < mDispatchers.length; i++) {
        NetworkDispatcher networkDispatcher = new NetworkDispatcher(mNetworkQueue, mNetwork,
                mCache, mDelivery);
        mDispatchers[i] = networkDispatcher;
        networkDispatcher.start();
    }
}

之前的介紹中提過,NetworkDispatcher與CacheDispatcher均為由ReuquestQueue管理的線程琐馆,他們都繼承自Thread類啡捶,同時運作的有一個CacheDispatcher與多個NetworkDispatcher(默認4個)×硕模可以看到兩種線程都傳入了mNetworkQueue(Request對象作為范型的優(yōu)先隊列)與mCache的引用勿她,因此它們可以對每個加入的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<>();
            }
            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;
    }
}

以上為處理新加入的request的方法,首先根據request.shouldCache()判斷請求是否可以加入緩存,若不可以直接加入mNetworkQueue,若可以則加mCacheQueue翘贮,途中會有一個判斷阻塞中的請求是否與新請求相同的過濾判斷。

接下來回到NetworkDispatcher與CacheDispatcher線程芍耘,看看他們都在執(zhí)行什么工作:

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

以上是CacheDispatcher的run()方法浸剩,首先initialize()持有的Cache對象,將緩存讀入內存重罪,接下來在while(true)的輪詢中,每次從mCacheQueue隊列中取出一個request,分別判斷是否命中緩存砸讳、緩存是否過期,若未命中或過期則加入mNetworkQueue常遂,否則直接返回緩存結果。

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

以上為NetworkDispatcher的run()方法纬傲,同樣是通過while(true)輪詢叹括,每次從mNetworkQueue中取出request报咳,調用Network接口的處理請求方法继低,獲得response對象柴底,后續(xù)自然也有相應的異常處理、錯誤碼處理鸿脓、response交給request的parseNetworkResponse()解析等操作拨黔,到這里流程就已經比較清晰了,解析操作大部分情況自帶的StringRequest與JsonObjectRequest已經足夠,volley也支持直接重寫該方法,可以參考上文鏈接中的示例绍撞。

源碼的解析有些雜亂,宏觀上還是線程的管理實現(xiàn)異步請求得院,加上一個與計算機中的二級緩存機制十分相似的緩存層實現(xiàn)傻铣,可以回顧一下之前的request生命周期圖,再次理解下三種線程的相互關系祥绞。

雖然寫了很多非洲,但volley框架使用起來非常簡潔鸭限,功能也很完善,之后有心情也許會給出一些具體應用項目中的實現(xiàn)場景(你就是想應付第三次博客啊喂A教ぁ)败京。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市梦染,隨后出現(xiàn)的幾起案子赡麦,更是在濱河造成了極大的恐慌,老刑警劉巖帕识,帶你破解...
    沈念sama閱讀 211,948評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件泛粹,死亡現(xiàn)場離奇詭異,居然都是意外死亡肮疗,警方通過查閱死者的電腦和手機晶姊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,371評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伪货,“玉大人们衙,你說我怎么就攤上這事〖詈簦” “怎么了蒙挑?”我有些...
    開封第一講書人閱讀 157,490評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長巍举。 經常有香客問我脆荷,道長,這世上最難降的妖魔是什么懊悯? 我笑而不...
    開封第一講書人閱讀 56,521評論 1 284
  • 正文 為了忘掉前任蜓谋,我火速辦了婚禮,結果婚禮上炭分,老公的妹妹穿的比我還像新娘脓诡。我一直安慰自己炸庞,他們只是感情好带猴,可當我...
    茶點故事閱讀 65,627評論 6 386
  • 文/花漫 我一把揭開白布姑丑。 她就那樣靜靜地躺著,像睡著了一般呀忧。 火紅的嫁衣襯著肌膚如雪师痕。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,842評論 1 290
  • 那天而账,我揣著相機與錄音胰坟,去河邊找鬼。 笑死泞辐,一個胖子當著我的面吹牛笔横,可吹牛的內容都是我干的竞滓。 我是一名探鬼主播,決...
    沈念sama閱讀 38,997評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼吹缔,長吁一口氣:“原來是場噩夢啊……” “哼商佑!你這毒婦竟也來了?” 一聲冷哼從身側響起厢塘,我...
    開封第一講書人閱讀 37,741評論 0 268
  • 序言:老撾萬榮一對情侶失蹤茶没,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后俗冻,有當地人在樹林里發(fā)現(xiàn)了一具尸體礁叔,經...
    沈念sama閱讀 44,203評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡牍颈,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,534評論 2 327
  • 正文 我和宋清朗相戀三年迄薄,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片煮岁。...
    茶點故事閱讀 38,673評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡讥蔽,死狀恐怖,靈堂內的尸體忽然破棺而出画机,到底是詐尸還是另有隱情冶伞,我是刑警寧澤,帶...
    沈念sama閱讀 34,339評論 4 330
  • 正文 年R本政府宣布步氏,位于F島的核電站响禽,受9級特大地震影響,放射性物質發(fā)生泄漏荚醒。R本人自食惡果不足惜芋类,卻給世界環(huán)境...
    茶點故事閱讀 39,955評論 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望界阁。 院中可真熱鬧侯繁,春花似錦、人聲如沸泡躯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,770評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽较剃。三九已至咕别,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間写穴,已是汗流浹背惰拱。 一陣腳步聲響...
    開封第一講書人閱讀 32,000評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留确垫,地道東北人弓颈。 一個月前我還...
    沈念sama閱讀 46,394評論 2 360
  • 正文 我出身青樓帽芽,卻偏偏與公主長得像,于是被迫代替她去往敵國和親翔冀。 傳聞我的和親對象是個殘疾皇子导街,可洞房花燭夜當晚...
    茶點故事閱讀 43,562評論 2 349

推薦閱讀更多精彩內容