網(wǎng)絡(luò)框架分析 – 全是套路
前言
這幾天抽時間啃完了Volley和Picasso的源碼,收獲頗多,所以在這里跟大家分享一下。
對于網(wǎng)絡(luò)請求框架或者圖片加載框架來說,我們的理想型大體應(yīng)該是這樣的:
- 簡單:框架的出現(xiàn)當(dāng)然是為了提升我們的開發(fā)效率,使我們的開發(fā)變得簡單,所以在保證質(zhì)量的情況下簡單是第一位的
- 可配置:天底下沒有完全相同的兩片樹葉,也沒有完全相同的兩個項(xiàng)目,所以某些差異應(yīng)該是可配置的,比如緩存位置、緩存大小、緩存策略等等
- 方便擴(kuò)展:框架在設(shè)計(jì)的時候就要考慮到變化,并且封裝起來。舉個例子,比如有了更好的Http客戶端,我們應(yīng)該能很方便的修改并且不能對我們之前的代碼產(chǎn)生太大影響
但萬變不離其宗,這些框架的骨架其實(shí)基本上都是一樣的,今天我們就來討論下這些框架中的套路。
基本模塊
既然我們說這些框架的結(jié)構(gòu)其實(shí)基本上都是一樣的,那么我們就先來看看它們之間類似的模塊結(jié)構(gòu)。
整體流程大概是這樣的:
客戶端請求->生成框架封裝的請求類型->調(diào)度器開始處理任務(wù)->調(diào)用數(shù)據(jù)獲取模塊->對獲取的數(shù)據(jù)進(jìn)行處理->回調(diào)給客戶端
生產(chǎn)者消費(fèi)者模型
框架中請求管理和任務(wù)調(diào)度模塊一般會用到生產(chǎn)者消費(fèi)者模型。
為什么會有生產(chǎn)者消費(fèi)者模型
在線程世界里,生產(chǎn)者就是生產(chǎn)數(shù)據(jù)的線程,消費(fèi)者就是消費(fèi)數(shù)據(jù)的線程。在多線程開發(fā)當(dāng)中,如果生產(chǎn)者處理速度很快,而消費(fèi)者處理速度很慢,那么生產(chǎn)者就必須等待消費(fèi)者處理完,才能繼續(xù)生產(chǎn)數(shù)據(jù)。同樣的道理,如果消費(fèi)者的處理能力大于生產(chǎn)者,那么消費(fèi)者就必須等待生產(chǎn)者。為了解決這個問題于是引入了生產(chǎn)者和消費(fèi)者模型。
什么是生產(chǎn)者消費(fèi)者模型
生產(chǎn)者消費(fèi)者模式是通過一個容器來解決生產(chǎn)者和消費(fèi)者的強(qiáng)耦合問題。生產(chǎn)者和消費(fèi)者彼此之間不直接通訊,而通過阻塞隊(duì)列來進(jìn)行通訊,所以生產(chǎn)者生產(chǎn)完數(shù)據(jù)之后不用等待消費(fèi)者處理,直接扔給阻塞隊(duì)列,消費(fèi)者不找生產(chǎn)者要數(shù)據(jù),而是直接從阻塞隊(duì)列里取,阻塞隊(duì)列就相當(dāng)于一個緩沖區(qū),平衡了生產(chǎn)者和消費(fèi)者的處理能力。
生產(chǎn)者消費(fèi)者模型的使用場景
Java中的線程池類其實(shí)就是一種生產(chǎn)者和消費(fèi)者模式的實(shí)現(xiàn)方式,但是實(shí)現(xiàn)方法更高明。生產(chǎn)者把任務(wù)丟給線程池,線程池創(chuàng)建線程并處理任務(wù),如果將要運(yùn)行的任務(wù)數(shù)大于線程池的基本線程數(shù)就把任務(wù)扔到阻塞隊(duì)列里,這種做法比只使用一個阻塞隊(duì)列來實(shí)現(xiàn)生產(chǎn)者和消費(fèi)者模型顯然要高明很多,因?yàn)橄M(fèi)者能夠處理直接就處理掉了,這樣速度更快,而生產(chǎn)者先存,消費(fèi)者再取這種方式顯然慢一些。
框架中的應(yīng)用
對于上述的使用場景我們分別可以在框架中找到實(shí)現(xiàn)。
Volley源碼中實(shí)現(xiàn)方式是用一個優(yōu)先級阻塞隊(duì)列來實(shí)現(xiàn)生產(chǎn)者消費(fèi)者模型。生產(chǎn)者是往隊(duì)列里添加數(shù)據(jù)的線程,消費(fèi)者是一個默認(rèn)4個元素的線程數(shù)組(不包括處理緩存的線程),來不停的取出消息處理。
而Picssso是一個比較典型的線程池實(shí)現(xiàn)的生產(chǎn)者消費(fèi)者模型,這里就不做過多介紹了。
這兩個框架使用的數(shù)據(jù)結(jié)構(gòu)都是PriorityBlockingQueue(優(yōu)先級阻塞隊(duì)列),目的是為了做排序,保證優(yōu)先級高的請求先被處理。
順便說一下Android的消息處理機(jī)制其實(shí)也是一個生產(chǎn)者消費(fèi)者模型。
一個小問題
這里博主當(dāng)時想到了一個小問題:那就是喚醒消費(fèi)者的時候喚醒的順序是怎樣的?
這里涉及到一個概念叫公平訪問隊(duì)列,所謂公平訪問隊(duì)列是指所有阻塞的生產(chǎn)者線程或者消費(fèi)者線程,當(dāng)隊(duì)列可用是,可以按照阻塞的先后順序訪問隊(duì)列,即先阻塞的生產(chǎn)者線程,可以先往隊(duì)列里插入元素,先阻塞的消費(fèi)者線程,可以先從隊(duì)列里獲取元素。通常情況下為了保證公平性會降低吞吐量。
緩存
Android緩存分為內(nèi)存緩存和文件緩存(磁盤緩存)。
一般網(wǎng)絡(luò)框架是不需要處理內(nèi)存緩存的,但是圖片加載框架需要。在Android3.1以后,Android推出了LruCache這個內(nèi)存緩存類,LruCache中的對象是強(qiáng)引用的。Picasso的內(nèi)存緩存就是使用的LruCache實(shí)現(xiàn)的。對于磁盤緩存,Google提供的一種解決方案是使用DiskLruCache(DiskLruCache并沒有集成到Android源碼中,在Android Doc的例子中有講解)。Picasso的磁盤緩存是基于okhttp的,使用了DiskLruCache。而Volley的磁盤緩存是在DiskBasedCache中實(shí)現(xiàn)得,也是基于Lru算法的。
至于其他緩存算法、緩存命中率等等概念這里我就不做過多介紹了。
異步的處理
我們知道Android是單線程模型,我們應(yīng)該避免在UI線程中進(jìn)行耗時操作,網(wǎng)絡(luò)請求算是一個比較典型的耗時操作,所以網(wǎng)絡(luò)相關(guān)的框架中都會對異步操作進(jìn)行一些封裝。
其實(shí)這里沒什么復(fù)雜的地方,無非就是利用Handler進(jìn)行線程間通信,然后配合回調(diào)機(jī)制,把結(jié)果返回到主線程里。這里可以參考我之前的文章《Android Handler 消息機(jī)制(解惑篇)》和《當(dāng)觀察者模式和回調(diào)機(jī)制遇上Android源碼》。
我們以Volley為例來簡單看一下,ExecutorDelivery類的職責(zé)是分發(fā)子線程產(chǎn)生的responses數(shù)據(jù)或者錯誤信息。初始化是在RequestQueue類里。
- public RequestQueue(Cache cache, Network network, int threadPoolSize) {
- this(cache, network, threadPoolSize,
- new ExecutorDelivery(new Handler(Looper.getMainLooper())));
- }
這里傳入的是主線程的Handler對象,而這個ExecutorDelivery對象會被傳入到NetworkDispatcher和CacheDispatcher中,這兩個類是繼承于Thread的,負(fù)責(zé)處理隊(duì)列中的請求。所以處理請求的操作是發(fā)生在子線程的。
然后我們看下ExecutorDelivery類的構(gòu)造方法
- 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進(jìn)行了一層包裝。Volley中的responses數(shù)據(jù)或者錯誤信息都會通過Executor發(fā)送出去,這樣消息就到了主線程中。
Picasso比Volley要稍稍復(fù)雜了一點(diǎn),由Picasso會對圖片進(jìn)行變換等操作,屬于耗時操作,所以在Picasso中請求的分發(fā)和結(jié)果的處理會單獨(dú)放到一個線程中。這個線程是一個帶有消息隊(duì)列的線程,用來執(zhí)行循環(huán)性任務(wù),即對獲取到的數(shù)據(jù)進(jìn)行處理。當(dāng)它對結(jié)果處理完成之后,才會通過主線程的Handler把結(jié)果發(fā)送回主線程進(jìn)行顯示等操作。
設(shè)計(jì)模式
優(yōu)秀的框架會合理的利用設(shè)計(jì)模式,使代碼易于擴(kuò)展和后期的維護(hù)。這里有一些出現(xiàn)頻率比較高的設(shè)計(jì)模式。
- 靜態(tài)工廠方法:由一個工廠對象決定創(chuàng)建出哪一種產(chǎn)品類的實(shí)例
- 單例模式:確保有且只有一個對象被創(chuàng)建
- 建造者模式:將一個復(fù)雜對象的構(gòu)建與它的表示分離,使得同樣的構(gòu)建過程可以創(chuàng)建不同的表示
- 外觀模式:簡化一群類的接口
- 命令模式:封裝請求成為對象
- 策略模式:封裝可以互選的行為,并使用委托來決定使用哪一個
框架入口
一般框架為了調(diào)用簡潔,并不會讓客戶端直接通過new實(shí)例化一個入口對象。這里就需要用到創(chuàng)建型模式。
Volley的入口使用的是靜態(tài)工廠方法,與Android源碼中Bitmap的實(shí)例化類似,具體可以參考《Android源碼中的靜態(tài)工廠方法》
- /**
- * 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;
- }
同時由于可配置項(xiàng)太多,所以Picasso還使用了Builder模式。
同時一些框架為了給給客戶端提供一個簡潔的的API,會使用外觀模式定義一個高層接口,使得框架中的各個模塊更加容易使用。外觀模式是一種結(jié)構(gòu)型模式。
外觀模式可以參考《Android源碼中的外觀模式》
命令模式
命令模式的定義是將一個請求封裝成一個對象,從而使你可用不同的請求對客戶進(jìn)行參數(shù)化,對請求排隊(duì)或記錄請求日志,以及支持可撤銷的操作。在網(wǎng)絡(luò)請求框架中都會將請求做一個封裝成對象,方便傳遞和使用。比如Volley中的Request,Picasso中的Request和Action。
命令模式可以參考《Android源碼中的命令模式》
策略模式
策略模式也是大部分框架都會用到的一個模式 ,作用是封裝可以互選的行為,并使用委托來決定使用哪一個。
Volley中就大量使用了面向接口編程的編程思想。這里我們看下Volley的入口方法
- public static RequestQueue newRequestQueue(Context context, HttpStack stack, int maxDiskCacheBytes) {
- //~省略部分無關(guān)代碼~
- 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);
- //~省略部分無關(guān)代碼~
- }
這里會根據(jù)API版本選擇不同的Http客戶端,它們實(shí)現(xiàn)了一個共同的接口
- /**
- * 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;
- }
當(dāng)然我們也可以自己實(shí)現(xiàn)這個接口,然后把Http客戶端換成okhttp。
后記
網(wǎng)絡(luò)相關(guān)的框架套路基本上就這些了,具體細(xì)節(jié)大家可以去自己看下相關(guān)源碼。如果有什么不完善或者不對的地方也請大家多指教。