Okhttp使用指南與源碼分析
標簽(空格分隔): Android
使用指南篇#
為什么使用okhttp###
Android為我們提供了兩種HTTP交互的方式:HttpURLConnection 和 Apache HTTP Client十嘿,雖然兩者都支持HTTPS,流的上傳和下載虽惭,配置超時妄迁,IPv6和連接池旅敷,已足夠滿足我們各種HTTP請求的需求。但更高效的使用HTTP可以讓您的應(yīng)用運行更快、更節(jié)省流量迎卤。而OkHttp庫就是為此而生厢拭。
OkHttp是一個高效的HTTP庫:
- 支持 SPDY 兰英,共享同一個Socket來處理同一個服務(wù)器的所有請求,socket自動選擇最好路線,并支持自動重連,擁有自動維護的socket連接池供鸠,減少握手次數(shù)
- 擁有隊列線程池畦贸,輕松寫并發(fā)
- 如果SPDY不可用,則通過連接池來減少請求延時
- 擁有Interceptors輕松處理請求與響應(yīng)(比如透明GZIP壓縮,LOGGING),無縫的支持GZIP來減少數(shù)據(jù)流量
- 基于Headers的緩存策略,緩存響應(yīng)數(shù)據(jù)來減少重復的網(wǎng)絡(luò)請求
-會從很多常用的連接問題中自動恢復楞捂。如果您的服務(wù)器配置了多個IP地址薄坏,當?shù)谝粋€IP連接失敗的時候,OkHttp會自動嘗試下一個IP寨闹。OkHttp還處理了代理服務(wù)器問題和SSL握手失敗問題胶坠。
-使用 OkHttp 無需重寫您程序中的網(wǎng)絡(luò)代碼。OkHttp實現(xiàn)了幾乎和java.net.HttpURLConnection一樣的API繁堡。如果您用了 Apache HttpClient沈善,則OkHttp也提供了一個對應(yīng)的okhttp-apache 模塊。
Okio庫###
Okio庫是一個由square公司開發(fā)的椭蹄,它補充了Java.io和java.nio的不足闻牡,以便能夠更加方便,快速的訪問绳矩、存儲和處理你的數(shù)據(jù)罩润。而OkHttp的底層也使用該庫作為支持。而在開發(fā)中翼馆,使用該庫可以大大給你帶來方便割以。
因為okhttp用到了Okio庫金度,所以在Gradle的配置也要引入Okio
compile 'com.squareup.okio:okio:1.6.0' //具體版本以最新的為準
用法細節(jié)###
同步就用execute,用這個實現(xiàn)異步拳球,要自己寫線程所以還不如用下面的enqueue實現(xiàn)異步
異步就用enqueue审姓,okHttp內(nèi)部自動實現(xiàn)了線程,自帶工作線程池
取消操作
網(wǎng)絡(luò)操作中祝峻,經(jīng)常會使用到對請求的cancel操作魔吐,okhttp的也提供了這方面的接口,當call沒有必要的時候莱找,使用這個api可以節(jié)約網(wǎng)絡(luò)資源酬姆。例如當用戶離開一個應(yīng)用時。不管同步還是異步的call都可以取消奥溺。
call的cancel操作辞色。使用Call.cancel()可以立即停止掉一個正在執(zhí)行的call。如果一個線程正在寫請求或者讀響應(yīng)浮定,將會引發(fā)IOException相满,
同時可以通過Request.Builder.tag(Objec tag)給請求設(shè)置一個標簽,并使用OkHttpClient.cancel(Object tag)來取消所有帶有這個tag的call桦卒。但如果該請求已經(jīng)在做讀寫操作的時候立美,cancel是無法成功的,會拋出IOException異常方灾。
具體用法請見以下的博客::####
[博客一][1]
[博客二][2]
[也可以看一下鴻洋的封裝自己的Okhttp庫的文章建蹄,在前面的部分也提及到一些基礎(chǔ)用法][3]
[稀土掘金的翻譯文章][4]
注意上面的幾篇博客對應(yīng)的OkHttp版本已經(jīng)過時了,例如
private void postRequest() {
final OkHttpClient client = new OkHttpClient();
// RequestBody formBody = new FormEncodingBuilder()//在3.0版本FormEncodingBuilder已經(jīng)被FormBody代替
RequestBody formBody = new FormBody.Builder()
.add(Constant.PUSHID,pushID)
.build();
final Request request = new Request.Builder()
.url(Constant.TUTOR_LOOKQUESKTION)
.post(formBody)
.build();
new Thread(new Runnable() {
@Override
public void run() {
Response response = null;
try {
response = client.newCall(request).execute();
if (response.isSuccessful()) {
String result=response.body().string();
//String result2=response.body().string();//每個 body 只能被消費一次裕偿,多次消費會拋出異常洞慎;例如這里的result2會報錯,因為是第二次獲取body了
response.body().close();//body 必須被關(guān)閉嘿棘,否則會發(fā)生資源泄漏劲腿;
Log.i("WY","打印POST響應(yīng)的數(shù)據(jù):" +result );
try {
getQuestionData(result,1);
}
catch (Exception e){
e.printStackTrace();
}
} else {
throw new IOException("Unexpected code " + response);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
緩存
Okhttp已經(jīng)內(nèi)置了緩存,使用DiskLruCache鸟妙;默認是不使用的焦人,如果想使用緩存我們需要手動設(shè)置。要建立緩存圆仔,要有以下條件:
- 可以讀寫的緩存目錄
- 緩存大小的限制
- 緩存目錄應(yīng)該是私有的垃瞧,不信任的程序不能讀取緩存內(nèi)容
- 全局用戶唯一的緩存訪問實例蔫劣。okhttp框架全局必須只有一個OkHttpClient實例(new OkHttpClient())坪郭,并在第一次創(chuàng)建實例的時候,配置好緩存脉幢。
OkHttp內(nèi)部維護著清理線程池歪沃,實現(xiàn)對緩存文件的自動清理,而且內(nèi)部的DiskLrucache結(jié)合了LinkedHashMap使用LRU算法嗦锐,從而實現(xiàn)對緩存文件的有效管理
如果服務(wù)器支持緩存,請求返回的Response會帶有這樣的Header:Cache-Control, max-age=xxx,這種情況下我們只需要手動給okhttp設(shè)置緩存就可以讓okhttp自動幫你緩存了沪曙。這里的max-age的值代表了緩存在你本地存放的時間奕污,可以根據(jù)實際需要來設(shè)置其大小。
例如可以在reponse的header設(shè)置max-age的值,液走,一般用在服務(wù)器不支持緩存碳默,然后需要使用Interceptor來重寫Respose的頭部信息 ,見下文
Request request = chain.request();
Response response = chain.proceed(request);
Response response1 = response.newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
//cache for 30 days
.header("Cache-Control", "max-age=" + 3600 * 24 * 30)
//設(shè)置max-age的值
.build();
也可以在request的header設(shè)置max-age的值,一般用在服務(wù)器支持緩存
int maxStale = 60 * 60 * 24 * 28; //4周
Request request = new Request.Builder()
.header("Cache-Control", "max-stale=" + maxStale)
.url("http://publicobject.com/helloworld.txt")
.build();
注:HTTP header中的max-age 和max-stale區(qū)別:
max-age 指示客戶機可以接收生存期不大于指定時間(以秒為單位)的響應(yīng)缘眶。
max-stale 指示客戶機可以接收超出超時期間的響應(yīng)消息嘱根。如果指定max-stale消息的值,那么客戶機可以接收超出超時期指定值之內(nèi)的響應(yīng)消息巷懈。
如果服務(wù)器支持緩存:###
開啟緩存该抒,并且設(shè)置緩存目錄
int cacheSize = 10 * 1024 * 1024; // 10 MiB
File cacheDirectory = new File("cache");
//出于安全性的考慮,在Android中我們推薦使用Context.getCacheDir()來作為緩存的存放路徑
if (!cacheDirectory.exists()) {
cacheDirectory.mkdirs();
}
Cache cache = new Cache(cacheDirectory, cacheSize);
OkHttpClient newClient = okHttpClient.newBuilder()
.Cache(cache)
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.build();
如果服務(wù)器不支持緩存:###
如果服務(wù)器不支持緩存就可能沒有指定這個頭部顶燕,或者指定的值是如no-store等凑保,但是我們還想在本地使用緩存的話要怎么辦呢?這種情況下我們就需要使用Interceptor來重寫Respose的頭部信息涌攻,從而讓okhttp支持緩存欧引。
如下所示,我們重寫的Response的Cache-Control字段
public class CacheInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
Response response1 = response.newBuilder()
.removeHeader("Pragma")
.removeHeader("Cache-Control")
//cache for 30 days
.header("Cache-Control", "max-age=" + 3600 * 24 * 30)
.build();
return response1;
}
}
然后將該Intercepter作為一個NetworkInterceptor加入到okhttpClient中:
OkHttpClient okHttpClient = new OkHttpClient();
OkHttpClient newClient = okHttpClient.newBuilder()
.addNetworkInterceptor(new CacheInterceptor())
.cache(cache)
.connectTimeout(20, TimeUnit.SECONDS)
.readTimeout(20, TimeUnit.SECONDS)
.build();
這樣我們就可以在服務(wù)器不支持緩存的情況下使用緩存了癣漆。
[參考文章][5]
- 強制使用網(wǎng)絡(luò)響應(yīng)
Request request = new Request.Builder()
.header("Cache-Control", "no-cache") // 刷新數(shù)據(jù)
.url("http://publicobject.com/helloworld.txt")
.build();
- 通過服務(wù)器驗證緩存數(shù)據(jù)是否有效
Request request = new Request.Builder()
.header("Cache-Control", "max-age=0")
.url("http://publicobject.com/helloworld.txt")
.build();
- 強制使用緩存響應(yīng)
Request request = new Request.Builder()
.header("Cache-Control", "only-if-cached")
.url("http://publicobject.com/helloworld.txt")
.build();
- 指定緩存數(shù)據(jù)過時的時間
int maxStale = 60 * 60 * 24 * 28; //4周
Request request = new Request.Builder()
.header("Cache-Control", "max-stale=" + maxStale)
.url("http://publicobject.com/helloworld.txt")
.build();
[參考文章][6]
okhttp框架獲取響應(yīng)數(shù)據(jù)有三種方法:
- 返回網(wǎng)絡(luò)上的數(shù)據(jù)维咸。如果沒有使用網(wǎng)絡(luò),則返回null惠爽。
public Response networkResponse()癌蓖;//從網(wǎng)絡(luò)返回的response取數(shù)據(jù)
- 返回緩存中的數(shù)據(jù)。如果不使用緩存婚肆,則返回null租副。對應(yīng)發(fā)送的GET請求,緩存響應(yīng)和網(wǎng)絡(luò)響應(yīng) 有可都非空较性。
public Response priorResponse()```
networkResponse()與cacheResponse()是互斥的用僧,要是networkResponse()返回的不為空null,那么cacheResponse()就會返回為nul赞咙;反之亦然责循。具體為什么我還沒搞清楚?攀操?院仿???歹垫?
所以在我們用request去請求網(wǎng)絡(luò)數(shù)據(jù)的步驟剥汤,可以這樣寫:
if (null != response.cacheResponse()) {
String str = response.cacheResponse().toString();
//這里有一個問題就是為什么要用一個reponse對象去拿緩存呢?不是應(yīng)該要用request排惨,像volley一樣嗎吭敢??暮芭?鹿驼??辕宏?蠢沿?
Log.i("wangshu", "cache---" + str);
} else {
response.body().string();
String str=response.networkResponse().toString();
Log.i("wangshu", "network---" + str);
}
---
下面是強制情況:
但有時候即使在有緩存的情況下我們依然需要去后臺請求最新的資源(比如資源更新了)這個時候可以使用強制走網(wǎng)絡(luò)來要求必須請求網(wǎng)絡(luò)數(shù)據(jù)
request = request.newBuilder().cacheControl(CacheControl.FORCE_NETWORK).build();
同樣的我們可以使用 FORCE_CACHE 強制只要使用緩存的數(shù)據(jù)
request = request.newBuilder().cacheControl(CacheControl.FORCE_CACHE).build();
注意:
如果開啟了緩存。但是要強制去網(wǎng)絡(luò)更新即配置了CacheControl.FORCE_NETWORK的話匾效,程序不會報錯舷蟀。這時對應(yīng)的緩存會清掉(注意不是所有的緩存喔!)即cacheResponse()返回為空面哼,等同于不使用緩存野宜;
但如果請求必須從網(wǎng)絡(luò)獲取才有數(shù)據(jù),卻配置了CacheControl.FORCE_CACHE的話就會返回504錯誤魔策,
綜上所述匈子,可以這樣理解:FORCE_CACHE是第一道門,而FORCE_NETWORK是第二道門闯袒。如果使用了第一道門虎敦,即配置了CacheControl.FORCE_CACHE,所有的請求都會從cache緩存里面取政敢,這時要請求網(wǎng)絡(luò)的話是過不了第一道門的其徙;但是如果使用的是第二道門的話,這時是可以過第一道門即繞過緩存喷户,而且會把對應(yīng)的緩存清掉唾那,然后從網(wǎng)絡(luò)取數(shù)據(jù)
關(guān)于OkHttp的封裝請看鴻洋的博客
[ Android OkHttp完全解析 是時候來了解OkHttp了][7] //這篇文章我還分析完》》》未完待續(xù)
---
[參考博客][8]
---
#源碼分析篇#
##更新補充##
推薦在分析之前先看看這篇文章
一片分析OkHttp不錯的文章:[拆輪子系列:拆 OkHttp][18]
##博客一##
[參考博客][9]
![總體設(shè)計圖][10]
上面是OKHttp總體設(shè)計圖,主要是通過Diapatcher不斷從RequestQueue中取出請求(Call)褪尝,根據(jù)是否已緩存調(diào)用Cache或Network這兩類數(shù)據(jù)獲取接口之一闹获,從內(nèi)存緩存或是服務(wù)器取得請求的數(shù)據(jù)。該引擎有同步和異步請求河哑,一個是普通的同步單線程避诽;另一種是使用了隊列進行并發(fā)任務(wù)的分發(fā)(Dispatch)與回調(diào)。同步請求通過Call.execute()直接返回當前的Response璃谨,而異步請求會把當前的請求Call.enqueue添加(AsyncCall)到請求隊列中沙庐,并通過回調(diào)(Callback)的方式來獲取最后結(jié)果。
注意:雖然在使用的沒有那么明顯,但是okhttp也有一個RequestQueue
![請求流程圖][11]
![類設(shè)計圖][12]
從OkHttpClient類的整體設(shè)計來看轨功,它采用```***門面模式***```來。client知曉子模塊的所有配置以及提供需要的參數(shù)容达。client會將所有從客戶端發(fā)來的請求委派到相應(yīng)的子系統(tǒng)去古涧。
在該系統(tǒng)中,有多個子系統(tǒng)花盐、類或者類的集合羡滑。例如上面的cache、連接以及連接池相關(guān)類的集合算芯、網(wǎng)絡(luò)配置相關(guān)類集合等等柒昏。每個子系統(tǒng)都可以被客戶端直接調(diào)用,或者被門面角色調(diào)用熙揍。子系統(tǒng)并不知道門面的存在职祷,對于子系統(tǒng)而言,門面僅僅是另外一個客戶端而已届囚。同時有梆,OkHttpClient可以看作是整個框架的上下文。
通過類圖意系,其實很明顯反應(yīng)了該框架的幾大核心子系統(tǒng)泥耀;路由、連接協(xié)議蛔添、攔截器痰催、代理、安全性認證迎瞧、連接池以及網(wǎng)絡(luò)適配夸溶。從client大大降低了開發(fā)者使用難度。同時非常明了的展示了該框架在所有需要的配置以及獲取結(jié)果的方式凶硅。
##同步與異步的實現(xiàn)##
在發(fā)起請求時蜘醋,整個框架主要通過Call來封裝每一次的請求。同時Call持有OkHttpClient和一份HttpEngine咏尝。而每一次的同步或者異步請求都會有Dispatcher的參與压语,不同的是:
同步
Dispatcher會在同步執(zhí)行任務(wù)隊列中記錄當前被執(zhí)行過得任務(wù)Call,同時在當前線程中去執(zhí)行Call的getResponseWithInterceptorChain()方法编检,直接獲取當前的返回數(shù)據(jù)Response胎食;
異步
首先來說一下Dispatcher,Dispatcher內(nèi)部實現(xiàn)了懶加載無邊界限制的線程池方式,同時該線程池采用了SynchronousQueue這種阻塞隊列朝墩。SynchronousQueue每個插入操作必須等待另一個線程的移除操作,同樣任何一個移除操作都等待另一個線程的插入操作戴尸。因此此隊列內(nèi)部其 實沒有任何一個元素粥航,或者說容量是0琅捏,嚴格說并不是一種容器。由于隊列沒有容量递雀,因此不能調(diào)用peek操作柄延,因為只有移除元素時才有元素。顯然這是一種快速傳遞元素的方式缀程,也就是說在這種情況下元素總是以最快的方式從插入者(生產(chǎn)者)傳遞給移除者(消費者)搜吧,這在多任務(wù)隊列中是最快處理任務(wù)的方式。對于高頻繁請求的場景杨凑,無疑是最適合的滤奈。
異步執(zhí)行是通過Call.enqueue(Callback responseCallback)來執(zhí)行,在Dispatcher中添加一個封裝了Callback的Call的匿名內(nèi)部類Runnable來執(zhí)行當前的Call撩满。這里一定要注意的地方這個AsyncCall是Call的匿名內(nèi)部類蜒程。AsyncCall的execute方法仍然會回調(diào)到Call的getResponseWithInterceptorChain方法來完成請求,同時將返回數(shù)據(jù)或者狀態(tài)通過Callback來完成伺帘。
##攔截器有什么作用##
先來看看Interceptor本身的文檔解釋:觀察搞糕,修改以及可能短路的請求輸出和響應(yīng)請求的回來。通常情況下攔截器用來添加曼追,移除或者轉(zhuǎn)換請求或者回應(yīng)的頭部信息窍仰。
從這里的執(zhí)行來看,攔截器主要是針對Request和Response的切面處理礼殊。
在這里再多說一下關(guān)于Call這個類的作用驹吮,在Call中持有一個HttpEngine。每一個不同的Call都有自己獨立的HttpEngine晶伦。在HttpEngine中主要是各種鏈路和地址的選擇碟狞,還有一個Transport比較重要
##緩存策略##
在OkHttpClient內(nèi)部暴露了有Cache和InternalCache。而InternalCache不應(yīng)該手動去創(chuàng)建婚陪,所以作為開發(fā)使用者來說族沃,一般用法
public final class CacheResponse {
private static final Logger logger = Logger.getLogger(LoggingInterceptors.class.getName());
private final OkHttpClient client;
public CacheResponse(File cacheDirectory) throws Exception {
logger.info(String.format("Cache file path %s",cacheDirectory.getAbsoluteFile()));
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(cacheDirectory, cacheSize);
client = new OkHttpClient();
client.setCache(cache);
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
Response response1 = client.newCall(request).execute();
if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);
String response1Body = response1.body().string();
System.out.println("Response 1 response: " + response1);
System.out.println("Response 1 cache response: " + response1.cacheResponse());
System.out.println("Response 1 network response: " + response1.networkResponse());
Response response2 = client.newCall(request).execute();
if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);
String response2Body = response2.body().string();
System.out.println("Response 2 response: " + response2);
System.out.println("Response 2 cache response: " + response2.cacheResponse());
System.out.println("Response 2 network response: " + response2.networkResponse());
System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
}
public static void main(String... args) throws Exception {
new CacheResponse(new File("CacheResponse.tmp")).run();
}
}
第一次是來至網(wǎng)絡(luò)數(shù)據(jù),第二次來至緩存
從Call.getResponse(Request request, boolean forWebSocket)執(zhí)行Engine.sendRequest()和Engine.readResponse()來詳細說明一下泌参。
具體的sendRequest()和readResponse()請看博客
sendRequest()
此方法是對可能的Response資源進行一個預判脆淹,如果需要就會開啟一個socket來獲取資源。如果請求存在那么就會為當前request添加請求頭部并且準備開始寫入request body沽一。
readResponse()
此方法發(fā)起刷新請求頭部和請求體盖溺,解析HTTP回應(yīng)頭部,并且如果HTTP回應(yīng)體存在的話就開始讀取當前回應(yīng)頭铣缠。在這里有發(fā)起返回存入緩存系統(tǒng)烘嘱,也有返回和緩存系統(tǒng)進行一個對比的過程昆禽。
##HTTP連接的實現(xiàn)方式(說說連接池)##
外部網(wǎng)絡(luò)請求的入口都是通過Transport接口來完成。該類采用了```橋接模式```將HttpEngine和HttpConnection來連接起來蝇庭。因為HttpEngine只是一個邏輯處理器醉鳖,同時它也充當了請求配置的提供引擎,而HttpConnection是對底層處理Connection的封裝哮内。
HttpConnection(一個用于發(fā)送HTTP/1.1信息的socket連接)這里盗棵。主要有如下的生命周期:
1、發(fā)送請求頭牍蜂;
2、打開一個sink(io中有固定長度的或者塊結(jié)構(gòu)chunked方式的)去寫入請求body泰涂;
3鲫竞、寫入并且關(guān)閉sink;
4逼蒙、讀取Response頭部从绘;
5、打開一個source(對應(yīng)到第2步的sink方式)去讀取Response的body是牢;
6僵井、讀取并關(guān)閉source;
![連接執(zhí)行時序圖][13]
1驳棱、連接池是暴露在client下的批什,它貫穿了Transport、HttpEngine社搅、Connection驻债、HttpConnection和SpdyConnection;在這里目前默認討論HttpConnection形葬;
2合呐、ConnectionPool有兩個構(gòu)建參數(shù)是maxIdleConnections(最大空閑連接數(shù))和keepAliveDurationNs(存活時間),另外連接池默認的線程池采用了Single的模式(源碼解釋是:一個用于清理過期的多個連接的后臺線程,最多一個單線程去運行每一個連接池)笙以;
3淌实、發(fā)起請求是在Connection.connect()這里,實際執(zhí)行是在HttpConnection.flush()這里進行一個刷入猖腕。這里重點應(yīng)該關(guān)注一下sink和source拆祈,他們創(chuàng)建的默認方式都是依托于同一個socket:
this.source = Okio.buffer(Okio.source(socket));
this.sink = Okio.buffer(Okio.sink(socket));
如果再進一步看一下io的源碼就能看到:
Source source = source((InputStream)socket.getInputStream(), (Timeout)timeout);
Sink sink = sink((OutputStream)socket.getOutputStream(), (Timeout)timeout);
source負責讀取,sink負責寫入
---
##簡單總結(jié)##
1倘感、從整體結(jié)構(gòu)和類內(nèi)部域中都可以看到OkHttpClient缘屹,有點類似與安卓的ApplicationContext∠莱穑看起來更像一個單例的類轻姿,這樣使用好處是統(tǒng)一犁珠。但是如果你不是高手,建議別這么用互亮,原因很簡單:邏輯牽連太深犁享,如果出現(xiàn)問題要去追蹤你會有深深地罪惡感的;
##自己理解##
1.首先Okhttp有自己的線程池豹休,所以可以有效管理線程炊昆,OkHttpClient自帶并發(fā)光環(huán),雖然Volley相比Okhttp是高級的封裝庫威根,但是Volley沒有線程池凤巨,而且工作線程是自己維護的,那么就有可能存在線程由于異常退出之后洛搀,沒有下一個工作線程補充的風險(線程池可以彌補這個缺陷)敢茁,相對底層的Okhttp卻沒有這個風險。
---
##博客二##
[參考博客][14]
###***因為Okhttp不同于Volley這樣的高層庫留美,它是類似于UrlHttpConnection的底層庫彰檬,所以要涉及到一些數(shù)據(jù)傳輸?shù)闹R,類似socket***###
##第一部分:請求的分發(fā)和任務(wù)隊列##
##主要對象##
Connections: 對JDK中的socket進行了封裝谎砾,用來控制socket連接
Streams: 維護HTTP的流逢倍,用來對Requset/Response進行IO操作
Calls: HTTP請求任務(wù)封裝
StreamAllocation: 用來控制Connections/Streams的資源分配與釋放
##請求的分發(fā)##
當我們用OkHttpClient.newCall(request)進行execute/enenqueue時,實際是將請求Call放到了Dispatcher中景图,okhttp使用Dispatcher進行線程分發(fā)较雕,它有兩種方法,一個是普通的同步單線程挚币;另一種是使用了隊列進行并發(fā)任務(wù)的分發(fā)(Dispatch)與回調(diào)郎笆,我們下面主要分析第二種,也就是隊列這種情況忘晤,這也是okhttp能夠競爭過其它庫的核心功能之一
##Dispatcher的結(jié)構(gòu):##
maxRequests = 64: 最大并發(fā)請求數(shù)為64
maxRequestsPerHost = 5: 每個主機最大請求數(shù)為5
Dispatcher: 分發(fā)者宛蚓,也就是生產(chǎn)者(默認在主線程)
AsyncCall: 隊列中需要處理的Runnable(包裝了異步回調(diào)接口)
ExecutorService:消費者池(也就是線程池)
Deque<readyAsyncCalls>:緩存(用數(shù)組實現(xiàn),可自動擴容设塔,無大小限制)
Deque<runningAsyncCalls>:正在運行的任務(wù)凄吏,僅僅是用來引用正在運行的任務(wù)以判斷并發(fā)量,注意它并不是消費者緩存
根據(jù)生產(chǎn)者消費者模型的模型理論闰蛔,當入隊(enqueue)請求時痕钢,如果滿足(runningRequests<64 && runningRequestsPerHost<5),那么就直接把AsyncCall直接加到runningCalls的隊列中序六,并在線程池中執(zhí)行任连。如果消費者緩存滿了,就放入readyAsyncCalls進行緩存等待例诀。
當任務(wù)執(zhí)行完成后,調(diào)用finished的promoteCalls()函數(shù)随抠,手動移動緩存區(qū)(這里是主動清理的裁着,而不會發(fā)生死鎖)
##OkHttp配置的線程池##
在OkHttp,使用如下構(gòu)造了單例線程池
public synchronized ExecutorService executorService() {
if (executorService == null) {
executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false));
}
return executorService;
}
可以看出拱她,在Okhttp中二驰,構(gòu)建了一個閥值為[0, Integer.MAX_VALUE]的線程池,它不保留任何最小線程數(shù)秉沼,隨時創(chuàng)建更多的線程數(shù)桶雀,當線程空閑時只能活60秒,它使用了一個不存儲元素的阻塞工作隊列唬复,一個叫做"OkHttp Dispatcher"的線程工廠矗积。
這里解釋一下不存儲元素的阻塞工作隊列SynchronousQueue,Dispatcher內(nèi)部實現(xiàn)了懶加載無邊界限制的線程池方式敞咧,同時該線程池采用了SynchronousQueue這種阻塞隊列棘捣。SynchronousQueue每個插入操作必須等待另一個線程的移除操作,同樣任何一個移除操作都等待另一個線程的插入操作妄均。因此此隊列內(nèi)部其 實沒有任何一個元素柱锹,或者說容量是0哪自,嚴格說并不是一種容器丰包。由于隊列沒有容量,因此不能調(diào)用peek操作壤巷,因為只有移除元素時才有元素邑彪。顯然這是一種快速傳遞元素的方式,也就是說在這種情況下元素總是以最快的方式從插入者(生產(chǎn)者)傳遞給移除者(消費者)胧华,這在多任務(wù)隊列中是最快處理任務(wù)的方式寄症。對于高頻繁請求的場景,無疑是最適合的矩动。
也就是說有巧,在實際運行中,當收到10個并發(fā)請求時悲没,線程池會創(chuàng)建十個線程篮迎,當工作全部完成后,線程池會在60s后相繼關(guān)閉所有線程示姿。
---
##自己對于為什么Okhttp的線程管理優(yōu)越于Volley的一些理解##
首先是Volley中的工作線程是自己維護的甜橱,那么就有可能存在線程由于異常退出之后,沒有下一個工作線程補充的風險(線程池可以彌補這個缺陷)栈戳,那okhtyp是怎樣避免volley的這個缺陷呢岂傲?首先是okhttp是有線程池的,其次這個線程池采用了SynchronousQueue這種阻塞隊列子檀。(SynchronousQueue每個插入操作必須等待另一個線程的移除操作镊掖,同樣任何一個移除操作都等待另一個線程的插入操作乃戈。)所以當一個線程發(fā)生異常要退出,但是因為在SynchronousQueue的管理下堰乔,所以不會馬上退出偏化,還要等新的線程加入后才會退出,所以避免了線程異常退出后沒有線程補充的問題
然后是線程池會自動關(guān)閉空閑的線程镐侯,注意這里是關(guān)閉而已侦讨,不是銷毀,所以線程池還是會有線程的
---
##反向代理模型##
在OkHttp中苟翻,使用了與Nginx類似的反向代理與分發(fā)技術(shù)韵卤,這是典型的單生產(chǎn)者多消費者問題。
我們知道在Nginx中崇猫,用戶通過HTTP(Socket)訪問前置的服務(wù)器沈条,服務(wù)器會自動轉(zhuǎn)發(fā)請求給后端,并返回后端數(shù)據(jù)給用戶诅炉。通過將工作分配給多個后臺服務(wù)器蜡歹,可以提高服務(wù)的負載均衡能力,實現(xiàn)非阻塞涕烧、高并發(fā)連接月而,避免資源全部放到一臺服務(wù)器而帶來的負載,速度议纯,在線率等影響父款。
![此處輸入圖片的描述][15]
而在OkHttp中,非常類似于上述場景瞻凤,它使用Dispatcher作為任務(wù)的轉(zhuǎn)發(fā)器憨攒,線程池對應(yīng)多臺后置服務(wù)器,用AsyncCall對應(yīng)Socket請求阀参,用Deque<readyAsyncCalls>對應(yīng)Nginx的內(nèi)部緩存
![此處輸入圖片的描述][16]
可以發(fā)現(xiàn)請求是否進入緩存的條件如下:
```(runningRequests<64 && runningRequestsPerHost<5)```
如果滿足條件肝集,那么就直接把AsyncCall直接加到runningCalls的隊列中,并在線程池中執(zhí)行(線程池會根據(jù)當前負載自動創(chuàng)建蛛壳,銷毀杏瞻,緩存相應(yīng)的線程)。反之就放入readyAsyncCalls進行緩存等待炕吸。
當任務(wù)執(zhí)行完成后伐憾,無論是否有異常,finally代碼段總會被執(zhí)行赫模,也就是會調(diào)用Dispatcher的finished函數(shù)树肃,打開源碼,發(fā)現(xiàn)它將runningAsyncCalls中對應(yīng)的Call移除后瀑罗,接著執(zhí)行promoteCalls()函數(shù)
##第一部分:Summary請求的分發(fā)和任務(wù)隊列##
通過上述的分析胸嘴,我們知道了:
OkHttp采用Dispatcher技術(shù)雏掠,類似于Nginx,與線程池配合實現(xiàn)了高并發(fā)劣像,低阻塞的運行
Okhttp采用Deque作為緩存乡话,按照入隊的順序先進先出
OkHttp最出彩的地方就是在try/finally中調(diào)用了finished函數(shù),可以主動控制隊列的移動耳奕,而不是采用鎖绑青,極大減少了編碼復雜性
---
##第二部分:Socket管理##
###科普###
通常我們進行http連接時,首先進行tcp握手屋群,然后傳輸數(shù)據(jù)闸婴,最后釋放
這種方法的確簡單,但是在復雜的網(wǎng)絡(luò)內(nèi)容中就不夠用了芍躏,創(chuàng)建socket需要進行3次握手邪乍,而釋放socket需要2次握手(或者是4次)。重復的連接與釋放tcp連接就像每次僅僅擠1mm的牙膏就合上牙膏蓋子接著再打開接著擠一樣对竣。而每次連接大概是TTL一次的時間(也就是ping一次)庇楞,甚至在TLS環(huán)境下消耗的時間就更多了。很明顯否纬,當訪問復雜網(wǎng)絡(luò)時吕晌,延時(而不是帶寬)將成為非常重要的因素。
當然烦味,上面的問題早已經(jīng)解決了聂使,在http中有一種叫做keepalive connections的機制壁拉,它可以在傳輸數(shù)據(jù)后仍然保持連接谬俄,當客戶端需要再次獲取數(shù)據(jù)時,直接使用剛剛空閑下來的連接而不需要再次握手
當然keepalive也有缺點弃理,在提高了單個客戶端性能的同時溃论,復用卻阻礙了其他客戶端的鏈路速度
---
在okhttp中,socket連接池對用戶痘昌,甚至開發(fā)者都是透明的钥勋。它自動創(chuàng)建socket連接池,自動進行泄漏連接回收辆苔,自動幫你管理線程池算灸,提供了put/get/clear的接口,甚至調(diào)用都幫你寫好了驻啤。
我們知道在socket連接中菲驴,也就是Connection中,本質(zhì)是封裝好的流操作骑冗,除非手動close赊瞬,基本不會被gc掉先煎,非常容易引發(fā)內(nèi)存泄露。但是Okhttp通過引用計數(shù)法巧涧,實現(xiàn)將沒有被使用的Socket關(guān)閉薯蝎。java內(nèi)部有垃圾回收GC,okhttp有socket回收SocketClean谤绳;垃圾回收是根據(jù)對象的引用樹實現(xiàn)的占锯,而okhttp是根據(jù)RealConnection的虛引用StreamAllocation引用計數(shù)是否為0實現(xiàn)的。
##總結(jié)##
通過上面的分析缩筛,我們可以總結(jié)烟央,okhttp使用了類似于引用計數(shù)法與標記擦除法的混合使用,當連接空閑或者釋放時歪脏,StreamAllocation的數(shù)量會漸漸變成0疑俭,從而被線程池監(jiān)測到并回收,這樣就可以保持多個健康的keep-alive連接婿失,Okhttp的黑科技就是這樣實現(xiàn)的钞艇。
---
##第三部分:HTTP請求序列化/反序列化##
1. 獲得HTTP流(httpStream)
我們已經(jīng)在上文第二部分的RealConnection通過connectSocket()構(gòu)造HttpStream對象并建立套接字連接(完成三次握手)
```httpStream = connect();```
在connect()有非常重要的一步,它通過***okio庫***與遠程socket建立了I/O連接豪硅,為了更好的理解哩照,我們可以把它看成管道
/source 用于獲取response
source = Okio.buffer(Okio.source(rawSocket));
//sink 用于write buffer 到server
sink = Okio.buffer(Okio.sink(rawSocket));
***Okhttp的I/O使用的是Okio庫,它是java中最好用的I/O API***
2. 拼裝Raw請求與Headers(writeRequestHeaders)
我們通過Request.Builder構(gòu)建了簡陋的請求后懒浮,可能需要進行一些修飾飘弧,這時需要使用Interceptors對Request進行進一步的拼裝了。
***攔截器***是okhttp中強大的流程裝置砚著,它可以用來監(jiān)控log次伶,修改請求,修改結(jié)果稽穆,甚至是對用戶透明的GZIP壓縮冠王。類似于函數(shù)式編程中的flatmap操作。在okhttp中舌镶,內(nèi)部維護了一個Interceptors的List柱彻,通過InterceptorChain進行多次攔截修改操作。
![此處輸入圖片的描述][17]
---
##更新補充##
推薦在分析之前先看看這篇文章
一片分析OkHttp不錯的文章:[拆輪子系列:拆 OkHttp][18]
[1]: http://codecloud.net/android-okhttp-6425.html
[2]: http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0106/2275.html
[3]: http://blog.csdn.net/lmj623565791/article/details/47911083
[4]: http://gold.xitu.io/entry/5728441d128fe1006058b6b9
[5]: http://mushuichuan.com/2016/03/01/okhttpcache/
[6]: http://aiwoapp.blog.51cto.com/8677066/1619654
[7]: http://blog.csdn.net/lmj623565791/article/details/47911083
[8]: http://blog.csdn.net/chenzujie/article/details/46994073
[9]: http://frodoking.github.io/2015/03/12/android-okhttp/
[10]: http://o6uwc0k25.bkt.clouddn.com/%E6%80%BB%E4%BD%93%E8%AE%BE%E8%AE%A1.jpg
[11]: http://o6uwc0k25.bkt.clouddn.com/%E8%AF%B7%E6%B1%82%E6%B5%81%E7%A8%8B%E5%9B%BE.jpg
[12]: http://o6uwc0k25.bkt.clouddn.com/%E7%B1%BB%E8%AE%BE%E8%AE%A1%E5%9B%BE.jpg
[13]: http://o6uwc0k25.bkt.clouddn.com/%E8%BF%9E%E6%8E%A5%E6%89%A7%E8%A1%8C%E6%97%B6%E5%BA%8F%E5%9B%BE.jpg
[14]: http://www.reibang.com/p/aad5aacd79bf
[15]: http://o6uwc0k25.bkt.clouddn.com/%E5%8D%95%E7%94%9F%E4%BA%A7%E8%80%85%E5%A4%9A%E6%B6%88%E8%B4%B9%E8%80%85.jpg
[16]: http://o6uwc0k25.bkt.clouddn.com/diapatcher.jpg
[17]: http://o6uwc0k25.bkt.clouddn.com/%E6%8B%A6%E6%88%AA%E5%99%A8.jpg
[18]: http://mp.weixin.qq.com/s?__biz=MzA4MjU5NTY0NA==&mid=2653419018&idx=1&sn=932eec802048861e616a10fb8aca083b&scene=1&srcid=0818NBtIt2B6T6VtfUcQOb85#rd