本文首發(fā)于 ——何時夕,搬運轉(zhuǎn)載請注明出處,否則將追究版權(quán)責(zé)任。交流qq群:859640274
連載文章
- 1.從零開始仿寫一個抖音app——開始
- 4.從零開始仿寫一個抖音App——日志和埋點以及后端初步架構(gòu)
- 6.從零開始仿寫一個抖音App——音視頻開篇
- 7.從零開始仿寫一個抖音App——基于FFmpeg的極簡視頻播放器
本項目的 github 地址:MyTikTok
國慶快結(jié)束了,國慶中有六天都在寫文章看代碼還有比我苦逼的嗎(買個慘,哈哈)。這幾天為項目新增了五個模塊,順便看了看 kotlin 的語法并在項目中簡單的實踐了一下。本文中會講解其中的兩個模塊,剩下的一些會在不久后發(fā)布的下一篇文章中進行講解。
- 1.討論——總結(jié)前兩周評論中有意義的討論并給予我的解答
- 2.app架構(gòu)更新——隨著開發(fā)的進行,發(fā)現(xiàn)第二篇文章中的架構(gòu)有一些問題,所以在這里更新一下
- 3.網(wǎng)絡(luò)層定制——基于 retrofit 和 okhttp3 定制一個網(wǎng)絡(luò)請求層,中間會附加一些原理講解
一、討論
討論1:zsh 對 bash 的支持并不是完全的,如果運行純 bash 有時候會出問題建議不要在服務(wù)器上用。
- 1.這個讀者的建議非常好,上篇文章中我寫了一個 unbunt 環(huán)境的初始化腳本,看來這個腳本只能自己在 linux 下開發(fā)的時候使用了
討論2:我以為 aop 是通過 aspectjrt 來實現(xiàn)的原來是和 Butterknife 類似來實現(xiàn)的
- 1.在我認知里面的 aop 可以簡單的歸納成:通過注解的信息在某些方法的前后添加代碼。
- 2.所以 aspectj 也是可以實現(xiàn)我在前篇文章中說的 aop 日志的。
- 3.如果讀者了解 aspectj 的原理的話就會發(fā)現(xiàn):他也是通過 gradle 插件來將代碼插到注解的方法前后的,只不過這一部分不需要開發(fā)者來是實現(xiàn)。
- 4.而項目中自己實現(xiàn)一個這樣的東西一個是為了可定制性,另一個就是為了能了解一些技術(shù)的原理而不是單單只會用。
討論3:建議以已完成某個功能模塊或者某篇文章為版本,創(chuàng)建不同的tag,這樣利于食用。(github 上面的 issue)
- 1.這個讀者的建議也非常好,我已經(jīng)在每次更新文章的 commit 上面加上了 tag ,大家可以結(jié)合這個來看代碼。
二、app架構(gòu)更新
我想看過本系列第二篇文章的同學(xué)都看過本項目的模塊架構(gòu)圖。距離寫下本項目的第一行代碼到現(xiàn)在已經(jīng)差不多三個月過去了,這個過程中項目中增加了很多模塊,我對大的項目的把握程度也加深了許多,所以這一節(jié)我更新一下 app 的架構(gòu)。
圖1:app 架構(gòu)圖.png
我接下來就按照圖1開始講解,標(biāo)了紅色的小模塊表示已經(jīng)進行過開發(fā)的模塊
- 1.首先從最底層開始,這里是一些二方庫(自己開發(fā)的sdk)、三方庫(開源的sdk)。其他的所有模塊都能依賴這里的庫,當(dāng)然都是單向依賴(A 依賴 B,但是 B 不能依賴 A)
- 2.再向上一層,這里有兩個大模塊,generate-code 和 internal-base。
- 1.generate-code:這里放著生成代碼的幾個模塊,比如用 apt 生成代碼的 annotation-progress,又如用 gradle 的 transform 配合 javassist 生成代碼的 invoker。
- 2.internal-base:這里放著 app 中的所有的底層模塊,例如負責(zé)網(wǎng)絡(luò)請求的 http 模塊,例如負責(zé)圖片加載的 image 模塊,例如復(fù)制數(shù)據(jù)庫的 database 模塊等等。
- 3.在這里 generate-code 與 internal-base 這兩個大模塊之間可以互相依賴(注意這里表示的不是類似 http 與 image 之間可以互相依賴,因為這樣會產(chǎn)生循環(huán)依賴的錯誤)
- 4.這兩個大模塊都可以被更上層的大模塊所依賴,注意這里是單向依賴,是必須遵守的約定,因為沒有代碼層面的約束
- 3.再向上看,左邊是一個 external-base 大模塊和一個 core 小模塊組成的
- 1.external-base:這個大模塊里面目前還沒有添加小模塊,但是未來應(yīng)該會添加進去,這里面裝著的是外部侵入的代碼的封裝,比如 bugly 除了需要添加庫的依賴還需要為其加一些另外的代碼,又比如一些 android 廠商的 push 方案集成之后需要的適配代碼
- 2.core:這個是一個小模塊,將其單獨放在外面是因為其起一個承上啟下的作用
- 1.這里面裝著更上層模塊的公共代碼,比如 app 進入時的初始化代碼。
- 2.解決一些底層小模塊之間需要互相引用的問題,比如 http 需要和 image 之間互相引用,此時會造成循環(huán)引用的錯誤,此時就將這些代碼放到 core 中進行處理。暫定,在寫下面的時候我發(fā)現(xiàn)這個特性可能會造成本模塊依賴過多的問題,后面應(yīng)該還會繼續(xù)拆分這個小模塊
- 3.溝通底層和上層的模塊
- 3.這里的兩個模塊可以被同層右邊的 app-plugins 大模塊所依賴,這里也是單向依賴
- 4.再看同層的右邊,這一個大模塊名為 app-plugins,里面的每一個小模塊都能被編譯成一個 app。然后其可以被最頂層的 app-variants 所依賴,最終構(gòu)建出不同功能的 app。
- 5.最頂層就是 app-variants,這個大模塊只能依賴 app-plugins,里面幾乎不會有什么代碼,有的就是一個個 gradle 配置,最終會生成不同功能的 app。
三、網(wǎng)絡(luò)層定制
現(xiàn)在 okhttp + retrofit,也許是一個新項目的標(biāo)配了,但是很多人都只是在使用這兩個庫的最基本的功能,殊不知這兩個庫可以通過定制來實現(xiàn)更多的功能。這一節(jié)我就來講講如何基于這兩個庫來定制一個大項目的網(wǎng)絡(luò)請求層。中間會穿插著一些原理的講解。
1.網(wǎng)絡(luò)層請求流程
圖2:網(wǎng)絡(luò)層定制圖.png
接下來我會按照圖2開始講解 okhttp + retrofit 整個請求流程,待讀者對整個流程有所了解之后再講定制的代碼,這樣會事半功倍。
- 1.圖中紅色的框是開始部分,我們就從這里開始。這里默認大家都會使用這兩個框架,多余的東西就不再贅述了。
- 2.首先我們在需要請求一個接口的時候會使用 Retrofit 對象調(diào)用其 create 方法創(chuàng)建一個 XXXService。我們看下圖3的代碼:
- 1.可以看見這里就是簡單的用了一下動態(tài)代理的方式將 XXXService 的每個接口交給特定的 ServiceMethod 來實現(xiàn)。
- 2.這里的 ServiceMethod 怎么來的呢?看36行的 loadServiceMethod 方法,這首先為了性能會去 serviceMethodCache 中看看是否有 XXXService 某個接口對應(yīng)的 ServiceMethod,如果沒有的話就用 Builder 模式創(chuàng)建一個。
圖3:Retrofit#create.png
- 3.回看圖2,創(chuàng)建好了 XXXService 的實現(xiàn)類之后,我們一般會結(jié)合 Rxjava 調(diào)用某個接口,讓其返回一個 Observable 對象。由前面的介紹,我們知道這里 Observable 其實是調(diào)用 ServiceMethod.adapt(OkhttpCall) 返回的(可以看圖3的21行),我們進入這個方法。
- 1.這個方法里會將調(diào)用交給 CallAdapter.adapt(OkhttpCall)
- 2.有些同學(xué)可能知道這個 CallAdapter 是在初始化 Retrofit 的時候被Retrofit.Builder() 添加的 CallAdapterFactory 創(chuàng)建的。其有幾個具體實現(xiàn)如圖2。
- 3.那么這里要選哪一個呢?選擇 CallAdapter 的具體邏輯在 ServiceMethod.build 里面他會調(diào)用 ServiceMethod.createCallAdapter 這里最終會交給 Retrofit.callAdapter 來尋找合適的 CallAdapter。
- 4.那么3中的具體查找邏輯是什么呢?這里我總結(jié)一下:
- 1.會對 CallAdapterFactory 進行循環(huán)查找,一旦返回一個 CallAdapter 不為 null 那么就使用這個。
- 2.具體是否為 null 的邏輯交給具體的 CallAdapterFactory 去實現(xiàn)。
- 3.因為是順序查找,所以如果列表中有多個匹配項,這里只取最開始的一個。
- 4.到這里我們先不看圖2,一般來說匹配上的 CallAdapterFactory 會是 RxJava2CallAdapterFactory。我們先研究一下他是怎么產(chǎn)生一個 Observable 的。
- 1.先看一下圖4,我們直接看20行,這里解釋了為什么一般會匹配到 RxJava2CallAdapterFactory 因為我們的 XXXService 定義接口的時候一般選擇的返回值 都是 Observable 或者有關(guān) Rxjava 的返回值。然后我們直接看55行,這里返回了一個 RxJava2CallAdapter,這個就是生成 Observable 的對象。
- 2.接著我們看圖5,還記得上面的3.1中我們說的嗎?Observable 就是 CallAdapter.adapt(OkhttpCall) 產(chǎn)生的。這里就是具體實現(xiàn)。
- 1.可以看見18行根據(jù)接口調(diào)用是同步還是異步會生成兩種不同的 Observable。
- 2.然后后面都是根據(jù)一些 flag,為 Observable 添加一些操作符。
圖4:RxJava2CallAdapterFactory#get.png
圖5:RxJava2CallAdapter#adapt.png
- 5.再回到圖2,現(xiàn)在我們已經(jīng)有 Observable 了。這里我們先跳過圖2中的幾個步驟,直接來到黃色的框,從這里開始我們可以讓得到的 Observable 開始運行。對 Rxjava 熟悉的同學(xué)應(yīng)該知道,一個 Observable 會從操作符流的最頂部開始運行。所以這里會從我們前面講到的 RxJava2CallAdapter.adapt 中定義的第一個 Observable 的 subscribe 開始運行。我們就默認這次接口調(diào)用是同步的這樣簡單點,所以會先進入 CallExecuteObservable 中。
- 1.先看圖6,第1行構(gòu)造這個對象的時候會傳入一個 Call 對象,其實現(xiàn)有很多我們在這里可以默認其為 OkhttpCall。
- 2.圖6的第5行,是 Observable 開始運行的時候最先調(diào)用的方法(有興趣的同學(xué)可以看看Rxjava 的源碼解析)。這里我們可以看見13行,其將調(diào)用交給了 Okhttp.execute。
- 3.我們可以看向圖7的20行,這里調(diào)用了 createRawCall 創(chuàng)建了一個 okhttp3.Call 其具體實現(xiàn)是 RealCall(我們直接使用 okhttp 的時候也是通過這個請求網(wǎng)絡(luò))。
- 4.在回到圖2中,如圖2所示當(dāng)調(diào)用 RealCall.execute 的時候,就會進入 okhttp 的請求鏈。okhttp 使用了責(zé)任鏈模式,將請求穿過圖2中的一個個攔截器,每個攔截器都負責(zé)一個功能。開發(fā)者可以在攔截器鏈的最開始插入自己的攔截器,以實現(xiàn)一些定制操作。
- 5.再回到圖7,okhttp 將數(shù)據(jù)請求完畢之后會返回一個 okhttp3.Response,這時候會在32行調(diào)用43行的 parseResponse 來將解析這個 Response。
- 6.圖7中后面有些代碼看不見了,其實最終 Response 的解析會交給 ServiceMethod.toResponse。而其又會交給 Converter.coverter。這接口的實現(xiàn)類也很多,最常見的應(yīng)該就是 GsonConverterFactory 提供的 GsonResponseBodyConverter 了。如圖2,我們一般也是在創(chuàng)建 Retrofit 的時候添加一些 Converter 以供這里使用。同樣類似 CallAdapter,Converter 的選取也是一樣的策略
- 7.經(jīng)過以上調(diào)用,我們就有了一個retrofit2.Respons,其內(nèi)部有一個解析了 body 之后的對象。
圖6:CallExecuteObservable#subscribeActual.png
圖7:OkHttpCall#execute.png
- 6.CallExecuteObservable 中調(diào)用完畢之后,調(diào)用流程一般會交給 BodyObservable,這里面很簡單,就是將 retrofit2.Respons 中的解析后的 body 交給下一個 Observable 操作符。就這樣順著操作符流最終我們在 XXXService 中定義的接口的返回值 Observable 的泛型對象就會被傳入到 subscribe 中供外部調(diào)用者使用。如圖2中的粉色框。
2.網(wǎng)絡(luò)層定制代碼
所謂定制就是在網(wǎng)絡(luò)請求流程的各個主要節(jié)點中添加自己的代碼實現(xiàn)以達到特殊的需求。經(jīng)過前面的講解,我想讀者應(yīng)該對整個網(wǎng)絡(luò)層的請求流程有了一個大致的了解。這時我們可以再看看圖2,可以看見其中有幾處我綠色的框,這幾個地方就是我們可以添加定制代碼的地方。接下來我就會按順序講解一下這幾處的定制代碼是如何實現(xiàn)的。
圖8:RetrofitFactory.png
圖9:DefaultRetrofitConfig.png
(1)retrofit2.Call的裝飾
我們按請求順序可以在圖2中首先看見的是 NewCall.execute 這個框,接下來我就來說說這個可以怎么定制。
- 1.按照我們前面的講解,大家應(yīng)該知道,如果不做任何定制的話這里的 NewCall 就是 OkhttpCall,其會返回一個 retrofit.Response。最終會在開發(fā)者的 subscribe 里面返回一個解析了 body 之后的數(shù)據(jù)結(jié)構(gòu)(這里就稱為 ContentData)。有時候我們會在 subscribe 里面需要更多的信息,比如在數(shù)據(jù)轉(zhuǎn)化過程中丟失的 head 的信息。
- 2.此時我們就可以對 OkhttpCall 進行一個封裝,首先我們可以定義一個我們自己的 DataContainer<T> 對象,其用于封裝 ContentData,然后其還可以裝數(shù)據(jù)轉(zhuǎn)化中丟失的數(shù)據(jù)。如圖10。
圖10:DataContainer.png
- 3.那么我們在定義 XXXService 的接口的返回值的時候就能這樣定義:Observable<DataContainer<ContentData>>。
- 4.此時有人眼尖就會發(fā)現(xiàn),不對啊這個 DataContainer 是被 Gson 反序列化過來的,里面的 okhttp3.Response 對象服務(wù)器又不知道是什么這樣怎么序列化呢?
- 5.答案就在圖8,圖9中。大家可以看圖8的第7行,這里我添加了一個自定義的 CallAdapterFactory。
- 6.在看圖8的44、48、49行,根據(jù)前面我們描述的請求流程,44行的 CallAdapter 會用來生成 Observable。再看48行,這里的 call 就是 OkhttpCall 了。我們將其傳入 buildCall 中返回了一個 NewCall,這里就是關(guān)鍵。
- 7.buildCall 的實現(xiàn)代碼在圖9,可以看38行。這里的實現(xiàn)非常簡單直接就是將 OkhttpCall 封裝 返回了一個 ContainerCall,如圖11。
圖11:DataContainerCall.png
- 8.DataContainerCall 里面的代碼就不用我說了吧,就是給 DataContainer 傳入一個 okhttp3.Response 對象。
- 9.大家是不是覺得就這樣一個小東西很簡單?其實我也覺得很簡單,但是只要你會用了這一個小東西,那么更多實用的功能都能被這樣實現(xiàn)。
(2)OkhttpClient定制
按順序下來,第二個定制的地方就是 OkhttpCall 調(diào)用 okhttp.RealCall 的地方了。
- 1.我們看圖8的21行,這里給 Retrofit 添加了一個 OkhttpClient。之后的請求都是通過它來發(fā)送的。
- 2.這里插一下,大家可以看看3行,這里傳的是一個 RetrofitConfig,它其實是一個接口,像圖9的 DefaultRetrofitConfig 就是它的一個實現(xiàn)。當(dāng)然我們還可以有不同的實現(xiàn)以實現(xiàn)不同的定制方式。
- 3.那么我們還是再看圖9的6行,可以看見這個方法的返回值 Builder 中添加了一系列 Intercept。由我們前面的講解可知,這些是攔截器,然后會按添加的順序攔截請求和響應(yīng)。
- 4.這里可以看見我實現(xiàn)了各種不同的功能:打印網(wǎng)絡(luò)請求日志(這個在上一篇文章中沒實現(xiàn),現(xiàn)在實現(xiàn)了)、過濾過于頻繁的請求(防止ddos攻擊)、SSL認證(當(dāng)然現(xiàn)在沒有后端還沒實現(xiàn))、超時攔截、添加自定義的參數(shù)等等。
- 5.這里的定制比較簡單,大家可以去看看各個攔截器中的實現(xiàn)。
(3)Converter定制
- 1.其實這個也很簡單,大家可能都用過,就是圖8的5、6兩行,添加的數(shù)據(jù)轉(zhuǎn)換器。
- 2.大家只要了解我前面講解的 Converter 的執(zhí)行策略就可以了。
(4)CallAdapter定制
- 1.大家可以回看 (1)retrofit2.Call的裝飾 這一節(jié),我們添加了一個 CustomAdapterFactory。
- 2.因為 CustomAdapterFactory 比 RxJava2CallAdapterFactory 先添加,所以其優(yōu)先級比較高。再看圖8的40行,這里獲取了一個 delegate,其實就是 RxJava2CallAdapterFactory。所以我們可以在 RxJava2CallAdapter 返回的 Observable 上面添加一些統(tǒng)一的操作符。
- 3.具體的代碼在圖8的49行,然后轉(zhuǎn)到圖9的42行??梢钥匆娢揖椭惶砑恿艘恍┖唵蔚牟僮鞣河嫈?shù)請求成功和失敗次數(shù)、配合 ThrottlingInterceptor 進行頻繁請求過濾。
(5)網(wǎng)絡(luò)層定制代碼總結(jié)
上面就是在網(wǎng)絡(luò)請求的四個主要節(jié)點進行定制的方式。其實總結(jié)起來比較簡單:1是擴展 Retrofit 返回的結(jié)果、2是擴展 okhttp 請求和返回、3是解析 okhttp 返回給 Retrofit 的結(jié)果、4是增強對 Retrofit 返回結(jié)果的處理。
四、總結(jié)
不知不覺已經(jīng)寫了這么多了,本來以為還可以寫一節(jié) Fresco 的定制,現(xiàn)在看來只能放在下一篇文章了。在這里預(yù)告一下:從零開始仿寫一個抖音App這一系列的文章大概還有一到兩篇 android 層面的文章,并且會在接下來的一周左右放出。
這一階段結(jié)束之后我的文章和學(xué)習(xí)重心將會轉(zhuǎn)向音視頻這塊。這幾個月過來雖然有時候文章會 delay,但最終我也信守承諾沒有棄坑。最后希望大家能持續(xù)關(guān)注本系列,畢竟我都已經(jīng)這么努力了不是:)。
本頁內(nèi)容由塔燈網(wǎng)絡(luò)科技有限公司通過網(wǎng)絡(luò)收集編輯所得,所有資料僅供用戶學(xué)習(xí)參考,本站不擁有所有權(quán),如您認為本網(wǎng)頁中由涉嫌抄襲的內(nèi)容,請及時與我們聯(lián)系,并提供相關(guān)證據(jù),工作人員會在5工作日內(nèi)聯(lián)系您,一經(jīng)查實,本站立刻刪除侵權(quán)內(nèi)容。本文鏈接:http://jstctz.cn/16145.html