從零開始仿寫一個抖音App——app架構更新與網路層定製

何時夕發表於2018-10-07

本文首發於微信公眾號——世界上有意思的事,搬運轉載請註明出處,否則將追究版權責任。微訊號:a1018998632,交流qq群:859640274

連載文章

本專案的 github 地址:MyTikTok

國慶快結束了,國慶中有六天都在寫文章看程式碼還有比我苦逼的嗎(買個慘,哈哈)。這幾天為專案新增了五個模組,順便看了看 kotlin 的語法並在專案中簡單的實踐了一下。本文中會講解其中的兩個模組,剩下的一些會在不久後釋出的下一篇文章中進行講解。

  • 1.討論——總結前兩週評論中有意義的討論並給予我的解答
  • 2.app架構更新——隨著開發的進行,發現第二篇文章中的架構有一些問題,所以在這裡更新一下
  • 3.網路層定製——基於 retrofit 和 okhttp3 定製一個網路請求層,中間會附加一些原理講解

一、討論

討論1:zsh 對 bash 的支援並不是完全的,如果執行純 bash 有時候會出問題建議不要在伺服器上用。

  • 1.這個讀者的建議非常好,上篇文章中我寫了一個 unbunt 環境的初始化指令碼,看來這個指令碼只能自己在 linux 下開發的時候使用了

討論2:我以為 aop 是通過 aspectjrt 來實現的 原來是和 Butterknife 類似來實現的

  • 1.在我認知裡面的 aop 可以簡單的歸納成:通過註解的資訊在某些方法的前後新增程式碼。
  • 2.所以 aspectj 也是可以實現我在前篇文章中說的 aop 日誌的。
  • 3.如果讀者瞭解 aspectj 的原理的話就會發現:他也是通過 gradle 外掛來將程式碼插到註解的方法前後的,只不過這一部分不需要開發者來是實現。
  • 4.而專案中自己實現一個這樣的東西一個是為了可定製性,另一個就是為了能瞭解一些技術的原理而不是單單隻會用。

討論3:建議以已完成某個功能模組或者某篇文章為版本,建立不同的tag,這樣利於食用。(github 上面的 issue)

  • 1.這個讀者的建議也非常好,我已經在每次更新文章的 commit 上面加上了 tag ,大家可以結合這個來看程式碼。

二、app架構更新

我想看過本系列第二篇文章的同學都看過本專案的模組架構圖。距離寫下本專案的第一行程式碼到現在已經差不多三個月過去了,這個過程中專案中增加了很多模組,我對大的專案的把握程度也加深了許多,所以這一節我更新一下 app 的架構。

圖1:app 架構圖.png

我接下來就按照圖1開始講解,標了紅色的小模組表示已經進行過開發的模組

  • 1.首先從最底層開始,這裡是一些二方庫(自己開發的sdk)三方庫(開源的sdk)。其他的所有模組都能依賴這裡的庫,當然都是單向依賴(A 依賴 B,但是 B 不能依賴 A)
  • 2.再向上一層,這裡有兩個大模組,generate-codeinternal-base
    • 1.generate-code:這裡放著生成程式碼的幾個模組,比如用 apt 生成程式碼的 annotation-progress,又如用 gradle 的 transform 配合 javassist 生成程式碼的 invoker。
    • 2.internal-base:這裡放著 app 中的所有的底層模組,例如負責網路請求的 http 模組,例如負責圖片載入的 image 模組,例如複製資料庫的 database 模組等等。
    • 3.在這裡 generate-code 與 internal-base 這兩個大模組之間可以互相依賴(注意這裡表示的不是類似 http 與 image 之間可以互相依賴,因為這樣會產生迴圈依賴的錯誤)
    • 4.這兩個大模組都可以被更上層的大模組所依賴,注意這裡是單向依賴,是必須遵守的約定,因為沒有程式碼層面的約束
  • 3.再向上看,左邊是一個 external-base 大模組和一個 core 小模組組成的
    • 1.external-base:這個大模組裡面目前還沒有新增小模組,但是未來應該會新增進去,這裡面裝著的是外部侵入的程式碼的封裝,比如 bugly 除了需要新增庫的依賴還需要為其加一些另外的程式碼,又比如一些 android 廠商的 push 方案整合之後需要的適配程式碼
    • 2.core:這個是一個小模組,將其單獨放在外面是因為其起一個承上啟下的作用
      • 1.這裡面裝著更上層模組的公共程式碼,比如 app 進入時的初始化程式碼。
      • 2.解決一些底層小模組之間需要互相引用的問題,比如 http 需要和 image 之間互相引用,此時會造成迴圈引用的錯誤,此時就將這些程式碼放到 core 中進行處理。暫定,在寫下面的時候我發現這個特性可能會造成本模組依賴過多的問題,後面應該還會繼續拆分這個小模組
      • 3.溝通底層和上層的模組
    • 3.這裡的兩個模組可以被同層右邊的 app-plugins 大模組所依賴,這裡也是單向依賴
  • 4.再看同層的右邊,這一個大模組名為 app-plugins,裡面的每一個小模組都能被編譯成一個 app。然後其可以被最頂層的 app-variants 所依賴,最終構建出不同功能的 app。
  • 5.最頂層就是 app-variants,這個大模組只能依賴 app-plugins,裡面幾乎不會有什麼程式碼,有的就是一個個 gradle 配置,最終會生成不同功能的 app。

