2.Android okhttp源碼教學(xué)十年老司機(jī)帶你飛 面試官都得對你刮目相看(極度針對面試)

面試官:為什么用Okhttp逗余,而不選擇其它網(wǎng)絡(luò)框架饼丘?

支持HTTP2/SPDY,允許所有同一個主機(jī)地址的請求共享同一個Socket連接(SPDY是Google開發(fā)的基于TCP的傳輸層協(xié)議锯玛,用以最小化網(wǎng)絡(luò)延遲慢味,提升網(wǎng)絡(luò)速度阱缓,優(yōu)化用戶的網(wǎng)絡(luò)使用體驗欲诺。)

? ?關(guān)于HTTP2的優(yōu)點嗦锐,主要有:

多路復(fù)用:就是針對同個域名的請求稿蹲,都可以在同一條連接中并行進(jìn)行屿讽,而且頭部和數(shù)據(jù)都進(jìn)行了二進(jìn)制封裝屿储。

二進(jìn)制分幀:傳輸都是基于字節(jié)流進(jìn)行的贿讹,而不是文本,二進(jìn)制分幀層處于應(yīng)用層和傳輸層之間够掠。

頭部壓縮:HTTP1.x每次請求都會攜帶完整的頭部字段民褂,所以可能會出現(xiàn)重復(fù)傳輸,因此HTTP2采用HPACK對其進(jìn)行壓縮優(yōu)化祖屏,可以節(jié)省不少的傳輸流量助赞。

?OkHttp由于基于Http協(xié)議,所以http協(xié)議都支持

?在早期的版本中袁勺,OkHttp支持Http1.0,1.1,SPDY協(xié)議雹食,但是Http2協(xié)議的問世,導(dǎo)致OkHttp也做出了改變期丰,OkHttp鼓勵開發(fā)者使用HTTP2群叶,不再對SPDY協(xié)議給予支持吃挑。另外,新版本的OkHttp還有一個新的亮點就是支持WebScoket街立,這樣我? ?們就可以非常方便的建立長連接了

?2.連接池減少請求延時(socket自動選擇最好路線舶衬,并支持自動重連,擁有自動維護(hù)的socket連接池赎离,減少握手次數(shù)逛犹,減少了請求延遲,共享Socket,減少對服務(wù)器的請求次數(shù)梁剔。)

透明的GZIP壓縮減少響應(yīng)數(shù)據(jù)的大小(基于Headers的緩存策略減少重復(fù)的網(wǎng)絡(luò)請求)

透明的GZIP壓縮減少響應(yīng)數(shù)據(jù)的大小{{{擁有Interceptors輕松處理請求與響應(yīng)(自動處理GZip壓縮)}}}虽画,使用Gzip來壓縮request和response, 減少傳輸數(shù)據(jù)量, 從而減少流量消耗.重試及重定向

面試官:簡述一下OkHttp

OkHttp是一個非常優(yōu)秀的網(wǎng)絡(luò)請求框架,已被谷歌加入到Android的源碼中荣病。

支持http2码撰,對一臺機(jī)器的所有請求共享同一個socket? ?。1.2是否支持呢个盆?脖岛??颊亮?

內(nèi)置連接池柴梆,支持連接復(fù)用,減少延遲

支持透明的gzip壓縮響應(yīng)體

通過緩存避免重復(fù)的請求

請求失敗時自動重試主機(jī)的其他ip编兄,自動重定向

很多設(shè)計模式轩性,好用的API。鏈?zhǔn)秸{(diào)用

面試官:okhttp支持的協(xié)議是什么狠鸳?

android支持http2.0,Okhttp如何開啟的Http2.0

https://blog.csdn.net/weixin_42522669/article/details/117576189

有非常好的握手過程

自動的一個過程:ALPN協(xié)議

當(dāng)后端支持的協(xié)議內(nèi)包含Http2.0時,則就會把請求升級到Http2.0階段悯嗓。

面試官:看過OkHttp的源碼嗎件舵,簡單說一下

三大點:

第一。分發(fā)器(處理高并發(fā)請求)

1.用法解析 同步和異步 怎么實現(xiàn)的

2.okhttp線程池工作原理

3.同步隊列里面脯厨,運(yùn)行隊列和準(zhǔn)備怎么工作的

4.如何實現(xiàn)并發(fā)的铅祸?并發(fā)控制

第二。攔截器(每個攔截器的作用)

1.責(zé)任鏈模式

2.攔截器?緩存機(jī)制 ?緩存基于DiskLruCache

3.自定義攔截器

4.多域名如何封裝合武?測試和正式如何封裝

第三临梗。網(wǎng)絡(luò)攔截器Connection原理

1.攔截器?socker連接池復(fù)用機(jī)制詳解

緩存機(jī)制(攔截器)

2.在無網(wǎng)的時候直接使用緩存,這該怎么做呢稼跳?很簡單盟庞,我們只要自定義一個攔截器,在我們的請求頭中判斷沒有網(wǎng)絡(luò)可用時汤善,緩存策略為強(qiáng)制使用緩存什猖。

3.緩存機(jī)制是怎么樣的票彪?網(wǎng)絡(luò)請求緩存處理,okhttp如何處理網(wǎng)絡(luò)緩存的不狮?

重試機(jī)制(攔截器)

1.請求失敗了怎么做的

2.網(wǎng)絡(luò)重試是怎么實現(xiàn)的

比如:請求失敗之后降铸,會在結(jié)束之后,添加新的摇零?還是繼續(xù)進(jìn)行重試推掸?重試多少次?

面試官:怎么設(shè)計一個自己的網(wǎng)絡(luò)訪問框架,為什么這么設(shè)計驻仅?

同上:并發(fā)终佛,處理請求,處理響應(yīng)雾家,復(fù)用

先參考現(xiàn)有的框架铃彰,找一個比較合適的框架作為啟動點,比如說芯咧,基于上面講到的okhttp的優(yōu)點牙捉,選擇okhttp的源碼進(jìn)行閱讀,并且將主線的流程抽取出敬飒,為什么這么做邪铲,因為okhttp里面雖然涉及到了很多的內(nèi)容,但是我們用到的內(nèi)容并不是特別多无拗;保證先能運(yùn)行起來一個基本的框架带到;

考慮拓展,有了基本框架之后英染,我會按照我目前在項目中遇到的一些需求或者網(wǎng)路方面的問題揽惹,看看能不能基于我這個框架進(jìn)行優(yōu)化,比如服務(wù)器它設(shè)置的緩存策略四康,

我應(yīng)該如何去編寫客戶端的緩存策略去對應(yīng)服務(wù)器的搪搏,還比如說,可能剛剛?cè)ソ⒒镜目蚣軙r闪金,不會考慮HTTPS的問題疯溺,那么也會基于后來都要求https,進(jìn)行拓展哎垦;

面試官:你是怎么封裝okhttp的囱嫩?

同一的請求頭,然后封裝請求體參數(shù)漏设。處理響應(yīng)墨闲,通過泛型,然后回調(diào)里面解析數(shù)據(jù)

面試官:okhttp如何處理并發(fā)的愿题?

.同步和異步

.3隊列 ?64鏈接 ?5 host ? ? ?同步1個隊列损俭,異步2個隊列

異步請求處理:隊列是否滿蛙奖,否則假如到準(zhǔn)備隊列。然后線程池執(zhí)行杆兵,通過5大攔截器返回響應(yīng)

. 處理完成雁仲,移除 。然后判斷準(zhǔn)備隊列大小琐脏。添加到正在執(zhí)行的隊列里面

其中Dispatcher有一個線程池,用于執(zhí)行異步的請求.并且內(nèi)部還維護(hù)了3個雙向任務(wù)隊列,

分別是:準(zhǔn)備異步執(zhí)行的任務(wù)隊列攒砖、正在異步執(zhí)行的任務(wù)隊列、正在同步執(zhí)行的任務(wù)隊列.

/** Executes calls. Created lazily. */

//這個線程池是需要的時候才會被初始化

? ? private @Nullable

? ? ExecutorServiceexecutorService;

? ? /** Ready async calls in the order they'll be run. */

? ? private final DequereadyAsyncCalls =new ArrayDeque<>();

? ? /** Running asynchronous calls. Includes canceled calls that haven't finished yet. */

