Android 網(wǎng)絡(luò)優(yōu)化方案
轉(zhuǎn)自:掘金-究極逮蝦戶
https://juejin.cn/post/6896302142542315533
面試官:小蝦啊,我好想你啊,你都好久沒(méi)來(lái)找我面試了呀。
小蝦:emmmmmmm,這不是怕被你打擊嗎。
面試官:ok,看來(lái)是有備而來(lái),那么我們今天聊聊網(wǎng)絡(luò)優(yōu)化咋做吧。
小蝦:我大意了,沒(méi)有閃。老頭子,你不講武德,我奉勸你耗子尾汁。

如何優(yōu)化一個(gè)網(wǎng)絡(luò)請(qǐng)求呢?
相信大家在面試的時(shí)候可能會(huì)被問(wèn)到這個(gè)問(wèn)題。今天我其實(shí)就是講述下我知道的一些簡(jiǎn)單的優(yōu)化方式,可以幫助大家在面試的過(guò)程中得到點(diǎn)基礎(chǔ)分?jǐn)?shù)。
我們先從最簡(jiǎn)單,大家比較容易了解到的講起。
DNS優(yōu)化
DNS則是典型的應(yīng)用層的協(xié)議了,至于說(shuō)為什么第二層能查第三層的IP,因?yàn)镈NS是Domain Name System縮寫(xiě),所以你認(rèn)為是服務(wù)是協(xié)1653議都可以。
一個(gè)Http請(qǐng)求在建立Tcp連接的過(guò)程中,肯定會(huì)產(chǎn)生一次DNS,那么我們是不是可以通過(guò)內(nèi)存緩存的方式,通過(guò)一個(gè)HashMap持有這個(gè)Host的IP,當(dāng)下次發(fā)起Tcp連接的時(shí)候,我們就可以用直接用內(nèi)存中的這個(gè)Ip,而不需要再去走一遍Dns服務(wù)了。
這個(gè)時(shí)候你肯定會(huì)問(wèn)我,臥槽,你這個(gè)不是搞我嗎,這可怎么改呀?
如果你的網(wǎng)絡(luò)層用的是OkHttp的話,Okhttp在封裝的時(shí)候就已經(jīng)考慮到這個(gè)部分了,其內(nèi)部提供了Dns的接口,可以讓外部在構(gòu)造Client的時(shí)候傳入。
class HttpDns : Dns {private val cacheHost = hashMapOf() override fun lookup(hostname: String): MutableList{ if (cacheHost.containsKey(hostname)) {cacheHost[hostname]?.apply {return mutableListOf(this)}}return try {InetAddress.getAllByName(hostname)?.first()?.apply {cacheHost[hostname] = this}mutableListOf(*InetAddress.getAllByName(hostname))} catch (e: NullPointerException) {val unknownHostException =UnknownHostException("Broken system behaviour for dns lookup of $hostname")unknownHostException.initCause(e)throw unknownHostException}}}
這里可以稍微給大家展開(kāi)下,LocalDns是不可以被信任的,經(jīng)常會(huì)有運(yùn)營(yíng)商會(huì)搞一些奇奇怪怪的Dns攔截,導(dǎo)致大家收到的請(qǐng)求是運(yùn)營(yíng)商所緩存的(目的是為了省流量),所以阿里騰訊等都有自己對(duì)外輸出的HttpDns的服務(wù)。這個(gè)服務(wù)可以幫助大家找到真實(shí)準(zhǔn)確的Host的Ip,就是這個(gè)服務(wù)是收錢(qián)的。
如果你是個(gè)IOS開(kāi)發(fā)人員,那么你一定要注意SNI(Server Name Indication),一個(gè)IP對(duì)應(yīng)多個(gè)多個(gè)Https證書(shū)的問(wèn)題。
CacheControl
Http請(qǐng)求在1.1階段就引入了CacheControl了,通過(guò)CacheControl可以讓后端直接控制請(qǐng)求內(nèi)容的緩存策略。所以還有比緩存更簡(jiǎn)單粗暴的網(wǎng)絡(luò)優(yōu)化方式嗎?
在http中,控制緩存開(kāi)關(guān)的字段有兩個(gè):
Pragma?和?Cache-Control。
通過(guò)圖片簡(jiǎn)單的介紹下一些緩存參數(shù)。