三、網路層定製

現在 okhttp + retrofit,也許是一個新專案的標配了,但是很多人都只是在使用這兩個庫的最基本的功能,殊不知這兩個庫可以通過定製來實現更多的功能。這一節我就來講講如何基於這兩個庫來定製一個大專案的網路請求層。中間會穿插著一些原理的講解。

1.網路層請求流程

圖2:網路層定製圖.png

接下來我會按照圖2開始講解 okhttp + retrofit 整個請求流程,待讀者對整個流程有所瞭解之後再講定製的程式碼,這樣會事半功倍。

  • 1.圖中紅色的框是開始部分,我們就從這裡開始。這裡預設大家都會使用這兩個框架,多餘的東西就不再贅述了。
  • 2.首先我們在需要請求一個介面的時候會使用 Retrofit 物件呼叫其 create 方法建立一個 XXXService。我們看下圖3的程式碼:
    • 1.可以看見這裡就是簡單的用了一下動態代理的方式將 XXXService 的每個介面交給特定的 ServiceMethod 來實現。
    • 2.這裡的 ServiceMethod 怎麼來的呢?看36行的 loadServiceMethod 方法,這首先為了效能會去 serviceMethodCache 中看看是否有 XXXService 某個介面對應的 ServiceMethod,如果沒有的話就用 Builder 模式建立一個。

圖3:Retrofit#create.png

  • 3.回看圖2,建立好了 XXXService 的實現類之後,我們一般會結合 Rxjava 呼叫某個介面,讓其返回一個 Observable 物件。由前面的介紹,我們知道這裡 Observable 其實是呼叫 ServiceMethod.adapt(OkhttpCall) 返回的(可以看圖3的21行),我們進入這個方法。
    • 1.這個方法裡會將呼叫交給 CallAdapter.adapt(OkhttpCall)
    • 2.有些同學可能知道這個 CallAdapter 是在初始化 Retrofit 的時候被 Retrofit.Builder() 新增的 CallAdapterFactory 建立的。其有幾個具體實現如圖2。
    • 3.那麼這裡要選哪一個呢?選擇 CallAdapter 的具體邏輯在 ServiceMethod.build 裡面他會呼叫 ServiceMethod.createCallAdapter 這裡最終會交給 Retrofit.callAdapter 來尋找合適的 CallAdapter。
    • 4.那麼3中的具體查詢邏輯是什麼呢?這裡我總結一下:
      • 1.會對 CallAdapterFactory 進行迴圈查詢,一旦返回一個 CallAdapter 不為 null 那麼就使用這個。
      • 2.具體是否為 null 的邏輯交給具體的 CallAdapterFactory 去實現。
      • 3.因為是順序查詢,所以如果列表中有多個匹配項,這裡只取最開始的一個。
  • 4.到這裡我們先不看圖2,一般來說匹配上的 CallAdapterFactory 會是 RxJava2CallAdapterFactory。我們先研究一下他是怎麼產生一個 Observable 的。
    • 1.先看一下圖4,我們直接看20行,這裡解釋了為什麼一般會匹配到 RxJava2CallAdapterFactory 因為我們的 XXXService 定義介面的時候一般選擇的返回值 都是 Observable 或者有關 Rxjava 的返回值。然後我們直接看55行,這裡返回了一個 RxJava2CallAdapter,這個就是生成 Observable 的物件。
    • 2.接著我們看圖5,還記得上面的3.1中我們說的嗎?Observable 就是 CallAdapter.adapt(OkhttpCall) 產生的。這裡就是具體實現。
      • 1.可以看見18行根據介面呼叫是同步還是非同步會生成兩種不同的 Observable。
      • 2.然後後面都是根據一些 flag,為 Observable 新增一些操作符。