? ? private final DequerunningAsyncCalls =new ArrayDeque<>();

? ? /** Running synchronous calls. Includes canceled calls that haven't finished yet. */

? ? private final DequerunningSyncCalls =new ArrayDeque<>();

? ? public synchronized ExecutorServiceexecutorService() {

if (executorService ==null) {

//注意,該線程池沒有核心線程,線程數(shù)量可以是Integer.MAX_VALUE個(相當(dāng)于沒有限制),超過60秒沒干事就要被回收

? ? ? ? ? ? executorService =new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,

? ? ? ? ? ? ? ? ? ? new SynchronousQueue<>(), Util.threadFactory("OkHttp Dispatcher", false));

? ? ? ? }

return executorService;

? ? }

synchronized void enqueue(AsyncCall call) {

if (runningAsyncCalls.size()

runningAsyncCalls.add(call);

? ? executorService().execute(call);

? }else {

readyAsyncCalls.add(call);

? }

}

2個隊列:重點一:

首先加上同步鎖日裙,然后判斷實際的運(yùn)行請求數(shù)是否小于允許的最大的請求數(shù)量(64) 并且共享主機(jī)的正在運(yùn)行的調(diào)用的數(shù)量小于同時最大的相同Host的請求數(shù)(5)

同一個服務(wù)器地址不超過5個

