前言
這幾天抽時間啃完了Volley和Picasso的原始碼,收穫頗多,所以在這裡跟大家分享一下。
對於網路請求框架或者圖片載入框架來說,我們的理想型大體應該是這樣的:
- 簡單:框架的出現當然是為了提升我們的開發效率,使我們的開發變得簡單,所以在保證質量的情況下簡單是第一位的
- 可配置:天底下沒有完全相同的兩片樹葉,也沒有完全相同的兩個專案,所以某些差異應該是可配置的,比如快取位置、快取大小、快取策略等等
- 方便擴充套件:框架在設計的時候就要考慮到變化,並且封裝起來。舉個例子,比如有了更好的Http客戶端,我們應該能很方便的修改並且不能對我們之前的程式碼產生太大影響
但萬變不離其宗,這些框架的骨架其實基本上都是一樣的,今天我們就來討論下這些框架中的套路。
基本模組
既然我們說這些框架的結構其實基本上都是一樣的,那麼我們就先來看看它們之間類似的模組結構。
整體流程大概是這樣的:
客戶端請求->生成框架封裝的請求型別->排程器開始處理任務->呼叫資料獲取模組->對獲取的資料進行處理->回撥給客戶端
生產者消費者模型
框架中請求管理和任務排程模組一般會用到生產者消費者模型。
為什麼會有生產者消費者模型
線上程世界裡,生產者就是生產資料的執行緒,消費者就是消費資料的執行緒。在多執行緒開發當中,如果生產者處理速度很快,而消費者處理速度很慢,那麼生產者就必須等待消費者處理完,才能繼續生產資料。同樣的道理,如果消費者的處理能力大於生產者,那麼消費者就必須等待生產者。為了解決這個問題於是引入了生產者和消費者模型。
什麼是生產者消費者模型
生產者消費者模式是通過一個容器來解決生產者和消費者的強耦合問題。生產者和消費者彼此之間不直接通訊,而通過阻塞佇列來進行通訊,所以生產者生產完資料之後不用等待消費者處理,直接扔給阻塞佇列,消費者不找生產者要資料,而是直接從阻塞佇列裡取,阻塞佇列就相當於一個緩衝區,平衡了生產者和消費者的處理能力。
生產者消費者模型的使用場景
Java中的執行緒池類其實就是一種生產者和消費者模式的實現方式,但是實現方法更高明。生產者把任務丟給執行緒池,執行緒池建立執行緒並處理任務,如果將要執行的任務數大於執行緒池的基本執行緒數就把任務扔到阻塞佇列裡,這種做法比只使用一個阻塞佇列來實現生產者和消費者模型顯然要高明很多,因為消費者能夠處理直接就處理掉了,這樣速度更快,而生產者先存,消費者再取這種方式顯然慢一些。
框架中的應用
對於上述的使用場景我們分別可以在框架中找到實現。
Volley原始碼中實現方式是用一個優先順序阻塞佇列來實現生產者消費者模型。生產者是往佇列裡新增資料的執行緒,消費者是一個預設4個元素的執行緒陣列(不包括處理快取的執行緒),來不停的取出訊息處理。
而Picssso是一個比較典型的執行緒池實現的生產者消費者模型,這裡就不做過多介紹了。
這兩個框架使用的資料結構都是PriorityBlockingQueue(優先順序阻塞佇列),目的是為了做排序,保證優先順序高的請求先被處理。
順便說一下Android的訊息處理機制其實也是一個生產者消費者模型。
一個小問題
這裡博主當時想到了一個小問題:那就是喚醒消費者的時候喚醒的順序是怎樣的?
這裡涉及到一個概念叫公平訪問佇列,所謂公平訪問佇列是指所有阻塞的生產者執行緒或者消費者執行緒,當佇列可用是,可以按照阻塞的先後順序訪問佇列,即先阻塞的生產者執行緒,可以先往佇列裡插入元素,先阻塞的消費者執行緒,可以先從佇列裡獲取元素。通常情況下為了保證公平性會降低吞吐量。
快取
Android快取分為記憶體快取和檔案快取(磁碟快取)。
一般網路框架是不需要處理記憶體快取的,但是圖片載入框架需要。在Android3.1以後,Android推出了LruCache這個記憶體快取類,LruCache中的物件是強引用的。Picasso的記憶體快取就是使用的LruCache實現的。對於磁碟快取,Google提供的一種解決方案是使用DiskLruCache(DiskLruCache並沒有整合到Android原始碼中,在Android Doc的例子中有講解)。Picasso的磁碟快取是基於okhttp的,使用了DiskLruCache。而Volley的磁碟快取是在DiskBasedCache中實現得,也是基於Lru演算法的。
至於其他快取演算法、快取命中率等等概念這裡我就不做過多介紹了。
非同步的處理
我們知道Android是單執行緒模型,我們應該避免在UI執行緒中進行耗時操作,網路請求算是一個比較典型的耗時操作,所以網路相關的框架中都會對非同步操作進行一些封裝。
其實這裡沒什麼複雜的地方,無非就是利用Handler進行執行緒間通訊,然後配合回撥機制,把結果返回到主執行緒裡。這裡可以參考我之前的文章《Android Handler 訊息機制(解惑篇)》和《當觀察者模式和回撥機制遇上Android原始碼》。
我們以Volley為例來簡單看一下,ExecutorDelivery類的職責是分發子執行緒產生的responses資料或者錯誤資訊。初始化是在RequestQueue類裡。
1 2 3 4 |
public RequestQueue(Cache cache, Network network, int threadPoolSize) { this(cache, network, threadPoolSize, new ExecutorDelivery(new Handler(Looper.getMainLooper()))); } |
這裡傳入的是主執行緒的Handler物件,而這個ExecutorDelivery物件會被傳入到NetworkDispatcher和CacheDispatcher中,這兩個類是繼承於Thread的,負責處理佇列中的請求。所以處理請求的操作是發生在子執行緒的。
然後我們看下ExecutorDelivery類的構造方法
1 2 3 4 5 6 7 8 9 |
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); } }; } |
這裡用Executor對Handler進行了一層包裝。Volley中的responses資料或者錯誤資訊都會通過Executor傳送出去,這樣訊息就到了主執行緒中。
Picasso比Volley要稍稍複雜了一點,由Picasso會對圖片進行變換等操作,屬於耗時操作,所以在Picasso中請求的分發和結果的處理會單獨放到一個執行緒中。這個執行緒是一個帶有訊息佇列的執行緒,用來執行迴圈性任務,即對獲取到的資料進行處理。當它對結果處理完成之後,才會通過主執行緒的Handler把結果傳送回主執行緒進行顯示等操作。
設計模式
優秀的框架會合理的利用設計模式,使程式碼易於擴充套件和後期的維護。這裡有一些出現頻率比較高的設計模式。
- 靜態工廠方法:由一個工廠物件決定建立出哪一種產品類的例項
- 單例模式:確保有且只有一個物件被建立
- 建造者模式:將一個複雜物件的構建與它的表示分離,使得同樣的構建過程可以建立不同的表示
- 外觀模式:簡化一群類的介面
- 命令模式:封裝請求成為物件
- 策略模式:封裝可以互選的行為,並使用委託來決定使用哪一個
框架入口
一般框架為了呼叫簡潔,並不會讓客戶端直接通過new例項化一個入口物件。這裡就需要用到建立型模式。
Volley的入口使用的是靜態工廠方法,與Android原始碼中Bitmap的例項化類似,具體可以參考《Android原始碼中的靜態工廠方法》
1 2 3 4 5 6 7 8 9 |
/** * 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. * @return A started {@link RequestQueue} instance. */ public static RequestQueue newRequestQueue(Context context) { return newRequestQueue(context, null); } |
Picasso的入口方法則用到了雙重鎖的單例模式
1 2 3 4 5 6 7 8 9 10 11 |
static volatile Picasso singleton = null; public static Picasso with(Context context) { if (singleton == null) { synchronized (Picasso.class) { if (singleton == null) { singleton = new Builder(context).build(); } } } return singleton; } |
同時由於可配置項太多,所以Picasso還使用了Builder模式。
同時一些框架為了給給客戶端提供一個簡潔的的API,會使用外觀模式定義一個高層介面,使得框架中的各個模組更加容易使用。外觀模式是一種結構型模式。
外觀模式可以參考《Android原始碼中的外觀模式》
命令模式
命令模式的定義是將一個請求封裝成一個物件,從而使你可用不同的請求對客戶進行引數化,對請求排隊或記錄請求日誌,以及支援可撤銷的操作。在網路請求框架中都會將請求做一個封裝成物件,方便傳遞和使用。比如Volley中的Request,Picasso中的Request和Action。
命令模式可以參考《Android原始碼中的命令模式》
策略模式
策略模式也是大部分框架都會用到的一個模式 ,作用是封裝可以互選的行為,並使用委託來決定使用哪一個。
Volley中就大量使用了面向介面程式設計的程式設計思想。這裡我們看下Volley的入口方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) { //~省略部分無關程式碼~ 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); //~省略部分無關程式碼~ } |
這裡會根據API版本選擇不同的Http客戶端,它們實現了一個共同的介面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
/** * An HTTP stack abstraction. */ public interface HttpStack { /** * Performs an HTTP request with the given parameters. * * <p>A GET request is sent if request.getPostBody() == null. A POST request is sent otherwise, * and the Content-Type header is set to request.getPostBodyContentType().</p> * * @param request the request to perform * @param additionalHeaders additional headers to be sent together with * {@link Request#getHeaders()} * @return the HTTP response */ public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders) throws IOException, AuthFailureError; } |
當然我們也可以自己實現這個介面,然後把Http客戶端換成okhttp。
後記
網路相關的框架套路基本上就這些了,具體細節大家可以去自己看下相關原始碼。如果有什麼不完善或者不對的地方也請大家多指教。
打賞支援我寫出更多好文章,謝謝!
打賞作者
打賞支援我寫出更多好文章,謝謝!
任選一種支付方式