圖4:RxJava2CallAdapterFactory#get.png

圖5:RxJava2CallAdapter#adapt.png

  • 5.再回到圖2,現在我們已經有 Observable 了。這裡我們先跳過圖2中的幾個步驟,直接來到黃色的框,從這裡開始我們可以讓得到的 Observable 開始執行。對 Rxjava 熟悉的同學應該知道,一個 Observable 會從操作符流的最頂部開始執行。所以這裡會從我們前面講到的 RxJava2CallAdapter.adapt 中定義的第一個 Observable 的 subscribe 開始執行。我們就預設這次介面呼叫是同步的這樣簡單點,所以會先進入 CallExecuteObservable 中。
    • 1.先看圖6,第1行構造這個物件的時候會傳入一個 Call 物件,其實現有很多我們在這裡可以預設其為 OkhttpCall。
    • 2.圖6的第5行,是 Observable 開始執行的時候最先呼叫的方法(有興趣的同學可以看看 Rxjava 的原始碼解析)。這裡我們可以看見13行,其將呼叫交給了 Okhttp.execute。
    • 3.我們可以看向圖7的20行,這裡呼叫了 createRawCall 建立了一個 okhttp3.Call 其具體實現是 RealCall(我們直接使用 okhttp 的時候也是通過這個請求網路)。
    • 4.在回到圖2中,如圖2所示當呼叫 RealCall.execute 的時候,就會進入 okhttp 的請求鏈。okhttp 使用了責任鏈模式,將請求穿過圖2中的一個個攔截器,每個攔截器都負責一個功能。開發者可以在攔截器鏈的最開始插入自己的攔截器,以實現一些定製操作。
    • 5.再回到圖7,okhttp 將資料請求完畢之後會返回一個 okhttp3.Response,這時候會在32行呼叫43行的 parseResponse 來將解析這個 Response。
    • 6.圖7中後面有些程式碼看不見了,其實最終 Response 的解析會交給 ServiceMethod.toResponse。而其又會交給 Converter.coverter。這介面的實現類也很多,最常見的應該就是 GsonConverterFactory 提供的 GsonResponseBodyConverter 了。如圖2,我們一般也是在建立 Retrofit 的時候新增一些 Converter 以供這裡使用。同樣類似 CallAdapter,Converter 的選取也是一樣的策略
    • 7.經過以上呼叫,我們就有了一個retrofit2.Respons,其內部有一個解析了 body 之後的物件。

圖6:CallExecuteObservable#subscribeActual.png

圖7:OkHttpCall#execute.png

  • 6.CallExecuteObservable 中呼叫完畢之後,呼叫流程一般會交給 BodyObservable,這裡面很簡單,就是將 retrofit2.Respons 中的解析後的 body 交給下一個 Observable 操作符。就這樣順著操作符流最終我們在 XXXService 中定義的介面的返回值 Observable 的泛型物件就會被傳入到 subscribe 中供外部呼叫者使用。如圖2中的粉色框。

2.網路層定製程式碼

所謂定製就是在網路請求流程的各個主要節點中新增自己的程式碼實現以達到特殊的需求。經過前面的講解,我想讀者應該對整個網路層的請求流程有了一個大致的瞭解。這時我們可以再看看圖2,可以看見其中有幾處我綠色的框,這幾個地方就是我們可以新增定製程式碼的地方。接下來我就會按順序講解一下這幾處的定製程式碼是如何實現的。

圖8:RetrofitFactory.png

圖9:DefaultRetrofitConfig.png

(1)retrofit2.Call的裝飾

我們按請求順序可以在圖2中首先看見的是 NewCall.execute 這個框,接下來我就來說說這個可以怎麼定製。

  • 1.按照我們前面的講解,大家應該知道,如果不做任何定製的話這裡的 NewCall 就是 OkhttpCall,其會返回一個 retrofit.Response。最終會在開發者的 subscribe 裡面返回一個解析了 body 之後的資料結構(這裡就稱為 ContentData)。有時候我們會在 subscribe 裡面需要更多的資訊,比如在資料轉化過程中丟失的 head 的資訊
  • 2.此時我們就可以對 OkhttpCall 進行一個封裝,首先我們可以定義一個我們自己的 DataContainer 物件,其用於封裝 ContentData,然後其還可以裝資料轉化中丟失的資料。如圖10。