public final class Dispatcher {? private int maxRequests = 64;? private int maxRequestsPerHost = 5;

如果這個AsyncCall請求符合條件(判斷實際的運(yùn)行請求數(shù)是否小于允許的最大的請求數(shù)量(64) 并且共享主機(jī)的正在運(yùn)行的調(diào)用的數(shù)量小于同時最大的相同Host的請求數(shù)(5)) 才會添加到執(zhí)行異步請求隊列,然后通過線程池進(jìn)行異步請求否則就把這個AsyncCall請求添加到就緒(等待)異步請求隊列當(dāng)中

如果都符合就把請求添加到正在執(zhí)行的異步請求隊列當(dāng)中,然后通過線程池去執(zhí)行這個請求call,否則的話在就緒(等待)異步請求隊列當(dāng)中添加

面試官:為什么是arrayDeque吹艇,這個隊列,有什么好處

說到這LinkedList表示不服昂拂,我們知道LinkedList同樣也實現(xiàn)了Deque接口受神,內(nèi)部是用鏈表實現(xiàn)的雙端隊列,那為什么不用LinkedList呢格侯?

實際上這與readyAsyncCalls向runningAsyncCalls轉(zhuǎn)換有關(guān)鼻听,當(dāng)執(zhí)行完一個請求或調(diào)用enqueue方法入隊新的請求時,會對readyAsyncCalls進(jìn)行一次遍歷联四,將那些符合條件的等待請求轉(zhuǎn)移到runningAsyncCalls隊列中并交給線程池執(zhí)行撑碴。盡管二者都能完成這項任務(wù),但是由于鏈表的數(shù)據(jù)結(jié)構(gòu)致使元素離散的分布在內(nèi)存的各個位置朝墩,CPU緩存無法帶來太多的便利醉拓,另外在垃圾回收時,使用數(shù)組結(jié)構(gòu)的效率要優(yōu)于鏈表收苏。

面試官:為什么要異步用兩個隊列呢亿卤?

因為Dispatcher默認(rèn)支持最大的并發(fā)請求是64個,單個Host最多執(zhí)行5個并發(fā)請求倒戏,

如果超過怠噪,則Call會先被放入到readyAsyncCall中,當(dāng)出現(xiàn)空閑的線程時杜跷,再將readyAsyncCall中的線程移入到runningAsynCalls中,執(zhí)行請求矫夷。先看Dispatcher的流程葛闷,跟著流程讀源碼

面試官:大于64鏈接之后,準(zhǔn)備都隊列是如何處理的双藕?準(zhǔn)備隊列是如何加入到運(yùn)行隊列里面淑趾?

處理完成一個請求之后,會在finlly調(diào)用忧陪,finish方法

private <T> void finished(Deque<T> calls, T call, boolean promoteCalls) {

? ? int runningCallsCount;

? ? Runnable idleCallback;

? ? synchronized (this) {

? ? ? if (!calls.remove(call)) throw new AssertionError("Call wasn't in-flight!");//將請求移除集合

? ? ? if (promoteCalls) promoteCalls();

? ? ?...

? ? }

private void promoteCalls() {

? ? if (runningAsyncCalls.size() >= maxRequests) return; // Already running max capacity.

? ? if (readyAsyncCalls.isEmpty()) return; // No ready calls to promote.


? ? for (Iterator<AsyncCall> i =?readyAsyncCalls.iterator(); i.hasNext(); ) {

? ? ? AsyncCall call = i.next();


? ? ? if (runningCallsForHost(call) < maxRequestsPerHost) {

? ? ? ? i.remove();

? ? ? ? runningAsyncCalls.add(call);

? ? ? ? executorService().execute(call);

? ? ? }

? ? ? if (runningAsyncCalls.size() >= maxRequests) return; // Reached max capacity.

? ? }

通過結(jié)束后再來一次

通過攔截器鏈得到Response,然后通過重定向攔截器判斷是否取消扣泊,取消調(diào)用callBack的失敗方法近范,沒有取消就直接返回結(jié)果

最后無論是否取消,都會調(diào)用dispatcher的finish方法延蟹,后面會講到

client.dispatcher().finished(this);

同步源代碼分析:

@Override public Responseexecute()throws IOException {

synchronized (this) {

if (executed)throw new IllegalStateException("Already Executed");

? ? executed =true;

? }

captureCallStackTrace();

? try {

client.dispatcher().executed(this);

? ? Response result = getResponseWithInterceptorChain();

? ? if (result ==null)throw new IOException("Canceled");

? ? return result;

? }finally {

client.dispatcher().finished(this);

? }

}

調(diào)用了dispatercher评矩,添加到隊列client.dispatcher().executed(this);

使用案例:

//1.創(chuàng)建OkHttpClient對象

? ? OkHttpClient okHttpClient =new OkHttpClient();

? ? //2.創(chuàng)建Request對象,設(shè)置一個url地址(百度地址),設(shè)置請求方式阱飘。

? ? Request request =new Request.Builder()

.url("http://www.baidu.com")

.get()

.build();

? ? //3.創(chuàng)建一個call對象,參數(shù)就是Request請求對象

? ? final Call call = okHttpClient.newCall(request);

? ? //4.同步調(diào)用會阻塞主線程,這邊在子線程進(jìn)行

? ? new Thread(new Runnable() {

@Override

? ? ? ? public void run() {

try {

//同步調(diào)用,返回Response,會拋出IO異常

? ? ? ? ? ? ? ? Response response =call.execute();

? ? ? ? ? ? }catch (IOException e) {

e.printStackTrace();

? ? ? ? ? ? }

}

}).start();

}

同步請求總結(jié):在哪個線程回調(diào)斥杜,就在哪個線程處理。

面試官:什么場景用同步請求沥匈?

舉例:分片上傳蔗喂!

異步請求:

executorService().execute(call);

public synchronized ExecutorServiceexecutorService() {

if (executorService ==null) {

executorService =new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS,

? ? ? ? new SynchronousQueue(), Util.threadFactory("OkHttp Dispatcher", false));

? }

return executorService;

}

面試官:okhttp線程池工作原理是怎樣的?

在Okhttp中高帖,構(gòu)建了一個核心為[0, Integer.MAX_VALUE]的線程池缰儿,它不保留任何最小線程數(shù),隨時創(chuàng)建更多的線程數(shù)散址,當(dāng)線程空閑時只能活60秒

線程池execute乖阵,其實就是要執(zhí)行線程的run方法有封裝了一層,最終看

AsyncCall的excute方法:

@Override protected void execute() {

boolean signalledCallback =false;

? try {

Response response = getResponseWithInterceptorChain();

? ? if (retryAndFollowUpInterceptor.isCanceled()) {

signalledCallback =true;

? ? ? responseCallback.onFailure(RealCall.this, new IOException("Canceled"));

? ? }else {

signalledCallback =true;

? ? ? responseCallback.onResponse(RealCall.this, response);

? ? }

}catch (IOException e) {

if (signalledCallback) {

// Do not signal the callback twice!

? ? ? Platform.get().log(INFO, "Callback failure for " + toLoggableString(), e);

? ? }else {

responseCallback.onFailure(RealCall.this, e);

? ? }

}finally {

client.dispatcher().finished(this);

? }

}

同步和異步總結(jié):

對于同步和異步請求爪飘,唯一的區(qū)別就是異步請求會放在線程池(ThreadPoolExecutor)中去執(zhí)行义起,而同步請求則會在當(dāng)前線程中執(zhí)行,注意:同步請求會阻塞當(dāng)前線程师崎。

同步請求可以得到respones,異步請求通過回調(diào)得到默终。

面試官:okhttp的攔截器是怎么理解的?

1.攔截器主要處理2個東西犁罩,request和respondse.可以看看源碼的攔截器齐蔽,攔截器主要用來觀察,修改以及可能短路的請求輸出和響應(yīng)的回來床估。

在依次介紹各個攔截器之前含滴,先介紹一個比較重要的類:RealInterceptorChain,直譯就是攔截器鏈類丐巫;

Response result = getResponseWithInterceptorChain();

沒錯谈况,在getResponseWithInterceptorChain();方法中我們就用到了這個RealInterceptorChain類。

ResponsegetResponseWithInterceptorChain()throws IOException {

// Build a full stack of interceptors.

? List interceptors =new ArrayList<>();

? interceptors.addAll(client.interceptors());

? interceptors.add(retryAndFollowUpInterceptor);

? interceptors.add(new BridgeInterceptor(client.cookieJar()));

? interceptors.add(new CacheInterceptor(client.internalCache()));

? interceptors.add(new ConnectInterceptor(client));

? if (!forWebSocket) {

interceptors.addAll(client.networkInterceptors());

? }

interceptors.add(new CallServerInterceptor(forWebSocket));

? Interceptor.Chain chain =new RealInterceptorChain(

interceptors, null, null, null, 0, originalRequest);

? return chain.proceed(originalRequest);

}

2.責(zé)任鏈模式:(遞歸調(diào)用)循環(huán)調(diào)用递胧,通過不停的調(diào)用自己碑韵。然后最后退出

public Response proceed(Request request)throws IOException {

return this.proceed(request, this.streamAllocation, this.httpCodec, this.connection);

面試官:責(zé)任鏈模式是怎么樣的?

責(zé)任鏈模式的類似情況如下:

publicResponse proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec,

? ? RealConnection connection)throws IOException {

if (index >=interceptors.size())throw new AssertionError();

? calls++;

? // If we already have a stream, confirm that the incoming request will use it.

? if (this.httpCodec !=null && !this.connection.supportsUrl(request.url())) {

throw new IllegalStateException("network interceptor " +interceptors.get(index -1)

+" must retain the same host and port");

? }

// If we already have a stream, confirm that this is the only call to chain.proceed().

? if (this.httpCodec !=null &&calls >1) {

throw new IllegalStateException("network interceptor " +interceptors.get(index -1)

+" must call proceed() exactly once");

? }

// Call the next interceptor in the chain.

? RealInterceptorChain next =new RealInterceptorChain(

interceptors, streamAllocation, httpCodec, connection, index +1, request);

? Interceptor interceptor =interceptors.get(index);

? Response response = interceptor.intercept(next);

? // Confirm that the next interceptor made its required call to chain.proceed().

? if (httpCodec !=null &&index +1

throw new IllegalStateException("network interceptor " + interceptor

+" must call proceed() exactly once");

? }

// Confirm that the intercepted response isn't null.

? if (response ==null) {

throw new NullPointerException("interceptor " + interceptor +" returned null");

? }

圖解:

每個攔截器負(fù)責(zé)一個特殊的職責(zé).最后那個攔截器負(fù)責(zé)請求服務(wù)器,然后服務(wù)器返回了數(shù)據(jù)再根據(jù)這個攔截器的順序逆序返回回去,最終就得到了網(wǎng)絡(luò)數(shù)據(jù).

demo:http://www.reibang.com/p/ecf55b01ec19

OkHttp的這種攔截器鏈采用的是責(zé)任鏈模式缎脾,這樣的好處是將請求的發(fā)送和處理分開祝闻,并且可以動態(tài)添加中間的處理方實現(xiàn)對請求的處理、短路等操作遗菠。

責(zé)任鏈模式:一個對象持有下個對象的引用联喘;

請求:通過5個攔截器华蜒,把一個一個請求拼接起來

結(jié)果:最后一個先響應(yīng),先進(jìn)后出的原理豁遭。

舉一個列子:出現(xiàn)彈框叭喜,然后一個個關(guān)閉。

攔截器得到響應(yīng)的結(jié)果可以看到這里的攔截鏈?zhǔn)褂玫姆浅G擅畹炭颍悬c像棧的數(shù)據(jù)結(jié)構(gòu)域滥。依次將各個攔截器的方法入棧,最后得到response蜈抓,再依次彈棧启绰。如果是我來寫的話,可能就直接一個for循環(huán)依次調(diào)用每個攔截器的攔截方法沟使。但是這樣的話我還得再來一遍反循環(huán)委可,再來依次處理加工response。很明顯這里棧的結(jié)構(gòu)更符合我們的業(yè)務(wù)場景腊嗡。

?demo:Android 手寫okhttp責(zé)任鏈:

面試官:okhttp有哪些攔截器着倾?它們的作用分別是什么?

五種攔截器

看源碼就知道執(zhí)行順序是

retryAndFollowUpInterceptor-》BridgeInterceptor-》CacheInterceptor-》ConnectInterceptor-》CallServerInterceptor

但是看上面這段源碼發(fā)現(xiàn)好像并不是只有這五種對吧燕少!有兩句代碼卡者,分別是addAll方法,添加了兩個集合客们,集合存儲的是啥崇决?

這里其實是自定義攔截器,可以看到底挫,自定義攔截器添加的順序分別又有兩種

根據(jù)順序分別叫做:Application Interceptors和Network Interceptors

CallServerInterceptor是攔截器鏈中最后一個攔截器恒傻,負(fù)責(zé)將網(wǎng)絡(luò)請求提交給服務(wù)器

攔截器的執(zhí)行順序:先執(zhí)行自定義的攔截器

interceptors.addAll(client.interceptors());

interceptors.add(retryAndFollowUpInterceptor);

interceptors.add(new BridgeInterceptor(client.cookieJar()));

interceptors.add(new CacheInterceptor(client.internalCache()));

interceptors.add(new ConnectInterceptor(client));

if (!forWebSocket) {

interceptors.addAll(client.networkInterceptors());

}

interceptors.add(new CallServerInterceptor(forWebSocket));

第一攔截器:RetryAndFollowUpInterceptor(重試,重定向攔截器建邓,code:301,302)

常用的重定向方式有:301 redirect盈厘、302 redirect與meta fresh。

用來實現(xiàn)連接失敗的重試和重定向

重試機(jī)制:

他處于責(zé)任鏈的頂端官边,負(fù)責(zé)網(wǎng)絡(luò)請求的開始工作沸手,也負(fù)責(zé)收尾的工作。

一開始注簿,創(chuàng)建了StreamAllocation對象罐氨,他封裝了網(wǎng)絡(luò)請求相關(guān)的信息:連接池,地址信息滩援,網(wǎng)絡(luò)請求,事件回調(diào)塔嬉,負(fù)責(zé)網(wǎng)絡(luò)連接的連接玩徊、關(guān)閉租悄,釋放等操作

followUpCount是用來記錄我們發(fā)起網(wǎng)絡(luò)請求的次數(shù)的,為什么我們發(fā)起一個網(wǎng)絡(luò)請求恩袱,可能okhttp會發(fā)起多次呢泣棋?

例如https的證書驗證,我們需要經(jīng)過:發(fā)起 -> 驗證 -> 響應(yīng)畔塔,三個步驟需要發(fā)起至少兩次的請求潭辈,或者我們的網(wǎng)絡(luò)請求被重定向,在我們第一次請求得到了新的地址后澈吨,再向新的地址發(fā)起網(wǎng)絡(luò)請求把敢。(發(fā)現(xiàn)遷移到新地址,訪問新的服務(wù)器地址谅辣!)

在網(wǎng)絡(luò)請求中修赞,不同的異常,重試的次數(shù)也不同桑阶,okhttp捕獲了兩種異常:RouteException和IOException柏副。

RouteException:所有網(wǎng)絡(luò)連接失敗的異常,包括IOException中的連接失敗異常蚣录;

IOException:除去連接異常的其他的IO異常割择。

這個時候我們需要判斷是否需要重試:

其中的路由地址我們先忽略,這個之后我們還會討論萎河。假定沒有其他路由地址的情況下:

1荔泳、連接失敗,并不會重試公壤;

2换可、如果連接成功,因為特定的IO異常(例如認(rèn)證失斚梅)沾鳄,也不會重試

其實這兩種情況是可以理解的,如果連接異常确憨,例如無網(wǎng)絡(luò)狀態(tài)译荞,重試也只是毫秒級的任務(wù),不會有特別明顯的效果休弃,如果是網(wǎng)絡(luò)很慢吞歼,到了超時時間,應(yīng)該讓用戶及時了解失敗的原因塔猾,如果一味重試篙骡,用戶就會等待多倍的超時時間,用戶體驗并不好。認(rèn)證失敗的情況就更不用多說了糯俗。

3. 默認(rèn)重連3次??????

RetryAndFollowUpInterceptor攔截設(shè)置最大重定向次數(shù)為20次尿褪;

private static final int MAX_FOLLOW_UPS =20;

如果我們非要重試多次怎么辦?

自定義Interceptor得湘,增加計數(shù)器杖玲,重試到你滿意就可以了:

通過 recover 方法檢測該 RouteException 是否能重新連接;

第二攔截器:BridgeInterceptor(橋接攔截器)

用來修改請求和響應(yīng)的?header?信息

負(fù)責(zé)設(shè)置編碼方式淘正,添加頭部摆马,Keep-Alive 連接以及應(yīng)用層和網(wǎng)絡(luò)層請求和響應(yīng)類型之間的相互轉(zhuǎn)換

public final class BridgeInterceptorimplements Interceptor {

private final CookieJar cookieJar;

? ? public BridgeInterceptor(CookieJar cookieJar) {

this.cookieJar = cookieJar;

? ? }

public Response intercept(Chain chain)throws IOException {

Request userRequest = chain.request();

? ? ? ? Builder requestBuilder = userRequest.newBuilder();

? ? ? ? RequestBody body = userRequest.body();

? ? ? ? if (body !=null) {

MediaType contentType = body.contentType();

? ? ? ? ? ? if (contentType !=null) {

requestBuilder.header("Content-Type", contentType.toString());

? ? ? ? ? ? }

long contentLength = body.contentLength();

? ? ? ? ? ? if (contentLength != -1L) {

requestBuilder.header("Content-Length", Long.toString(contentLength));

? ? ? ? ? ? ? ? requestBuilder.removeHeader("Transfer-Encoding");

? ? ? ? ? ? }else {

requestBuilder.header("Transfer-Encoding", "chunked");

? ? ? ? ? ? ? ? requestBuilder.removeHeader("Content-Length");

? ? ? ? ? ? }

}

第三攔截器:CacheInterceptor(緩存攔截器)

用來實現(xiàn)響應(yīng)緩存。比如獲取到的 Response 帶有 Date鸿吆,Expires囤采,Last-Modified,Etag 等 header伞剑,表示該 Response 可以緩存一定的時間斑唬,下次請求就可以不需要發(fā)往服務(wù)端,直接拿緩存的

面試官:okhttp的緩存是怎么樣的黎泣?

緩存策略

1恕刘、如果網(wǎng)絡(luò)不可用并且無可用的有效緩存,則返回504錯誤抒倚;

2褐着、繼續(xù),如果不需要網(wǎng)絡(luò)請求托呕,則直接使用緩存含蓉;

3、繼續(xù)项郊,如果需要網(wǎng)絡(luò)可用馅扣,則進(jìn)行網(wǎng)絡(luò)請求;

4着降、繼續(xù)差油,如果有緩存,并且網(wǎng)絡(luò)請求返回HTTP_NOT_MODIFIED任洞,說明緩存還是有效的蓄喇,則合并網(wǎng)絡(luò)響應(yīng)和緩存結(jié)果。同時更新緩存交掏;

5妆偏、繼續(xù),如果沒有緩存盅弛,則寫入新的緩存钱骂;

對于okhttp的緩存解決方案叔锐,我的需求是:

1、有網(wǎng)的時候也可以讀取緩存罐柳,并且可以控制緩存的過期時間掌腰,這樣可以減輕服務(wù)器壓力

2、有網(wǎng)的時候不讀取緩存张吉,比如一些及時性較高的接口請求

3、無網(wǎng)的時候讀取緩存催植,并且可以控制緩存過期的時間

okhttp&http緩存策略

強(qiáng)制緩存:當(dāng)用戶端第一次請求數(shù)據(jù)是肮蛹,服務(wù)端返回了緩存的過期時間(Expires與Cache-Control),沒有過期即可以繼續(xù)使用緩存创南,否則則不適用伦忠,無需再向服務(wù)端訊問。

比照緩存:當(dāng)用戶端第一次請求數(shù)據(jù)時稿辙,服務(wù)端會將緩存標(biāo)識(Etag/If-None-Match與Last-Modified/If-Modified-Since)與數(shù)據(jù)一起返回給用戶端昆码,用戶端將兩者都備份到緩存中 ,再次請求數(shù)據(jù)時邻储,用戶端將上次備份的緩存

標(biāo)識發(fā)送給服務(wù)端赋咽,服務(wù)端根據(jù)緩存標(biāo)識進(jìn)行判斷,假如返回304吨娜,則表示緩存可用脓匿,假如返回200,標(biāo)識緩存不可用宦赠,使用最新返回的數(shù)據(jù)陪毡。

ETag是用資源標(biāo)識碼標(biāo)識資源能否被修改,Last-Modified是用時間戳標(biāo)識資源能否被修改勾扭。ETag優(yōu)先級高于Last-Modified毡琉。

解決辦法:添加攔截器:

/**

? ? * 有網(wǎng)時候的緩存

? ? */finalInterceptorNetCacheInterceptor=newInterceptor(){@OverridepublicResponseintercept(Chainchain)throwsIOException{Requestrequest=chain.request();Responseresponse=chain.proceed(request);intonlineCacheTime=30;//在線的時候的緩存過期時間,如果想要不緩存妙色,直接時間設(shè)置為0returnresponse.newBuilder().header("Cache-Control","public, max-age="+onlineCacheTime).removeHeader("Pragma").build();}};/**

? ? * 沒有網(wǎng)時候的緩存

? ? */finalInterceptorOfflineCacheInterceptor=newInterceptor(){@Override

緩存策略:

那么服務(wù)器是如何對比呢桅滋?有兩種情況,一種是ETag燎斩。服務(wù)器會對資源做一個ETag算法虱歪,生成一個字符串。這個字符串實際上代表了資源在服務(wù)器的版本栅表。服務(wù)器在響應(yīng)頭中會加入這個ETag給客戶端笋鄙。客戶端緩存后下次繼續(xù)請求的時候把緩存中的ETag取出來加入到請求頭怪瓶。服務(wù)器取出請求頭中的ETag萧落,與當(dāng)前服務(wù)器該資源實時的ETag作對比践美。如果不一致則表明服務(wù)器有更新資源,則返回200響應(yīng)碼找岖,并返回資源陨倡。如果客戶端的ETag和服務(wù)端的相同,代表服務(wù)端沒有更新數(shù)據(jù)许布,則返回304響應(yīng)碼兴革,并不返回資源。這樣就達(dá)到了使用緩存去節(jié)省時間和流量的目的蜜唾。另一種是Last-Modified杂曲,它代表的是資源在服務(wù)器更新的時間,和ETag類似客戶端從緩存中取出這個時間加入到請求頭袁余,服務(wù)器根據(jù)最近的一次更新時間與之對比擎勘,決定是返回資源還是返回304。

總結(jié)下颖榜,如果服務(wù)器通過Cache-Control頭規(guī)定了過期時間棚饵,沒過期的話可以直接使用緩存。否則掩完,使用Etag或者Last-Modified加入請求體噪漾,服務(wù)器接收后與最新的數(shù)據(jù)進(jìn)行比對,決定直接返回最新數(shù)據(jù)還是304響應(yīng)碼藤为。是不是其實一點都不復(fù)雜呢怪与?

我們的客戶端也就是OkHttpClient在緩存這塊要做哪些事呢?

1.首先缅疟,需要取緩存分别,并確定緩存有無過期病涨。

2.如果過期核芽,看看有沒有Etag或Last-Modified可以加到請求體。

3.最后如果服務(wù)器返回304枢赔,我們要直接使用緩存桅咆,

4.如果返回200括授,我們需要更新緩存。按照這個順序岩饼,我們來在源碼中找一找荚虚,這些操作是如何實現(xiàn)的。

1)CacheControl( HTTP 中的Cache-Control 和Pragma 緩存控制):指定緩存規(guī)則

2)Cache(緩存類)

3)DiskLruCache(文件化的LRU 緩存類)

(1)讀取緩存:先獲限OkHttpClient 的Cache 緩存對象籍茧,就是上面創(chuàng)建OkHttpClient 設(shè)置的

Cahce; 傳Request 請求到Cache 的get 方法查找緩存響應(yīng)數(shù)據(jù)Response版述;構(gòu)造一個緩存策略,

再調(diào)用它的get 去決策使用網(wǎng)絡(luò)請求還是緩存響應(yīng)寞冯。若使用緩存渴析,它的cacheResponse 不為

空,networkRequest 為空晚伙,用緩存構(gòu)造響應(yīng)直接返回。若使用請求俭茧,則cacheResponse 為

空,networkRequest 不為空咆疗,開始網(wǎng)絡(luò)請求流程。

Cache 的get 獲取緩存方法母债,計算request 的key 值(請求url 進(jìn)行md5 加密)午磁,根據(jù)key 值

去DisLruCache 查找是否存在緩存內(nèi)容,存則則創(chuàng)建繪存Entry 實體场斑。ENTRY_METADATA 代表

響應(yīng)頭信息漓踢,ENTRY_BODY 代表響應(yīng)體信息。如果緩存存在漏隐,在指定目錄下會有兩個文件

****.0 *****.1 分別存儲某個請求緩存響應(yīng)頭和響應(yīng)體信息。

CacheStrategy 的get 方法:1)若緩存響應(yīng)為空或2)請求是https 但緩存響應(yīng)沒有握手信息奴迅;

3)請求和緩存響應(yīng)都是不可緩存的青责;4)請求是onCache,并且又包含if-Modified-Since 或

