溫故而知新 Volley源碼解讀與思考

来源:http://www.cnblogs.com/sphere/archive/2017/10/27/7745304.html
-Advertisement-
Play Games

相比新的網路請求框架Volley真的很落後,一無是處嗎,要知道Volley是由google官方推出的,雖然推出的時間很久了,但是其中依然有值得學習的地方。 從命名我們就能看出一些端倪,volley中文意為群射,齊射,官方解釋說它適合通信頻繁但是數據量不大的網路請求操作( a burst or emi ...


  相比新的網路請求框架Volley真的很落後,一無是處嗎,要知道Volley是由google官方推出的,雖然推出的時間很久了,但是其中依然有值得學習的地方。  從命名我們就能看出一些端倪,volley中文意為群射,齊射,官方解釋說它適合通信頻繁但是數據量不大的網路請求操作( a burst or emission of many things or a large amount at once ),至於為什麼我們解讀完源碼就知道了。

  回想下使用Volley的過程:比如請求一個網頁的內容。

  1. 創建RequestQueue對象

 RequestQueue mQueue = Volley.newRequestQueue(MyApplication.getInstance());

  2. 先創建一個StringRequest對象

private StringRequest stringRequest = new StringRequest(
            Request.Method.GET,
            "https://www.baidu.com",
            new Response.Listener<String>() {
                @Override
                public void onResponse(String response) {
                    Log.d(TAG, "current thread :" + Thread.currentThread().getName());  // main thread
                    ((TextView)findViewById(R.id.content)).setText(response);
                }
            },
            new Response.ErrorListener() {
                @Override
                public void onErrorResponse(VolleyError error) {
                    Log.d(TAG, "error :" + error.getMessage());
                }
            }
    ) ;
View Code

  3.  將請求對象添加到mQueue中

mQueue.add(stringRequest);

  

  如下流程描述請自行結合Volley中的源碼閱讀(需要說明的是本文分析的Volley代碼不是最新版本,還是1.0.x的版本):

 

  請求執行流程:

  首先我們要構造RequestQueue, 其內部封裝了緩存請求隊列:

  首先我們要構造RequestQueue, 其內部封裝了緩存請求隊列PriorityBlockingQueue<Request<?>> mCacheQueue 和網路請求隊列 PriorityBlockingQueue<Request<?>> mNetworkQueue,同時也封裝了一條緩存調度線程mCacheDispatcher和若幹條網路請求調度線程 NetworkDispatcher[] mDispatchers,雖然RequestQueue的構造方法是public,但是我們還是調用Volley的newRequestQueue方法,因為在newRequestQueue方法有些重要的處理,比如設置DiskBasedCache的目錄, 添加請求的User-agent,判斷SDK的版本號,如果是2.3(API=9)以下則使用HttpClient, 如果是>=2.3的版本,則使用HttpUrlConnection,接著構建RequestQueue對象,並調用其start方法,創建並啟動緩存調度線程和網路請求調度線程,目前的版本是1條緩存線程和4條網路請求線程。

  接著查看RequestQueue.add的相關邏輯:

   將構造的Request添加到RequestQueue中,即調用RequestQueue.add方法,這裡會將請求先Add到一個Set集合中,即Set<Request<?>> mCurrentRequests中,然後判斷是否禁用了緩存,如果禁用緩存則直接添加到mNetworkQueue中, 又因為NetworkDispatcher調度線程run方法中是while死迴圈,會一直取隊列中的對象,故加入網路請求隊列後,就相當於直接發起了網路請求。 而如果允許緩存,即Request.shouldCache返回true,則判斷Map(Map<String,Queue<Request<?>> mWaitingRequests中是否有相同的請求,判斷的標準就是請求的url,即request.getCacheKey()),如果mWaitingRequests中存在,則做提示處理,如果不存在則將請求添加到map中做記錄,並執行mCacheQueue.add(request)

   請求加入了CacheQueue隊列中,則緩存調度線程就可以從隊列中取出requeset做處理。查看緩存調度線程CacheDispatcher的run方法,while迴圈中的邏輯如下,先取出緩存queue中的請求對象request,根據請求的url得到cache, 判斷cache中entry是否為空,如果為空則說明沒有緩存,則將請求添加到mNetworkQueue中,mNetworkQueue.put(request), 交由網路請求線程處理。如果有緩存,判斷緩存是否過期,如果過期則同上,如果緩存可用,則取出緩存中數據做解析並返回,即調用request.parseNetworkResponse方法,解析之後調用mDelivery.postResponse方法做結果的投遞,這裡就將操作從子線程轉移到主線程了,具體是由mDelivery去處理切換的操作, mDelivery(具體實現類是ExecutorDelivery)內部封裝了Handler和Executor,將最終解析出的結果投遞到主線程handler.post(runnable), 此handler是主線程的handler,構造RequestQueue隊列時創建了主線程的Handler對象了,代碼如下:

public RequestQueue(Cache cache, Network network, int threadPoolSize) {

        this(cache, network, threadPoolSize,

                new ExecutorDelivery(new Handler(Looper.getMainLooper())));

}

    5. 當請求添加到網路請求隊列queue之後,在NetworkDispatcher的run方法中執行真正的網路請求,首先會判斷線程是否退出了,或者request是否被取消了等邏輯,一切ok則執行mNetwork.performRequest(request),發起網路請求,然後解析結果,做緩存操作,派發解析結果到主線程等等

 

// 這裡註意BlockingQueue的add offer put//// remove poll take peek等方法的區別

  1.add 將元素插入queue中,如果立即可行且不違反容量規則返回true,如果當前沒有可用空間,則拋出IllegalStateExecption

        2.offer 與add方法類似,但是使用有限制容量的queue時,此方法通常優於add方法,後者可能可能無法插入元素,只是拋出一個異常

  3. put 插入元素到queue尾部,如果空間不夠,則等待空間變得可用

  -----------------------------------------------------------------------------------------------------------------------------------

    4. remove 移除元素,返回true如果queue總包含此元素

       5.poll  獲取並移除頭部元素, E poll(), 如果queue為空,則返回null

  6.take  獲取並移除頭部元素,如果沒有則等待直到有頭部元素變得可用, E take() throws InterruptedException。

  7.peek 只是獲取頭部元素,並不做移除操作,如果queue為空,則返回null。

 

緩存執行流程

    上面簡要分析了請求執行的過程,那麼Volley是如何實現緩存和獲取緩存的呢,我們接著分析,試想我們第一次請求某個網路資源時,必然是沒有緩存的,那麼最終會走到網路調用線程NetworkDispatcher  run方法中的邏輯,執行網路請求拿到NetworkResponse,然後解析networkResponse,即調用request的parseNetworkResponse得到Response對象,然後判斷request是否允許緩存,如果需要緩存且response中的Cache.Entry即緩存對象不為空,則做緩存的操作。Cache.Entry對象cacheEntry什麼時候被賦值的呢?就是在parseNetworkResponse返回Response對象的過程中,構造Response對象調用Response.success(result, HttpHeaderParser.parseCacheHeaders(response));, success函數的第二參數即為cacheEntry,查看parseCacheHeaders方法可以看到,entry中包含有data, etag,softTtl,lastModified,responseHeaders等數據。我們要緩存就是上邊的cacheEntry,對應代碼中的mCache.put(request.getCacheKey(), response.cacheEntry); 這裡的mCache又是什麼呢。查找mCache的源頭又回到了Volley.newRequestQueue方法中,這裡構建RequestQueue時傳入了DiskBasedCache,那麼看來mCache的具體實現類就是DiskBasedCache了。查看DiskBasedCache的源碼,可以看到其預設緩存路徑是/data/data/packagename/cache/volley/  , 預設的緩存大小為10M,其中最關鍵的就是put方法,put(String key, Entry entry) ,此方法首先會根據entry中data數組的長度判斷是否能夠緩存得下,也就是緩存後是否超過了設定的最大緩存容量值。具體在pruneINeed中做判斷,如果超過最大值,則會按順序依次從已緩存的文件中做刪除操作(PS:如何做到按順序刪除呢,因為在putEntry方法中將key和cacheHeader的信息存儲在了LinkedHashMap中了, 所以刪除的時候才能依次按照緩存的先後順序刪除,最先緩存的先被刪除掉),直到緩存本次data不再超過最大值為止,然後創建一個File對象存儲緩存數據,File的name是將Url字元串的前半部分的hashcode加上字元串後半部分的hashcode組合而成,具體請查看getFilenameForKey(String key)方法,然後構建FileOutputStream對象分別將CacheHeader信息和data數據部分信息寫入文件,如果寫入的過程中發生了異常,則會做刪除文件的處理。至於讀取的操作請查看get方法.

 

  ClearCacheRequest請求執行流程

  可以看到在toolbox包下有一個ClearCacheRequest的類,看名字大概能猜測出來它是做清除緩存操作的。因為我們已經知道在Volley中的緩存邏輯是在DiskBasedCache中,查看DiskBasedCache中的的代碼,可以找個一個clear方法, 我們可以在此方法的第一行打上斷點,然後構造一個ClearCacheRequest對象,並添加到請求隊列中(在構造ClearCacheRequest方法中需要傳遞兩個參數,一個是mCache,一個是Runnable,其實mCache就是我們內部實現緩存的引用,Runnable可以做Clear後主線程上的操作), 啟動調試模式,可以看到其執行流程 CacheDispatcher.run --- > ClearCacheReqeuest.isCanceled -->

DiskBasedCache.clear方法,其中ClearCacheRequest的isCanceled方法與其他xxxRequest的isCancled方法不同,其內部調用了mCache.clear() ,,並將Runnable對象投遞到主線程的消息隊列中,如果mCallback不為空的話。在DiskBasedCache的clear方法中則分別做了對文件緩存刪除 和對記憶體緩存mEntries clear的操作。  

 

  網路請求流程

  發起網路請求的邏輯在BasicNetwork的performRequest方法中,我們可以看到方法內部使用的是while死迴圈也就是說要麼得到請求的結果,要麼拋出異常。 而使用while迴圈也是重試機制的關鍵。 先看下大致的流程, 添加請求的header (這裡會從CacheHeader中獲取,如果entry不為空,取出etag,headers.put("If-None-Match", etag, 取出lastModified,headers.put("If-Modified-Since", lastModified)) --> 發起網路請求 mHttpStack.performRequest --> 得到response ---> 解析response --> 返回NetworkResponse。 如果返回的狀態碼statusCode == 304 ,那麼說明伺服器在對比etag和lastModified後發現資源沒有修改過,客戶端直接使用緩存即可, 如果返回的狀態碼是301或302,則說明請求的資源移動了位置,需要重定向,我們取出響應頭中的location信息,調用request.setRedirectUrl(url), 而後由於邏輯的處理返回的狀態碼不是2XX則會拋出IOException異常, 在catch的處理中會再次判斷狀態碼並調用attemptRetryOnException,而此方法中的預設重試代理是DefaultRetryPolicy, 那麼這個RetryPolicy是在哪設置的呢,查看Request的構造方法不難發現, 其中有setRetryPolicy(new DefaultRetryPolicy()) 的身影, 其retry方法中會對重試次數做判斷,如果超過最大重試次數,則拋出異常,那麼performRequest方法也會終止執行,如果小於等於最大重試次數則while迴圈的邏輯會再次執行,直到有結果。 其中需要註意到一點, 因為預設的連接超時時間較短只有2500ms,(不管是HttpClientStack的PerformRequest方法還是HurlStack的openConnection方法都會拿到request中設置的超時時間 int time = request.getTimeOutMs();在國內複雜的網路環境中可能從發起請求到響應時間會超過此值,一旦超過此值Volley預設則認為是超時了,從而觸發重試的機制,導致一個請求發送兩次的情況。解決的辦法是可以增大預設超時的時間值,比如設置5000ms,或者設置不使用重試機制。

request.setRetryPolicy(new DefaultRetryPolicy(DefaultRetryPolicy.DEFAULT_TIMEOUT_MS, 0, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT)); 關於這個問題Volley的github庫issue中也有提及:https://github.com/google/volley/issues/7

 

 

其實說了這麼多,還是下麵這張流程圖的內容:

 

現在來做下問題總結:

1. 為什麼說Volley不適合大文件的下載等操作,而是數據量小的通信網路場景?

  因為從Volley的源碼中我們可以發現,其內部執行網路請求的線程是固定數量4條線程,如果下載大文件可能就會導致線程被長時間占用,後面排隊的Request可能長時間得不到執行,且在Volley內部有緩存機制,如果大文件也允許緩存,而設定的最大緩存容量值較小,則可能發生長時間的IO操作(因為可能超過最大容量而要做刪除文件操作),導致應用性能下降。

2. Volley中的緩存調度線程和網路調用線程的run方法中是while死迴圈,什麼時候退出,也就是緩存和網路調度線程什麼時候結束工作?

  其實在run方法的內部有相關邏輯, 比如NetworkDispatcher的run方法中,會捕獲InterruptedException異常,在異常處理中判斷mQuit的值,如果為true則直接返回。而調用Interrupt方法和設置mQuit值的處理就在NetworkDispatcher對應的quit() 方法中。

3.  可否將處理網路請求的線程改成線程池ThreadPoolExecutor?

  可以改,但是即使改為線程池實現,性能可能也不會有提升,一方面對於手機cpu來說其核心數是有限的,如果線程池內的線程數配置的較大,則網路請求時可能導致線程的頻繁的發生切換,而線程的切換是有開銷的。

4. Volley可否載入較大的圖片,比如十幾M,幾十M等?

  因為Volley中解析完數據是要保存在byte[] data,中的,所以如果數據過大則有可能發生OOM異常。https://github.com/google/volley/issues/12

5. 使用Volley時應該在哪裡創建RequestQueue合適?

  具體可以在自定義的Application中,主要是傳遞給newRequestQueue的Context應該使用ApplicationContext,這樣可以避免可能發生的記憶體泄漏的情況,試想如果持有Activity的context那麼Volley內部的工作沒有做完則一直持有Activity,導致Activity無法釋放,故在自定義的Apllication初始化一個全局的請求隊列即可。

6. onResponse是在主線程中執行,但是返回結果後還需要做耗時操作怎麼辦?

  從Volley的源碼中我們能夠知道派發器mDelivery的是ExecutorDelivery,其預設實現是傳遞主線程的handler的構造方法,而ExecutorDelivery的內部還有一個傳遞executor的構造方法,只要構建一個的executor,在new RequestQueue時,讓 mDelivery = new ExecutorDelivery(executor), 那麼onResponse最終就在executor的線程中執行, 不再是主線程了。

7. 如何取消某個或者多個網路請求?

  取消單個request可以調用request.cancel(), 如果是多個可以給某個類別的request設置一個tag,想要取消請求調用requestQueue.cancelAll(tag),調用cancel方法後Request內的屬性mCanneled即被覆製為true,在CacheDispatcher或者NetworkDispatcher的run方法中會對request.isCanceled做判斷。如果是取消多個請求,調用cancelAll 方法,則會在當前的請求集合中進行遍歷,找到tag一致的request。

7. Volley有什麼優缺點。

  優點:  

  還是那句: 適合網路通信頻繁,但是通信數據量不大的請求,不適合大文件的下載。

  可以緩存http請求,過濾重覆請求(一般網路請求框架也都支持)

      支持請求的優先順序

   支持取消請求的API,可以取消單個請求,也可以設置取消請求的範圍域

      基於介面的設計,使擴展相對容易(比如寫一個XMLRequest類 繼承Request,實現onResponse方法和parseNetworkResponse方法)

  缺點:

  對於文件的上傳和下載支持的不好

  與Apache的Httpclient 和 HttpUrlConnection耦合較緊密

  Android 6.0系統移除對HttpClient的支持,所以要使用Volley,需要配置org.apache.http.legacy.jar的引用

 

  https://github.com/google/volley/releases 最新的Volley是1.1.0的版本,修複瞭如下問題:

  • Apache HTTP is now an optional dependency (#2). See Migrating from Apache HTTP for details on how to avoid using it.
  • Fix OutOfMemoryErrors and NegativeArraySizeExceptions in DiskBasedCache (#12).
  • Fix memory leak in Request#mErrorListener (#15).
  • Support for multiple identical response headers (#21).
  • Fix potential NullPointerException in ImageRequest/JsonRequest/StringRequest (#64).
  • Fix soft TTL for duplicate in-flight requests (#73).
  • Fix case-sensitive header reads from cache (#76).

待補充。。。

  

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 實現效果: 實現代碼: myScroll.js ...
  • tab面板 ... ...
  • 一:通過margin負值 相容性好,但是必須要定寬高 二:通過margin:auto 以上兩種方法都可以把absolute換成fixed,註意,fixed在ie下不支持 三:行內元素居中 這種方法只能居中行內元素。常用於文字對其居中 四:transform居中 存在css3瀏覽器相容問題 五:fle ...
  • <!DOCTYPE html><html xmlns="http://www.w3.org/1999/xhtml"><head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title></title> ...
  • 註冊博客園有一年多了,每次都是來找點資料,從來沒有寫過點什麼,促使我開始寫博客的原因主要有兩點 一是在查找資料的過程中,經常需要重覆的查找某個知識點,一個知識點時間長了之後總是忘記,這樣重覆的過程卻是浪費了大量的時間,把東西記下來一方面是方便自己的查詢,一方面加深自己的理解。 二是提高自己的寫作能力 ...
  • UIScrollView有一個BOOL類型的tracking屬性,用來返回用戶是否已經觸及內容並打算開始滾動,我們從這個屬性開始探究UIScrollView的工作原理: 當手指觸摸到UIScrollView內容的一瞬間,會產生下麵的動作: 攔截觸摸事件 tracking屬性變為YES 一個內置的計時 ...
  • 我們先放一張Hierarchy Viewer的圖:(模擬器Android4.4) 看到數字6了嗎,那個RelativeLayout是MainActivity的根ViewGroup, 而在RelativeLayout之前還有5層, 即系統預設的界面DecorView(FrameLayout) --> ...
  • 更新完後試下運行正在維護的舊項目,出現各種錯誤,因為後來發現問題不在這,所以沒記完整,大概如下: A larger heap for the Gradle daemon is recommended for running jack. It currently has 512 MB.For fast ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...