圖10:DataContainer.png

  • 3.那麼我們在定義 XXXService 的介面的返回值的時候就能這樣定義:Observable<DataContainer<ContentData>>
  • 4.此時有人眼尖就會發現,不對啊這個 DataContainer 是被 Gson 反序列化過來的,裡面的 okhttp3.Response 物件伺服器又不知道是什麼這樣怎麼序列化呢?
  • 5.答案就在圖8,圖9中。大家可以看圖8的第7行,這裡我新增了一個自定義的 CallAdapterFactory。
  • 6.在看圖8的44、48、49行,根據前面我們描述的請求流程,44行的 CallAdapter 會用來生成 Observable。再看48行,這裡的 call 就是 OkhttpCall 了。我們將其傳入 buildCall 中返回了一個 NewCall,這裡就是關鍵。
  • 7.buildCall 的實現程式碼在圖9,可以看38行。這裡的實現非常簡單直接就是將 OkhttpCall 封裝 返回了一個 ContainerCall,如圖11。

圖11:DataContainerCall.png

  • 8.DataContainerCall 裡面的程式碼就不用我說了吧,就是給 DataContainer 傳入一個 okhttp3.Response 物件。
  • 9.大家是不是覺得就這樣一個小東西很簡單?其實我也覺得很簡單,但是隻要你會用了這一個小東西,那麼更多實用的功能都能被這樣實現。

(2)OkhttpClient定製

按順序下來,第二個定製的地方就是 OkhttpCall 呼叫 okhttp.RealCall 的地方了。

  • 1.我們看圖8的21行,這裡給 Retrofit 新增了一個 OkhttpClient。之後的請求都是通過它來傳送的。
  • 2.這裡插一下,大家可以看看3行,這裡傳的是一個 RetrofitConfig,它其實是一個介面,像圖9的 DefaultRetrofitConfig 就是它的一個實現。當然我們還可以有不同的實現以實現不同的定製方式。
  • 3.那麼我們還是再看圖9的6行,可以看見這個方法的返回值 Builder 中新增了一系列 Intercept。由我們前面的講解可知,這些是攔截器,然後會按新增的順序攔截請求和響應。
  • 4.這裡可以看見我實現了各種不同的功能:列印網路請求日誌(這個在上一篇文章中沒實現,現在實現了)、過濾過於頻繁的請求(防止ddos攻擊)、SSL認證(當然現在沒有後端還沒實現)、超時攔截、新增自定義的引數等等。
  • 5.這裡的定製比較簡單,大家可以去看看各個攔截器中的實現。

(3)Converter定製

  • 1.其實這個也很簡單,大家可能都用過,就是圖8的5、6兩行,新增的資料轉換器。
  • 2.大家只要瞭解我前面講解的 Converter 的執行策略就可以了。

(4)CallAdapter定製

  • 1.大家可以回看 (1)retrofit2.Call的裝飾 這一節,我們新增了一個 CustomAdapterFactory。
  • 2.因為 CustomAdapterFactory 比 RxJava2CallAdapterFactory 先新增,所以其優先順序比較高。再看圖8的40行,這裡獲取了一個 delegate,其實就是 RxJava2CallAdapterFactory。所以我們可以在 RxJava2CallAdapter 返回的 Observable 上面新增一些統一的操作符。
  • 3.具體的程式碼在圖8的49行,然後轉到圖9的42行。可以看見我就只新增了一些簡單的操作符:計數請求成功和失敗次數、配合 ThrottlingInterceptor 進行頻繁請求過濾。

(5)網路層定製程式碼總結

上面就是在網路請求的四個主要節點進行定製的方式。其實總結起來比較簡單:1是擴充套件 Retrofit 返回的結果、2是擴充套件 okhttp 請求和返回、3是解析 okhttp 返回給 Retrofit 的結果、4是增強對 Retrofit 返回結果的處理。

四、總結

不知不覺已經寫了這麼多了,本來以為還可以寫一節 Fresco 的定製,現在看來只能放在下一篇文章了。在這裡預告一下:從零開始仿寫一個抖音App這一系列的文章大概還有一到兩篇 android 層面的文章,並且會在接下來的一週左右放出。

這一階段結束之後我的文章和學習重心將會轉向音視訊這塊。這幾個月過來雖然有時候文章會 delay,但最終我也信守承諾沒有棄坑。最後希望大家能持續關注本系列,畢竟我都已經這麼努力了不是:)

不販賣焦慮,也不標題黨。分享一些這個世界上有意思的事情。題材包括且不限於:科幻、科學、科技、網際網路、程式設計師、計算機程式設計。下面是我的微信公眾號:世界上有意思的事,乾貨多多等你來看。

世界上有意思的事

相關文章