If-None-Match 則不使用緩存取具; 再計算請求有效時間是否符合響應(yīng)的過期時間脖隶,若響應(yīng)在有

效范圍內(nèi),則緩存策略使用緩存暇检,否則創(chuàng)建一個新的有條件的請求产阱,返回有條件的緩存策略。

( 2 ) 存儲緩存流程: 從HttpEngine 的readResponse() 發(fā)送請求開始块仆, 判斷

hasBody(userResponse), 如果緩存的話构蹬, maybeCache() 緩存響應(yīng)頭信息,

unzip(cacheWritingResponse(storeRequest, userResponse))緩存響應(yīng)體

3.網(wǎng)絡(luò)請求緩存處理悔据,okhttp 如何處理網(wǎng)絡(luò)緩存的庄敛;

(1)網(wǎng)絡(luò)緩存優(yōu)先考慮強(qiáng)制緩存,再考慮對比緩存

--首先判斷強(qiáng)制緩存中的數(shù)據(jù)的是否在有效期內(nèi)科汗。如果在有效期藻烤,則直接使用緩存。如

果過了有效期头滔,則進(jìn)入對比緩存怖亭。

--在對比緩存過程中,判斷ETag 是否有變動坤检,如果服務(wù)端返回沒有變動兴猩,說明資源未改

