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