網絡框架分析 – 全是套路
前言
這幾天抽時間啃完了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類里。
- public RequestQueue(Cache cache, Network network, int threadPoolSize) {
- this(cache, network, threadPoolSize,
- new ExecutorDelivery(new Handler(Looper.getMainLooper())));
- }
這里傳入的是主線程的Handler對象,而這個ExecutorDelivery對象會被傳入到NetworkDispatcher和CacheDispatcher中,這兩個類是繼承于Thread的,負責處理隊列中的請求。所以處理請求的操作是發生在子線程的。
然后我們看下ExecutorDelivery類的構造方法
- 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源碼中的靜態工廠方法》
- /**
- * 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的入口方法則用到了雙重鎖的單例模式
- 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的入口方法
- 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客戶端,它們實現了一個共同的接口
- /**
- * 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。
后記
網絡相關的框架套路基本上就這些了,具體細節大家可以去自己看下相關源碼。如果有什么不完善或者不對的地方也請大家多指教。