變,使用緩存缀蹄。如果有變動峭跳,判斷Last-Modified膘婶。

--判斷Last-Modified,如果服務(wù)端對比資源的上次修改時間沒有變化蛀醉,則使用緩存悬襟,否

則重新請求服務(wù)端的數(shù)據(jù),并作緩存工作

CacheStrategy是其中的緩存策略,Cache類

除了緩存策略類CacheStrategy拯刁,還有一個重要的類Cache沒講脊岳。這里簡單講下這個類就是負(fù)責(zé)存取緩存的。它里面存儲緩存的對象是DiskLruCache垛玻,而DiskLruCache內(nèi)部又是LinkedHashMap<String, Entry>割捅,關(guān)于LinkedHashMap大家有可以查閱一下相關(guān)文章,它底層是一個哈希表和鏈表的組合帚桩,并且由于同時維護(hù)了一個可以給存儲對象排序的雙向鏈表亿驾,因此可以實現(xiàn)Lru算法。

private static boolean validate(Response cached, Response network) {

if (network.code() ==304) {

return true;

? ? }else {

Date lastModified = cached.headers().getDate("Last-Modified");

? ? ? ? if (lastModified !=null) {

Date networkLastModified = network.headers().getDate("Last-Modified");

? ? ? ? ? ? if (networkLastModified !=null && networkLastModified.getTime() < lastModified.getTime()) {

return true;

? ? ? ? ? ? }

}

return false;

? ? }

}

if

(networkRequest ==null && cacheResponse ==null) {

return (new Builder()).request(chain.request()).protocol(Protocol.HTTP_1_1).code(504).message("Unsatisfiable Request (only-if-cached)").body(EMPTY_BODY).sentRequestAtMillis(-1L).receivedResponseAtMillis(System.currentTimeMillis()).build();

}else if (networkRequest ==null) {

return cacheResponse.newBuilder().cacheResponse(stripBody(cacheResponse)).build();

}else {

分析:

1.cacheResponse也為null說明我們沒有有效的緩存response账嚎,而我們又不會進(jìn)行網(wǎng)絡(luò)請求莫瞬,因此給上層構(gòu)建了一個響應(yīng)碼味504的response

2.如果cacheResponse不為null,說明我們有可用緩存郭蕉,而此次請求又不會再請求網(wǎng)絡(luò)疼邀,因此直接將緩存response返回。

3.重點分析:

1).此時如果服務(wù)器返回的響應(yīng)碼為HTTP_NOT_MODIFIED召锈,也就是我們常見的304旁振,代表服務(wù)器的資源沒有變化,客戶端去取本地緩存即可涨岁,此時服務(wù)器不會返回響應(yīng)體

private static boolean

validate(Response cached, Response network) {

if (network.code() ==304) {

return true;

? ? }else {

Date lastModified = cached.headers().getDate("Last-Modified");

? ? ? ? if (lastModified !=null) {

Date networkLastModified = network.headers().getDate("Last-Modified");

? ? ? ? ? ? if (networkLastModified !=null && networkLastModified.getTime() < lastModified.getTime()) {

return true;

? ? ? ? ? ? }

}

return false;

? ? }

}

2).直接使用networkResponse構(gòu)建response并返回拐袜。此時我們還需要做一件事,就是更新我們的緩存卵惦,將最終response寫入到cache對象中去