OkHttp攔截器的方式給網(wǎng)絡(luò)請(qǐng)求添加一個(gè)統(tǒng)一的CacheControl,當(dāng)然如果你有定制化的需求肯定還是要自己開(kāi)發(fā)的,我這里只負(fù)責(zé)科普下這個(gè)面試可以回答的地方,細(xì)節(jié)大家可以參考下這個(gè)倉(cāng)庫(kù)。HTTP協(xié)議規(guī)格說(shuō)明定義ETag為“被請(qǐng)求變量的實(shí)體值”。另一種說(shuō)法是,ETag是一個(gè)可以與Web資源關(guān)聯(lián)的記號(hào)(token)。典型的Web資源可以一個(gè)Web頁(yè),但也可能是JSON或XML文檔。服務(wù)器單獨(dú)負(fù)責(zé)判斷記號(hào)是什么及其含義,并在HTTP響應(yīng)頭中將其傳送到客戶端,以下是服務(wù)器端返回的格式:ETag:"50b1c1d4f775c61:df3"客戶端的查詢(xún)更新格式是這樣的:If-None-Match : W / "50b1c1d4f775c61:df3"如果ETag沒(méi)改變,則返回狀態(tài)304然后不返回,這也和Last-Modified一樣。測(cè)試Etag主要在斷點(diǎn)下載時(shí)比較有用。
而我們只要使用了CacheControl,就可以用到ETag, 如果當(dāng)數(shù)據(jù)內(nèi)容沒(méi)有發(fā)生變更的情況下,就不會(huì)傳輸數(shù)據(jù),這樣也可以給大家略微優(yōu)化下你們的Api請(qǐng)求。
Http 1.0 - 1.1 - 1.X - 2.0
長(zhǎng)連接,HTTP 1.1支持長(zhǎng)連接(PersistentConnection)和請(qǐng)求的流水線(Pipelining)處理,在一個(gè)TCP連接上可以傳送多個(gè)HTTP請(qǐng)求和響應(yīng),減少了建立和關(guān)閉連接的消耗和延遲,在HTTP1.1中默認(rèn)開(kāi)啟Connection:keep-alive,一定程度上彌補(bǔ)了HTTP1.0每次請(qǐng)求都要?jiǎng)?chuàng)建連接的缺點(diǎn)。 header壓縮,如上文中所言,對(duì)前面提到過(guò)HTTP1.x的header帶有大量信息,而且每次都要重復(fù)發(fā)送,HTTP2.0使用encoder來(lái)減少需要傳輸?shù)膆eader大小,通訊雙方各自cache一份header fields表,既避免了重復(fù)header的傳輸,又減小了需要傳輸?shù)拇笮 ?/span> 新的二進(jìn)制格式(Binary Format),HTTP1.x的解析是基于文本?;谖谋緟f(xié)議的格式解析存在天然缺陷,文本的表現(xiàn)形式有多樣性,要做到健壯性考慮的場(chǎng)景必然很多,二進(jìn)制則不同,只認(rèn)0和1的組合?;谶@種考慮HTTP2.0的協(xié)議解析決定采用二進(jìn)制格式,實(shí)現(xiàn)方便且健壯。 多路復(fù)用(MultiPlexing),即連接共享,即每一個(gè)request都是是用作連接共享機(jī)制的。一個(gè)request對(duì)應(yīng)一個(gè)id,這樣一個(gè)連接上可以有多個(gè)request,每個(gè)連接的request可以隨機(jī)的混雜在一起,接收方可以根據(jù)request的 id將request再歸屬到各自不同的服務(wù)端請(qǐng)求里面。
HTTP2.0的多路復(fù)用和HTTP1.X中的長(zhǎng)連接復(fù)用有什么區(qū)別?
HTTP/1.* 一次請(qǐng)求-響應(yīng),建立一個(gè)連接,用完關(guān)閉;每一個(gè)請(qǐng)求都要建立一個(gè)連接;
HTTP/1.1 Pipeling解決方式為,若干個(gè)請(qǐng)求排隊(duì)串行化單線程處理,后面的請(qǐng)求等待前面請(qǐng)求的返回才能獲得執(zhí)行機(jī)會(huì),一旦有某請(qǐng)求超時(shí)等,后續(xù)請(qǐng)求只能被阻塞,毫無(wú)辦法,也就是人們常說(shuō)的線頭阻塞;
HTTP/2多個(gè)請(qǐng)求可同時(shí)在一個(gè)連接上并行執(zhí)行。某個(gè)請(qǐng)求任務(wù)耗時(shí)嚴(yán)重,不會(huì)影響到其它連接的正常執(zhí)行;
好了,下面要開(kāi)始真的進(jìn)入牛逼的東西了,前文你肯定以為我是個(gè)大水逼,復(fù)制黏貼。
GRPC( A high-performance, open-source universal RPC framework)
不知道各位有沒(méi)有聽(tīng)說(shuō)過(guò)一個(gè)都市怪談,字節(jié)的網(wǎng)絡(luò)庫(kù)優(yōu)化有多厲害多厲害,網(wǎng)絡(luò)底層采用的是Webview底層的Chromium的網(wǎng)絡(luò)庫(kù),在弱網(wǎng)情況下對(duì)于api的優(yōu)化啥的,巴拉巴拉.....
Cronet是Chromium網(wǎng)絡(luò)引擎對(duì)不同操作系統(tǒng)做的封裝,實(shí)現(xiàn)了移動(dòng)端應(yīng)用層、表示層、會(huì)話層協(xié)議,支持HTTP1/2、SPDY、QUIC、WebSocket、FTP、DNS、TLS等協(xié)議標(biāo)準(zhǔn)。支持Android、IOS、Chrome OS、Fuchsia,部分支持Linux、MacOS、Windows桌面操作系統(tǒng)。實(shí)現(xiàn)了Brotli數(shù)據(jù)壓縮、預(yù)連接、DNS緩存、session復(fù)用等策略?xún)?yōu)化以及TCP fast open等系統(tǒng)優(yōu)化。本文內(nèi)容基于Chromium 75版本。
Chrome的cronet網(wǎng)絡(luò)庫(kù)(順便展開(kāi)下,cronet同時(shí)支持ios,android,前端)。而由于grpc協(xié)議的問(wèn)題,所以傳輸內(nèi)容直接使用的protobuf格式,所以其不僅僅是網(wǎng)絡(luò)層上的優(yōu)化,同時(shí)由于流能直接轉(zhuǎn)化成實(shí)體類(lèi),同時(shí)也減少了可序列化的時(shí)間。protocol buffers 是一種語(yǔ)言無(wú)關(guān)、平臺(tái)無(wú)關(guān)、可擴(kuò)展的序列化結(jié)構(gòu)數(shù)據(jù)的方法,它可用于(數(shù)據(jù))通信協(xié)議、數(shù)據(jù)存儲(chǔ)等。
Protocol Buffers 是一種靈活,高效,自動(dòng)化機(jī)制的結(jié)構(gòu)數(shù)據(jù)序列化方法-可類(lèi)比 XML,但是比 XML 更?。? ~ 10倍)、更快(20 ~ 100倍)、更為簡(jiǎn)單。
你可以定義數(shù)據(jù)的結(jié)構(gòu),然后使用特殊生成的源代碼輕松的在各種數(shù)據(jù)流中使用各種語(yǔ)言進(jìn)行編寫(xiě)和讀取結(jié)構(gòu)數(shù)據(jù)。你甚至可以更新數(shù)據(jù)結(jié)構(gòu),而不破壞由舊數(shù)據(jù)結(jié)構(gòu)編譯的已部署程序。
但是正常的網(wǎng)絡(luò)框架基本都使用了Retrofit+Okhttp,而且大家都已經(jīng)使用的很習(xí)慣了,所以我大膽的猜測(cè),字節(jié)其實(shí)應(yīng)該用OkHttp橋接了cronet。所以這樣基本就能無(wú)縫橋接當(dāng)前已有的網(wǎng)絡(luò)庫(kù)了。
由GRRC升級(jí)QUIC
QUIC(Quick UDP Internet Connection)是谷歌制定的一種基于UDP的低時(shí)延的互聯(lián)網(wǎng)傳輸層協(xié)議。在2016年11月國(guó)際互聯(lián)網(wǎng)工程任務(wù)組(IETF)召開(kāi)了第一次QUIC工作組會(huì)議,受到了業(yè)界的廣泛關(guān)注。這也意味著QUIC開(kāi)始了它的標(biāo)準(zhǔn)化過(guò)程,成為新一代傳輸層協(xié)議
QUIC協(xié)議(Http3.0協(xié)議)本來(lái)就是谷歌寫(xiě)的,所以谷歌的Cronet本身就支持這也是正常的。我其實(shí)之前就特地去查過(guò)OKHttp支持的協(xié)議內(nèi)容,當(dāng)前還是只停留在2.0階段,主要就還是因?yàn)楫?dāng)前的Connection寫(xiě)的太好了,而且需要把Tcp直接更換成Udp,所以遲遲沒(méi)有更新3.0協(xié)議的支持。所以各位如果想從協(xié)議層去做對(duì)應(yīng)的優(yōu)化,那么可能OkHttp帶給大家的應(yīng)該還是無(wú)盡的等待了。
還能干嗎?
其實(shí)優(yōu)化方面我的大概的姿勢(shì)點(diǎn)就這么多了,但是我們可以考慮從監(jiān)控方面的角度去再重新審視這個(gè)話題哦。客戶端請(qǐng)求從發(fā)起到網(wǎng)關(guān)實(shí)際接收到,其實(shí)中間有很復(fù)雜的鏈路,簡(jiǎn)單的說(shuō),OKhttp內(nèi)也走過(guò)了這么多個(gè)攔截器了。但是當(dāng)一個(gè)線上用戶反饋這個(gè)界面怎么刷出來(lái)的這么慢的情況下,我們以后端網(wǎng)關(guān)開(kāi)始作為請(qǐng)求的開(kāi)始節(jié)點(diǎn),就會(huì)出現(xiàn)難以定位真實(shí)問(wèn)題的情況。
基于OkHttp的網(wǎng)絡(luò)監(jiān)控
我們是不是可以考慮把整個(gè)api發(fā)起到結(jié)束進(jìn)行監(jiān)控,從而可以方便線上去監(jiān)控一個(gè)Api真實(shí)的發(fā)起到結(jié)束的狀況呢?我們先簡(jiǎn)單的把一個(gè)請(qǐng)求的節(jié)點(diǎn)拆分下。我要盜圖了。
參考數(shù)據(jù)深入理解OkHttp3:(七)事件(Events)

OKHttp提供的EventListener,我們就可以對(duì)于一個(gè)請(qǐng)求發(fā)起到最后的各個(gè)節(jié)點(diǎn)進(jìn)行監(jiān)控,之后上報(bào)日志數(shù)據(jù),這樣在后續(xù)的撕逼過(guò)程中,其實(shí)就可以做到有理有據(jù),有話可說(shuō),你真的慢了。總結(jié)
這篇文章基本就存粹是為了各位應(yīng)付面試用的,也算是我對(duì)于Android網(wǎng)絡(luò)優(yōu)化的一些簡(jiǎn)單的總結(jié)吧。其實(shí)中間能展開(kāi)的內(nèi)容也還是有的,就是需要各位自己去摸一摸了。