private Response cacheWritingResponse(final CacheRequest cacheRequest, Response response)throws IOException {

if (cacheRequest ==null) {

return response;

? ? }else {

Sink cacheBodyUnbuffered = cacheRequest.body();

? ? ? ? if (cacheBodyUnbuffered ==null) {

return response;

? ? ? ? }else {

final BufferedSource source = response.body().source();

? ? ? ? ? ? final BufferedSink cacheBody = Okio.buffer(cacheBodyUnbuffered);

? ? ? ? ? ? Source cacheWritingSource =new Source() {

boolean cacheRequestClosed;

? ? ? ? ? ? ? ? public long read(Buffer sink, long byteCount)throws IOException {

long bytesRead;

? ? ? ? ? ? ? ? ? ? try {

bytesRead = source.read(sink, byteCount);

? ? ? ? ? ? ? ? ? ? }catch (IOException var7) {

if (!this.cacheRequestClosed) {

this.cacheRequestClosed =true;

? ? ? ? ? ? ? ? ? ? ? ? ? ? cacheRequest.abort();

? ? ? ? ? ? ? ? ? ? ? ? }

throw var7;

? ? ? ? ? ? ? ? ? ? }

if (bytesRead == -1L) {

if (!this.cacheRequestClosed) {

this.cacheRequestClosed =true;

? ? ? ? ? ? ? ? ? ? ? ? ? ? cacheBody.close();

? ? ? ? ? ? ? ? ? ? ? ? }

return -1L;

? ? ? ? ? ? ? ? ? ? }else {

sink.copyTo(cacheBody.buffer(), sink.size() - bytesRead, bytesRead);

? ? ? ? ? ? ? ? ? ? ? ? cacheBody.emitCompleteSegments();

? ? ? ? ? ? ? ? ? ? ? ? return bytesRead;

? ? ? ? ? ? ? ? ? ? }

}

public Timeout timeout() {

return source.timeout();

? ? ? ? ? ? ? ? }

public void close()throws IOException {

if (!this.cacheRequestClosed && !Util.discard(this, 100, TimeUnit.MILLISECONDS)) {

this.cacheRequestClosed =true;

? ? ? ? ? ? ? ? ? ? ? ? cacheRequest.abort();

? ? ? ? ? ? ? ? ? ? }

source.close();

? ? ? ? ? ? ? ? }

};

? ? ? ? ? ? return response.newBuilder().body(new RealResponseBody(response.headers(), Okio.buffer(cacheWritingSource))).build();

? ? ? ? }

}

}

先了解下HTTP協(xié)議的緩存機(jī)制:

首先緩存分為三種:過期時間緩存阻肿、第一差異緩存和第二差異緩存,而且在優(yōu)先級上沮尿,過期時間緩存 > 第一差異緩存 > 第二差異緩存丛塌。

過期時間緩存,就是通過HTTP響應(yīng)頭部的字段控制:

expires:響應(yīng)字段畜疾,絕對過期時間赴邻,HTTP1.0。

Cache-Control:響應(yīng)字段啡捶,相對過期時間姥敛,HTTP1.1。注意如果值為no-cache瞎暑,表示跳過過期時間緩存邏輯彤敛,值為no-store表示跳過過期時間緩存邏輯和差異緩存邏輯与帆,也就是不使用緩存數(shù)據(jù)。

當(dāng)客戶端請求時墨榄,發(fā)現(xiàn)緩存未過期玄糟,就直接返回緩存數(shù)據(jù)了,不請求網(wǎng)絡(luò)袄秩,否則阵翎,執(zhí)行第一差異緩存邏輯:

If-None-Match:請求字段,值為ETag之剧。

ETag:響應(yīng)字段郭卫,服務(wù)端會根據(jù)內(nèi)容生成唯一的字符串。

如果服務(wù)端發(fā)現(xiàn)If-None-Match的值和當(dāng)前ETag一樣背稼,就說明數(shù)據(jù)內(nèi)容沒有變化贰军,就返回304,否則蟹肘,執(zhí)行第二差異緩存邏輯:

If-Modified-Since:請求字段谓形,客戶端告訴服務(wù)端本地緩存的資源的上次修改時間。

Last-Modified:響應(yīng)字段疆前,服務(wù)端告訴客戶端資源的最后修改時間。

如果服務(wù)端發(fā)現(xiàn)If-Modified-Since的值就是資源的最后修改時間聘萨,就說明數(shù)據(jù)內(nèi)容沒有變化竹椒,就返回304,否則米辐,返回所有資源數(shù)據(jù)給客戶端胸完,響應(yīng)碼為200。

回到OkHttp翘贮,CacheInterceptor攔截器處理的邏輯赊窥,其實就是上面所說的HTTP緩存邏輯,注意到OkHttp提供了一個現(xiàn)成的緩存類Cache狸页,它采用DiskLruCache實現(xiàn)緩存策略锨能,至于緩存的位置和大小,需要你自己指定芍耘。

這里其實會有個問題址遇,上面的緩存都是依賴HTTP協(xié)議本身的緩存機(jī)制的,如果我們請求的服務(wù)器不支持這套緩存機(jī)制斋竞,或者需要實現(xiàn)更靈活的緩存管理倔约,直接使用上面這套緩存機(jī)制就可能不太可行了,這時我們可以自己新增攔截器坝初,自行實現(xiàn)緩存的管理浸剩。

第四攔截器:

ConnectInterceptor負(fù)責(zé)與服務(wù)器建立鏈接:很重要(是最后一個攔截器)

和服務(wù)器通信

我們發(fā)現(xiàn)目前為止我們還沒有進(jìn)行真正的請求钾军。別急,ConnectInterceptor就是一個負(fù)責(zé)建立http連接的攔截器

封裝了socket連接和TLS握手等邏輯

用來打開到服務(wù)端的連接绢要。其實是調(diào)用了 StreamAllocation 的newStream 方法來打開連接的吏恭。建聯(lián)的 TCP 握手,TLS 握手都發(fā)生該階段袖扛。過了這個階段砸泛,和服務(wù)端的 socket 連接打通

public final class RealConnectionextends Listenerimplements Connection {

private final Route route;

? ? private Socket rawSocket;

? ? public Socket socket;

? ? private Handshake handshake;

? ? private Protocol protocol;

? ? public volatile FramedConnection framedConnection;

? ? public int successCount;

? ? public BufferedSource source;

? ? public BufferedSink sink;

? ? public int allocationLimit;

? ? public final List> allocations =new ArrayList();

? ? public boolean noNewStreams;

? ? public long idleAtNanos =9223372036854775807L;

? ? public RealConnection(Route route) {

this.route = route;

? ? }

2)然后在ConnectInterceptor,也就是負(fù)責(zé)建立連接的攔截器中蛆封,首先會找可用連接唇礁,也就是從連接池中去獲取連接,具體的就是會調(diào)用到ConectionPool的get方法惨篱。

需要看下這個源碼:ConnectInterceptor的intercept()方法

publicResponseintercept(Chainchain)throwsIOException{RealInterceptorChainrealChain=(RealInterceptorChain)chain;Requestrequest=realChain.request();StreamAllocationstreamAllocation=realChain.streamAllocation();// We need the network to satisfy this request. Possibly for validating a conditional GET.booleandoExtensiveHealthChecks=!request.method().equals("GET");// 1HttpCodechttpCodec=streamAllocation.newStream(client,chain,doExtensiveHealthChecks);RealConnectionconnection=streamAllocation.connection();returnrealChain.proceed(request,streamAllocation,httpCodec,connection);}

來源:http://www.reibang.com/p/7cdd598d382b

第五攔截器:CallServerInterceptor

主要的工作就是把請求的Request寫入到服務(wù)端盏筐,而后從服務(wù)端讀取Response。

(1)砸讳、寫入請求頭

(2)琢融、寫入請求體

(3)、讀取響應(yīng)頭

(4)簿寂、讀取響應(yīng)體

用來發(fā)起請求并且得到響應(yīng)漾抬。上一個階段已經(jīng)握手成功,HttpStream 流已經(jīng)打開常遂,所以這個階段把 Request 的請求信息傳入流中纳令,并且從流中讀取數(shù)據(jù)封裝成 Response 返回

面試官:自定義過哪些攔截器?

自定義攔截器:通常情況下攔截器用來添加克胳,移除或者轉(zhuǎn)換請求或者響應(yīng)的頭部信息平绩。比如將域名替換為ip地址,將請求頭中添加host屬性漠另,也可以添加我們應(yīng)用中的一些公共參數(shù)捏雌,比如設(shè)備id、版本號等等

okhttp攔截器主要在以下幾種情況使用:

自定義攔截器不一定要繼承基本的5大攔截器笆搓,而是繼承Interceptor

網(wǎng)絡(luò)請求性湿、響應(yīng)日志輸出

在Header中統(tǒng)一添加cookie、token

設(shè)置網(wǎng)絡(luò)緩存

1.添加日志攔截器

可以用系統(tǒng)的砚作,或者通過添加自己寫的攔截器

2.在Header中統(tǒng)一添加cookie窘奏、token ?

public?class?HeaderInterceptor?implements?Interceptor?

面試官:okhttp是如何實現(xiàn)連接池復(fù)用的?

OkHttp的底層是通過Java的Socket發(fā)送HTTP請求與接受響應(yīng)的(這也好理解葫录,HTTP就是基于TCP協(xié)議的)着裹,但是OkHttp實現(xiàn)了連接池的概念,

KeepAlive

當(dāng)然大量的連接每次連接關(guān)閉都要三次握手四次分手的很顯然會造成性能低下,因此http有一種叫做keepalive connections的機(jī)制骇扇,它可以在傳輸數(shù)據(jù)后仍然保持連接摔竿,當(dāng)客戶端需要再次獲取數(shù)據(jù)時,直接使用剛剛空閑下來的連接而不需要再次握手

我們可以Reuqst的header中將Connection設(shè)置為keepalive來復(fù)用連接少孝。

什么時候用到鏈接池復(fù)用继低?

同一端口和同一域名。socket復(fù)用稍走。減少3次握手袁翁,4次揮手。連接池源碼分析婿脸。

即對于同一主機(jī)的多個請求粱胜,其實可以公用一個Socket連接,而不是每次發(fā)送完HTTP請求就關(guān)閉底層的Socket狐树,這樣就實現(xiàn)了連接池的概念焙压。而OkHttp對Socket的讀寫操作使用的OkIo庫進(jìn)行了一層封裝。

2 okhttp連接池復(fù)用是具體的一個連接還是同一個域名下的連接都可以復(fù)用抑钟?

是同一個域名下連接都可以復(fù)用涯曲,服務(wù)器和PC瀏覽器同一個域名下只能建立6個TCP 連接,為了讓同一個網(wǎng)頁中的圖片快速加載在塔,所以要把圖片放到不同的域名下幻件,這樣就可以實現(xiàn)>6個的連接請求。

源碼中同一域名下默認(rèn)是5個TCP接連蛔溃,超過后會等待(這個是分發(fā)器要求的5)不同域名下最多64個請求,但是大部分時候同一個域名比較多

會從很多常用的連接問題中自動恢復(fù)傲武。如果您的服務(wù)器配置了多個IP地址,當(dāng)?shù)谝粋€IP連接失敗的時候城榛,OkHttp會自動嘗試下一個IP,此外OkHttp還處理了代理服務(wù)器問題和SSL握手失敗問題态兴。

如上代碼設(shè)置相當(dāng)于沒有復(fù)用 keep-alive導(dǎo)致每次請求都需要重新進(jìn)行 DNS解析狠持,3次握手4次揮手操作,這樣是非常浪費(fèi)性能的瞻润,源碼中默認(rèn)的是5個空閑TCP接連喘垂,并且活躍時間為5分鐘。

通一個請求地址绍撞?

Okhttp支持5個并發(fā)KeepAlive正勒,默認(rèn)鏈路生命為5分鐘(鏈路空閑后,保持存活的時間)傻铣。

創(chuàng)建連接池實在OkHttpClient初始化的時候章贞,ConnectinoPool通過直接new出的

public ConnectionPool() {

this(5, 5L, TimeUnit.MINUTES);

}

public final class ConnectionPool {

private static final Executor executor;

? ? private final int maxIdleConnections;

? ? private final long keepAliveDurationNs;

? ? private final Runnable cleanupRunnable;

? ? private final Deque connections;

? ? final RouteDatabase routeDatabase;

? ? boolean cleanupRunning;

? ? public ConnectionPool() {

this(5, 5L, TimeUnit.MINUTES);

? ? }

RealConnection get(Address address, StreamAllocation streamAllocation) {

assert Thread.holdsLock(this);

? ? Iterator var3 =this.connections.iterator();

? ? RealConnection connection;

? ? do {

if (!var3.hasNext()) {

return null;

? ? ? ? }

connection = (RealConnection)var3.next();

? ? }while(connection.allocations.size() >= connection.allocationLimit || !address.equals(connection.route().address) || connection.noNewStreams);

? ? streamAllocation.acquire(connection);

? ? return connection;

}

主要的變量有必要說明一下:

什么時候鏈接?什么時候獲确侵蕖鸭限?什么時候添加蜕径?什么時候移除

在第四個鏈接攔截器的時候添加和獲取的,還有鏈接的败京。然后移除通過一個線程一直移除

executor線程池兜喻,類似于CachedThreadPool,需要注意的是這種線程池的工作隊列采用了沒有容量的SynchronousQueue

Deque<RealConnection>赡麦,雙向隊列朴皆,雙端隊列同時具有隊列和棧性質(zhì),經(jīng)常在緩存中被使用泛粹,里面維護(hù)了RealConnection也就是socket物理連接的包裝遂铡。

RouteDatabase,它用來記錄連接失敗的Route的黑名單戚扳,當(dāng)連接失敗的時候就會把失敗的線路加進(jìn)去忧便。

? ? ??ConnectionPool:?連接池

static {

executor =new ThreadPoolExecutor(0, 2147483647, 60L, TimeUnit.SECONDS, new SynchronousQueue(), Util.threadFactory("OkHttp ConnectionPool", true));

}

可以看出連接池復(fù)用的核心就是用Deque<RealConnection>來存儲連接

里面的線程池干嘛的?去清理無用鏈接帽借。Executor的線程池是用來清理閑置的連接的

在cleanupRunnable的run方法會不停的調(diào)用cleanup清理并返回下一次清理的時間間隔珠增。然后進(jìn)入wait,等待下一次的清理砍艾。那么cleanup()是怎么計算時間間隔的蒂教?

鏈路的概念

Collection

HttpCodec

StreamAllocation

ConnectionPool

HttpEngine在發(fā)起請求之前,會先調(diào)用nextConnection()來獲取一個Connection對象脆荷,如果可以從ConnectionPool中獲取一個Connection對象凝垛,就不會新建,

如果無法獲取蜓谋,就會調(diào)用createnextConnection()來新建一個Connection對象梦皮,

這就是okhttp多路復(fù)用的核心,不像之前的網(wǎng)絡(luò)框架桃焕,無論有沒有剑肯,都會新建Connection對象。

http://www.reibang.com/p/8928f4b128f1

https://www.cnblogs.com/tony-yang-flutter/p/12383267.html

復(fù)用第一要素:ConnectionPool 要點(對鏈接隊列進(jìn)行添加观堂,移除操作)

ConnectionPool提供對Deque<RealConnection>進(jìn)行操作的方法分別為put让网、get、connectionBecameIdle师痕、evictAll幾個操作溃睹。分別對應(yīng)放入連接、獲取連接胰坟、移除連接因篇、移除所有連接操作。

管理http和http/2的鏈接,以便減少網(wǎng)絡(luò)請求延遲惜犀。同一個address將共享同一個connection铛碑。該類實現(xiàn)了復(fù)用連接的目標(biāo)。

ConnectionPool內(nèi)部以隊列方式存儲連接

連接池最多維持5個連接虽界,且每個鏈接最多活5分鐘

每次添加鏈接的時候回執(zhí)行一次清理任務(wù)汽烦,清理空閑的鏈接(RealConnection)。

連接池的清理和回收:我們在put新連接到隊列的時候會先執(zhí)行清理閑置連接的線程

復(fù)用第二要素:RealConnection 要點

RealConnection是socket物理連接的包裝莉御,

真實連接:public final class RealConnectionextends Listenerimplements Connection {

代表著鏈接socket的鏈路撇吞,如果擁有了一個RealConnection就代表了我們已經(jīng)跟服務(wù)器有了一條通信鏈路

實現(xiàn)了三次握手等操作。

Put方法:

void put(RealConnection connection) {

assert (Thread.holdsLock(this));

? ? if (!cleanupRunning) {

cleanupRunning =true;

? ? ? ? executor.execute(cleanupRunnable);

? ? }

connections.add(connection);

}

歷connections緩存列表礁叔,當(dāng)某個連接計數(shù)的次數(shù)小于限制的大小并且request的地址和緩存列表中此連接的地址完全匹配牍颈。則直接復(fù)用緩存列表中的connection作為request的連接。

在RealConnectionPool中維護(hù)了一個線程池琅关,來進(jìn)行回收和復(fù)用煮岁;connections是一個記錄連接的雙端隊列;routeDatabase是記錄路由失敗的線路涣易,cleanupRunnable是用來進(jìn)行自動回收連接的画机。

然后我們重點看一下cleanupRunnable:首先是一個死循環(huán),一直執(zhí)行新症,通過cleanup方法進(jìn)行回收連接步氏,并返回了下次清理的間隔時間(以納米為單位,下次調(diào)用這個方法的時間)徒爹,-1表示不需要再進(jìn)行回收荚醒,則跳出循環(huán),否則一直進(jìn)行回收隆嗅,同時讓連接池RealConnectionPool等待

根據(jù)連接中的引用計數(shù)來計算空閑連接數(shù)和活躍連接數(shù)界阁,然后標(biāo)記出空閑的連接,如果空閑連接keepAlive時間超過5分鐘胖喳,或者空閑連接數(shù)超過5個铺董,則從Deque中移除此連接。接下來根據(jù)空閑連接或者活躍連接來返回下次需要清理的時間數(shù):如果空閑連接大于0則返回此連接即將到期的時間禀晓,如果都是活躍連接并且大于0則返回默認(rèn)的keepAlive時間5分鐘,如果沒有任何連接則跳出循環(huán)并返回-1

Clear方法:

首先會對緩存中的連接進(jìn)行遍歷坝锰,以尋找一個閑置時間最長的連接粹懒,然后根據(jù)該連接的閑置時長和最大允許的連接數(shù)量等參數(shù)來決定是否應(yīng)該清理該連接。同時注意上面的方法的返回值是一個時間顷级,如果閑置時間最長的連接仍然需要一段時間才能被清理的時候凫乖,會返回這段時間的時間差,然后會在這段時間之后再次對連接池進(jìn)行清理。

復(fù)用第三要素:Connection 要點

復(fù)用第四要素:StreamAllocation 要點(計數(shù)對象帽芽。判斷是否是空閑的 )删掀。

如何判斷閑置的連接:主要通過弱引用和計數(shù)的方式

在遍歷緩存列表的過程中

使用連接數(shù)目inUseConnectionCount 和閑置連接數(shù)目idleConnectionCount 的計數(shù)累加值都是通過pruneAndGetAllocationCount()?是否大于0來控制的。

那么很顯然pruneAndGetAllocationCount() 方法就是用來識別對應(yīng)連接是否閑置的导街。>0則不閑置披泪。否則就是閑置的連接。

在RealConnection中保存了StreamAllocationd的一個列表搬瑰,作為連接上流的計數(shù)器款票。如果列表大小為0,表示連接是空閑的泽论,可以回收艾少;否則連接還在用,不能關(guān)閉翼悴。

對StreamAllocation使用了弱引用包裝缚够。只要弱引用還存在,說明連接還在用鹦赎。

總結(jié):清理閑置連接的核心主要是引用計數(shù)器List<Reference<StreamAllocation>>?和?選擇排序的算法以及excutor的清理線程池谍椅。

一個鏈接對應(yīng)多個流,鏈接和流的對應(yīng)關(guān)系通過StreamAllocation來記錄钙姊。

判斷connection.allocations列表中能否有StreamAllocation毯辅,假如沒有就是空閑連接,否則不是煞额。

它里面維護(hù)了List<Reference<StreamAllocation>>的引用思恐。List中StreamAllocation的數(shù)量也就是socket被引用的計數(shù),如果計數(shù)為0的話膊毁,說明此連接沒有被使用就是空閑的胀莹,需要通過下文的算法實現(xiàn)回收;如果計數(shù)不為0婚温,則表示上層代碼仍然引用描焰,就不需要關(guān)閉連接。

在okhttp中栅螟,在高層代碼的調(diào)用中荆秦,使用了類似于引用計數(shù)的方式跟蹤Socket流的調(diào)用,這里的計數(shù)對象是StreamAllocation力图,它被反復(fù)執(zhí)行aquire與release操作步绸,這兩個函數(shù)其實是在改變RealConnection中的List<Reference<StreamAllocation>>?的大小。(StreamAllocation.Java)吃媒。

List中StreamAllocation的數(shù)量也就是socket被引用的計數(shù)

public final class StreamAllocation {

? public void acquire(RealConnection connection) {

? ? connection.allocations.add(new WeakReference<>(this));

? }

? private void release(RealConnection connection) {

? ? for (int i = 0, size = connection.allocations.size(); i < size; i++) {

? ? ? Reference<StreamAllocation> reference = connection.allocations.get(i);

? ? ? if (reference.get() == this) {

? ? ? ? connection.allocations.remove(i);

? ? ? ? return;

? ? ? }

? ? }

? ? throw new IllegalStateException();

? }

復(fù)用第五要素:RouteDatabase瓤介,它用來記錄連接失敗的Route的黑名單吕喘,當(dāng)連接失敗的時候就會把失敗的線路加進(jìn)去

面試官:okhttp是如何保證通信安全的?

面試官:okhttp如何方法https里面怎么處理SSL刑桑?

客戶端默認(rèn)信任全部證書

X509TrustManager

SSLSocketFactory

客戶端默認(rèn)信任全部證書

面試官:OKhttp針對網(wǎng)絡(luò)層有哪些優(yōu)化氯质?

另外一個博客

面試官:okhttp,在訪問一個界面沒有結(jié)束祠斧,關(guān)閉activity闻察。還是會,這個要驗證一下梁肿,那么下一次進(jìn)去重新訪問接口還是--------

網(wǎng)絡(luò)請求如何取消蜓陌?底層是怎么實現(xiàn)的

還是會執(zhí)行

面試官:如何修改服務(wù)器返回的數(shù)據(jù)類型修改

Response的body().string()方法獲取返回回來的json數(shù)據(jù)(也可以是其他類型的數(shù)據(jù)(XML類型) 這個需要和服務(wù)器端商量好)

面試官:delay和delayqueue

面試官:OkHttp中為什么使用構(gòu)建者模式?

使用多個簡單的對象一步一步構(gòu)建成一個復(fù)雜的對象吩蔑;

優(yōu)點:?當(dāng)內(nèi)部數(shù)據(jù)過于復(fù)雜的時候钮热,可以非常方便的構(gòu)建出我們想要的對象,并且不是所有的參數(shù)我們都需要進(jìn)行傳遞烛芬;

缺點:?代碼會有冗余

好封裝

面試官:OkHttp中模板方法設(shè)計模式是怎樣的隧期?

面試官:?okhttp的缺點?

就是責(zé)任鏈模式的缺點:當(dāng)然它也有缺點:因為調(diào)用鏈路長赘娄,而且存在嵌套仆潮,遇到問題排查其它比較麻煩。

面試官:okio是用來做什么的遣臼?

寫流

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末性置,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子揍堰,更是在濱河造成了極大的恐慌鹏浅,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件屏歹,死亡現(xiàn)場離奇詭異隐砸,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)蝙眶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門季希,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人幽纷,你說我怎么就攤上這事式塌。” “怎么了友浸?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵峰尝,是天一觀的道長。 經(jīng)常有香客問我尾菇,道長境析,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任派诬,我火速辦了婚禮劳淆,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘默赂。我一直安慰自己沛鸵,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布缆八。 她就那樣靜靜地躺著曲掰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪奈辰。 梳的紋絲不亂的頭發(fā)上栏妖,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機(jī)與錄音奖恰,去河邊找鬼吊趾。 笑死,一個胖子當(dāng)著我的面吹牛瑟啃,可吹牛的內(nèi)容都是我干的论泛。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蛹屿,長吁一口氣:“原來是場噩夢啊……” “哼屁奏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起错负,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤坟瓢,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后湿颅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體载绿,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年油航,在試婚紗的時候發(fā)現(xiàn)自己被綠了崭庸。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡谊囚,死狀恐怖怕享,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情镰踏,我是刑警寧澤函筋,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站奠伪,受9級特大地震影響跌帐,放射性物質(zhì)發(fā)生泄漏首懈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一谨敛、第九天 我趴在偏房一處隱蔽的房頂上張望究履。 院中可真熱鬧,春花似錦脸狸、人聲如沸最仑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泥彤。三九已至,卻和暖如春卿啡,著一層夾襖步出監(jiān)牢的瞬間吟吝,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工牵囤, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留爸黄,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓揭鳞,卻偏偏與公主長得像炕贵,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子野崇,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容