天天看點

從零開始仿寫一個抖音App——Apt代碼生成技術、gradle插件開發與protocol協定本項目的 github 位址:MyTikTok

本文首發于簡書——何時夕,搬運轉載請注明出處,否則将追究版權責任。交流qq群:859640274

有人說我标題黨,也怪我開始決定寫的時候沒有注意标題,也沒想到會有這麼多閱讀量,的确會生出一些是非出來。那麼從現在開始标題改為 從零開始仿寫一個抖音App 系列。希望技術讨論能夠多一點,沒看文章就噴的人能夠少一點。我會堅持寫下去,好好提升自己的能力。

本項目的 github 位址:MyTikTok

大家好,兩周不見技術有沒有增長呢?本周的文章主要讨論下面幾個問題,大家可以按需跳章檢視以節省大家寶貴的時間,本文預計閱讀時間10分鐘。
  • 1.讨論——總結前兩周評論中有意義的讨論并給予我的解答
  • 2.mvps代碼生成原理——将上周的 mvps 架構的代碼生成原理進行解析
  • 3.開發一款gradle插件——從 mvps 的代碼引出 gradle 插件的開發
  • 4.高效的跨語言資料協定protocol——protocol 資料協定在 android 項目中的使用以及優勢

一、讨論

在放上讨論之前我需要重申本項目的意義、初衷和前提:
  • 1.本項目希望複刻大廠開發大 app 的流程和模式,這樣不僅對我自己是一個提升,對廣大讀者來說也是一個了解大廠的好機會。
  • 2.既然是複刻大廠的流程,大家就應該以一人分飾多角的角度來看待項目的結構、架構與開發過程,比如
    • 1.寫業務的同學應該想的是怎麼讓業務代碼高效高可複用。
    • 2.寫架構的同學應該想的是怎麼在對業務同學透明的前提下減少模闆代碼,為架構添加一些”工具糖“讓業務同學寫起代碼來更爽,使用規則的限制讓業務同學”帶着鐐铐碼代碼“使得項目代碼不會随着時間推移而”膨脹腐化“。
    • 3.寫底層算法的同學應該想的是怎樣讓算法更高效,讓底層算法對業務同學透明,同時讓他們用起算法來更友善。
    • 4.在學習本項目的過程中,如果能随時切換上面的三個視角,那麼一些困惑就會迎刃而解。
  • 3.對于大項目來說”麻煩就是友善“,一些目前看起來麻煩的操作都是為了以後項目的可控性。
  • 4.最後也希望大家在讨論一個問題的時候:有異議就拿出自己的想法和論據來進行讨論,我對于這種評論都會認真回複。如果是毫無論據的攻讦我會用同樣的方式回怼。

讨論1:為啥不用viewmode/mvvm架構?

  • 1.mvp 現在還是主流的 app 開發架構。
  • 2.光 livedata 和 viewmodel 如果不用 databinding 的話并不能成為 mvvm 架構。
  • 3.如果用了 databinding,databinding 不成熟也是一個坑。
  • 4.谷歌的架構元件也可以加入到mvp裡面,我在第一篇文章中叙述過。

讨論2:架構感覺不是很簡潔?

  • 1.參考本項目的意義

讨論3:apt 的這套操作,隻是為了 Presenter 的初始化傳參,如果使用注解通過 apt 生成 build 對象,是否簡單很多?

  • 1.參考本項目的意義中的一人分飾多角

讨論4:基本的接口定義都瞎寫…不允許有public…可怕!

  • 1.首先這裡的限制是對于開發者來說的,并不是語言上的限制,也就是說這是一個開發契約。在多人合作開發的時候,各個成員需要遵守契約,不可在 presenter 子類上面增加 public 的方法,變量和有參構造函數。
  • 2.為什麼要有這個契約?那是為了讓 presenter 的所有子類的行為與 presenter 一緻,這樣在後面的業務變化與重構的時候能實作 presenter 的可插拔,減少耦合度。
  • 3.參考本項目意義中的”帶着鐐铐碼代碼“

讨論5:因為是按子產品劃分的,所有子產品都使用mvp模式開發是否夠穩妥,我覺得每個子產品應該根據具體業務場景,來選擇适合的架構模式,有些适用mvc,有些适用mvvm,有些适用mvp。

  • 1.大的工程,不可能使用多種架構混合的,這樣不同的開發人員使用的架構不同,不具備思想一緻性,開發起來會非常困難,參考本項目的意義.
  • 2.mvc 隻适用于小的項目的架構,我想很多事實已經證明 mvc 會使 Activity/Fragment 的代碼 膨脹,就算是按現在分子產品,到了後面一個 Activity 的代碼也會膨脹到幾千行,這個問題在 mvc 下面是無解的。
  • 3.mvvm 的缺點參考第一個讨論中的回答

二、mvps代碼生成原理

上篇部落格對于 mvps 我隻介紹了在 app 運作時的整個流程,但是對于編譯時的整個代碼生成的流程卻因為時間限制沒有寫完,本章節就會将整個流程走一遍,同時介紹一下 apt 下 debug 的技巧。

1.例子

圖1:MainActivity.png

圖2:LinearPresenter.png

圖3:TextPresenter.png

圖4:ImagePresenter.png

  • 1.先上一個完整的例子吧,如圖1、2、3、4:MainActivity 包含了一個 LinearPresenter,LinearPresenter 包含了一個 TextPresenter 和一個 ImagePresenter。這裡的層級結構和 xml 檔案中是一緻的。
  • 2.我們的目的是将 MainActivity 中的兩個字段設定到 TextView 和 ImageView 中,這裡我們需要建立一個 LinearPresenter 然後用 create() 注入 view,用 bind()注入參數,最終在各個 子Presenter 中進行相應的操作。
  • 3.有人會說:這麼簡單的操作,我直接在 MainActivity 裡面一下就做好了,還需要這麼麻煩?但是大家可以仔細想想這這裡的結構會發現,這種結構有下面這些優點:
    • 1.我們其實是将 MainActivity 裡面的業務代碼和 ui 操作分散到各個不同的 presenter 中去了,而且仔細比較起來代碼量其實增加的不多。大多數模闆代碼都被 apt 自動生成了。
    • 2.在這裡 MainActivity 就隻需要管資料的生産以及生命周期的管理。這樣在未來業務複雜起來的時候所有的代碼都會被均攤到各個 Presenter 中,而不會讓 Activity 膨脹。
    • 3.除此之外,因為我們的在前面說過 Presenter 的子類是不允許有 public 的方法和變量、有參構造函數,這樣一來任何的 Presenter 都不會與外界進行耦合,我們未來想怎麼重構界面就怎麼重構。
    • 4.當然現在這樣還是有一些問題的:第一個問題是:每個 Presenter 都和固定的 Viewid 綁定了,如果我一個界面上有多個相同的 View 難道要定義多個不同的 Presenter 嗎?第二個問題是:View 之間如果有互相調用或者 Presenter 之間有互相調用怎麼辦?
    • 5.4中的問題由于篇幅限制本篇文章暫時不會說明解決方案,如果想知道答案的話,一定要關注本系列接下來的文章喲!

2.代碼生成流程

我們在上篇文章裡面講解了 Presenter 的整個運作流程,這一節我們就來講講使用 apt 生成模闆代碼的流程。建議結合項目源碼食用!
  • 1.有些無關緊要的代碼我就不一一截圖了,大家可以結合項目源碼來走整個流程。
  • 2.首先我們需要定義幾個注解:看看我們第一節中的例子吧,我定義了 @Field 和 @Inject 這兩個注解。這兩個注解在項目的 Annotation module 裡面。需要注意的是該 module 是 java library 的原因是因為要使用到 javax 這個包
  • 3.有了注解了就相當于有了一個辨別,這樣 APT 在編譯的時候就能擷取到注解注釋的字段的資訊。有了資訊之後我們就可以根據這些資訊來生成代碼了。
  • 4.那麼生成代碼的代碼在哪裡呢?細心的同學會發現有一個 Annotation-Processing module,很顯然這就是生成代碼的代碼。我們就拿其中的一個類進行分析好了。就決定是你了:FieldProcessor

圖5:FieldProcessor1.png

圖6:AutoService生成的檔案.png

  • 5.我就貼一些核心代碼,圖5中主要有用的就是最上面的兩個注解,@AutoService:注解處理器是Google開發的,用來生成 META-INF/services/javax.annotation.processing.Processor 檔案如圖6,而這個檔案就是讓 APT 知道 FieldProcessor是用于生成代碼的。大家可以在項目中找到這個檔案看看裡面寫的是啥。
  • 6.圖5中第二個注解,@SupportedAnnotationTypes:用于辨別 FieldProcessor 需要處理的注解,我們這裡需要處理的就是 @Field 所标注的字段。

圖7:FieldProcessor2.png

  • 7.圖7中有兩個方法:init 和 process。這兩個方法會按順序被在 APT 調用一次。

圖8:FetcherHelper.png

圖9:MainActivityFetcher.png

圖10:app module 配置檔案.png

  • 8.我貼一下生成後的代碼,這樣更友善講解。
  • 9.圖7中我們先進入 init 方法,首先這裡擷取了一個 fullName。這裡似乎是擷取了哪裡的配置?是哪裡呢?大家可以看圖10或者打開 App module 的 gradle 檔案。可以看見給 providerInterfaceName 配置了一個 "value" 。這裡的 ”value“ 就是我們擷取到的 fullName。
  • 10.圖7中接下來幾行很簡單就是分割一下,擷取一下 package 和 className。
  • 11.然後我們進入到圖7中的 process 方法裡面,這裡用到了 squareup.javapoet 這個庫的 api 我這裡不細講,就講講含義,有興趣的同學可以去百度用法。
    • 1.我們先直接去 FieldProcessor 的96行。這裡的意思很明顯,就是生成一個名為 FetcherHelper 的類,類的内容被包含在 fetcherInitClass 裡面。
    • 2.再看95行,fetcherInitClass 添加了一個名為 init 的方法。
    • 3.我們再向前看 fetcherInitClass 的定義處,發現這個 FetcherHelper 被定義成了 public final 的形式。
    • 4.再看 init 的定義處,這個方法是 public static final 的,并且這個方法被 invokeBy 這個 Annotation 給注釋了。
    • 5.最後我們回頭看看圖8的 FetcherHelper 類的代碼,發現生成的代碼和我們想象中差不多,就是 init 方法裡面還有一些代碼,這些代碼我們在後面講。

圖11:FieldProcessor3.png

  • 12.我們繼續解析圖7,進入 init 方法的88-94行,這裡是一個循環,循環的 map 的 key 表示某個含有 @Field 注解的 class 對象的資訊結構,value 表示該類中所有被 @Field 注解的字段的 class 對象的資訊結構的集合。那麼 generateForClass 方法的用處也就呼之欲出:就是為每個含有 @Field 注解的 class 對象生成一個 XXXFetcher 類,這個類的實作了 Fetcher 接口。具體例子可以看圖1的 MainActivity 對應生成的 Fetcher 類就是圖9中的 MainActivityFetcher。接下來我們進入 generateForClass 方法來看個究竟,如圖11:
    • 1.106行老樣子擷取需要生成的類的名字。
    • 2.177-123行,構造一個 class,這個 class 是 public final 的,并含有 Set mAccessibleNames、Set mAccessibleTypes、Fetcher mSuperFetche 這幾個 private final 的字段。
    • 3.124-127行,構造一個 構造函數,然後初始化 mAccessibleTypes 和 mAccessibleNames 這兩個變量。
    • 4.接下來就是依葫蘆畫瓢,不斷的按照 Fetcher 接口的定義生成方法。
    • 5.我們可以發現使用 squareup.javapoet 庫生成代碼就類似搭積木一樣,給一個個方法添加一個個節點,然後讓一個個方法組成一個類。下面的更多代碼就交給讀者去解析了,我想這應該不是很難的事情。

三、開發一款gradle插件

上一節我們講了如何使用 APT 生成模闆代碼,可能有同學會想如果我想向已經有的代碼裡面插入一些模闆代碼怎麼辦呢?這一件事 APT 是辦不到的,但是我們可以開發一款 gradle 插件來滿足我們這種需求。

1.背景知識

  • 1.Android apk 的建構流程是由一系列 gradle task 來實作的。比如說生成 R 檔案,比如 将 java 檔案編譯成 class 檔案最終生成 dex 檔案,比如将所有資源打包成一個 apk。
  • 2.我們可以通過定義 gradle 插件來将自己的 task 插入到 Android apk 的建構流程之中,這樣就能實作批量修改和插入代碼,減少重複的勞動。
  • 3.gradle 插件是使用 groovy 語言來開發的,是一種腳本語言比較簡單,我就不詳細介紹了,百度上都有。

2.開發插件

圖12:invoker 項目結構.png

圖13:invoker gradle檔案.png

圖14:invoker maven 配置.png

  • 1.我就非常簡單的介紹一下 gradle 插件開發的開始流程吧!
    • 1.先看圖12的項目結構,main 目錄裡面就是改了一下開發目錄的名字,然後就是需要在 resource 目錄裡面加一個配置檔案。
    • 2.然後就是 groovy 和 java 可以混編(圓形的是 java 檔案,方形的是 groovy 檔案),是以圖13中的 gradle 配置裡面添加了 groovy 和 java 插件來分别編譯兩種檔案。還有就是加了一個 maven 插件用于将插件傳到本地 maven 庫中,友善在主項目中引用。
    • 3.最後就是定義一些圖14中插件的 maven 配置,友善在 gradle 檔案中引用。

圖15:FetcherHelper.png

圖16:Fetchers.png

圖17:app gradle.png

圖18:invoker_info.png

  • 2.突然想起還沒說這個插件的目的是什麼,是以現在先來說一下這款插件的目的。
    • 1.我們先看看圖15中的代碼,細心的同學會發現這個類是我們前面 APT 生成的代碼,是用于初始化 MainActivityFetcher 類的。我們可以看見這裡用了一個靜态方法 init 來初始化,如果一個 module 裡面有多個不同的類中含有 @Field 注解的話,那麼 init 裡面就會初始化多個對應的 XXXFetcher 對象。
    • 2.這個時候有同學就會發現了,如果我們有很多個 module 都使用了 @Field 的話,那麼就會有很多個 FetcherHelper 類在等着初始化,此時我們是每增加一個 module 就手動增加代碼嗎?如果我又有其他注解也是類似這樣的模式的話比如 @Inject 那還是手動增加嗎?
    • 3.讓我們切換到架構組的視角,會發現這種事情是光靠人肉添加來保證是不可控制的,鬼知道某個業務同學會不會寫了一個 module 之後就忘記某些必須操作,最後上線就爆炸了。當然可以通過某些檢測和測試來保證正确性,但是那樣就會消耗人力資源,我們還是的從根源解決這個問題。
    • 4.人不可靠,但是代碼是可靠的。是以我們能不能想一個辦法讓所有的 FetcherHelper 的初始化代碼每次編譯都自動在某個地方注入代碼,然後被調用呢?答案就是:用 gradle 插件在編譯的時候将所有 FetcherHelper 的初始化代碼插入到圖16 Fetchers 的 init 方法裡面,最後我們隻需要調用 Fetchers.init 就能初始化所有的 FetcherHelper 了。
    • 5.整個方案分三步:
      • 1.還是使用 APT,我們定義兩個注解:@ForInvoker 和 @InvokeBy,就像圖15和圖16裡面那樣,需要被調用的方法就用 @InvokeBy 注解。需要調用别的方法的方法就用 @ForInvoker 注解。
      • 2.使用和前面生成代碼類似的流程,定義一個 InvokerProcessor,然後像圖17中那樣傳入一個檔案路徑,最後将上一步注解中的資訊寫入檔案中。最終的結果如圖18所示,大家可以在項目中檢視代碼和相應的檔案,這裡就不重複介紹了。
      • 3.最後在我們的插件中讀取上一步中儲存在檔案中的資訊,然後用 javassist 這個庫在相應的位置注入代碼。
    • 6.是不是很簡單?簡單個屁啊!肯定有一大波人又要吐槽了。這麼麻煩的方式虧你想的出來?、這就是一個僞需求!。。。當然我要辯解一番:
      • 1.第一個原因也是最重要的一個:站在架構組的角度,為了保證項目可控,這是一個對業務同學透明的好的解決方式。
      • 2.其實有了這個插件,我們不僅僅是解決這一個問題,一批類似的問題我們都可以用這兩個注解來解決,算是一勞永逸。
      • 3.有了這一個插件的經驗,我們可以定制更多插件,這就極大的增加了我們對 android apk 打包流程的控制程度。

圖19:invoker properties.png

圖20:invoker plugin.png

圖21:invoker transform.png

圖22:jar scanner.png

圖23:jar modifier.png

  • 3.到了這裡我們就可以開始正式的插件的解析了。
    • 1.首先任何程式的運作都會有一個入口,gradle 插件也不例外。還記得我們前面在 resource 目錄裡面定義的檔案嗎?如圖19這就是我們定義的插件的入口,當 gradle 運作的時候回去讀取這個檔案然後運作圖20中我們定義的 InvokerPlugin 的 apply 方法。
    • 2.我們看 apply 方法的内部,這裡傳入的是一個 project 對象,這對象儲存着整個工程的資訊對應着根目錄下面的 build.gradle 腳本。
    • 3.然後我們僅僅在目前的 module 是一個 android app 的時候才讓插件進行編譯替換,這樣能減少無謂的編譯。
    • 4.然後我們注冊了一個自定義的 Transform 對象,Transform是Android官方提供給開發者在項目建構階段即由class到dex轉換期間修改class檔案的一套api。目前比較經典的應用是位元組碼插樁、代碼注入技術。要了解更多這個類的詳情戳這裡: Transform ,我就不詳細講了。
    • 5.我們運作的時候圖21的 transform 方法就會被 gralde 調用。
      • 1.先看25-53,這裡是周遊被彙聚在 android app module 的 jar 和 源碼檔案,然後複制到輸出目錄也就是 build 目錄下面。這裡在周遊的過程中将兩種檔案的資訊存成了集合,以供後面使用。
      • 2.然後我們定義了一個 JarScanner 用來在後面掃描源碼檔案和 jar 檔案。這裡我們存了一個路徑,我想大家應該還記得,這個路徑就是前面我們定義的 invoker_info 檔案的相對路徑。
      • 3.59-69行我們就開始周遊前面存起來的 jar 和源碼檔案了。這裡用的是我們定義的 JarScanner 來周遊的。我們進入圖22的 scan 方法來看一看。
        • 1.先去 scanMetaInfo 裡,我們發現這方法是讀取每個 jar 的 meta 檔案資料,這裡我們要注意的是,比如 module1 被內建到 module2裡面的時候,到了目前這一步,module1的所有源碼檔案都被打包成了 jar 的形式,然後我們之前定義的 invoke_info 檔案裡的資料就被存在 jar 的 meta 資料中。
        • 2.這裡讀取了每個 jar 中的 invoker_info 資料之後,我們就有了 要被調用的方法 --》要調用被調用方法的方法 這樣的鍵值對集合。這樣先存入 mUnmatchedClasses。等後面一一驗證一下這裡的鍵值對是否是正确的。
        • 3.然後我們進入了 scanClasses 方法裡面,這裡隻要每驗證了 mUnmatchedClasses 中的一個鍵值對是正确的,那麼就将其從 mUnmatchedClasses 中移除,然後将資訊加入到 mInfos 裡面。
      • 4.出了 scan 方法,addToPath 周遊源碼檔案也是一樣的行為。最後 mInfos 裡面就有了經過驗證确實存在的 要被調用的方法 --》要調用被調用方法的方法 集合。
      • 5.最後我們看72行,這建立了一個 JarModifier 對象用于class位元組碼進行修改。
      • 6.看圖23的 modify 方法,這裡先将傳入的資訊整合成 mFile2InvocationMap<File, Map<String, Set<Invocation>>> 對象,這個對象的意義就是:某個 File 中的某個 String 名字的方法中需要調用對應的 Set 中的全部方法。
      • 7.然後周遊 mFile2InvocationMap,這裡每次周遊的主要邏輯在 modifyClass 裡面,這裡就是用 javassist 的 api 進行代碼注入。

3.上傳插件到本地Maven庫

到這裡為止我們的插件已經開發完成了,但是我們該如何使用這個插件呢?其實在任何項目中我們都在使用着 gradle 插件。

圖24:根 build.gradle.png

圖25:app module build.gradle.png

圖26:invoker gradle properties.png

圖27:invoker gradle檔案.png

圖28:maven目錄.png

圖29:運作 upload.png

  • 1.關掉 instant run
  • 2.使用插件有兩個步驟:
    • 1.在根目錄的 build.gradle 檔案裡面引入插件的代碼庫。這裡可以先注釋掉,等本地 maven庫建好之後再引用。如圖24
    • 2.在需要使用插件的 module 中引入插件。現在可以先注釋掉app module 插件中的引用,等待插件上傳成功的之後再引用,如圖25
  • 3.可能會有人奇怪了,我運作了項目之後報錯了啊!說是找不到這個插件。這裡我們應該了解一下關于 Maven 的一些知識。
    • 1.Maven 是一種建構源代碼的工具,他會将某些源代碼以某種格式(Project Object Model)進行打包,這樣我們就能很友善的引用某個别人開源的代碼庫了。
    • 2.gradle 中能夠使用 Maven 包,使用的方法就是大家在 dependencies 塊裡面的引用方式。
    • 3.在圖1中我們可以看見 repositories 塊裡面寫着好幾行代碼,每一行都表示一個 Maven 庫。有 google 的、有 jcenter 的、最後一個是我本地的 Maven 庫。當我們引用一個包的時候,gradle 就會去這些庫裡面找相應的 Maven 包然後下載下傳下來供項目使用。
  • 4.怎麼建立一個本地 Maven 庫呢?很簡單:
    • 1.将本地某個空目錄路徑設定為倉庫根目錄,比如我在 mac 下的庫根目錄就如圖24所示。
    • 2.比如我們需要将 Invoker 這個 module 上傳到本地庫中,那麼就在 module 中建立一個 gradle.properties 檔案,如圖26,這樣在本 module 的 gradle 腳本中就能讀取裡面的配置
    • 3.注意 gradle.properties 以及 build.gradle 檔案裡面引用的路徑需要是你自己設定的本地路徑。
    • 4.我們需要再在 module 的 gradle 檔案裡面添加一個 maven 插件,然後寫一個上傳方法 uploadArchives。如圖27.
    • 5.在Gradle project視窗運作 uploadArchives任務,這樣就上傳了 Invoker 插件,如圖29。
  • 5.最後将插件的引用和代碼庫的引用打開,重新 clean build 一下就可以在app module 裡面使用插件了。
  • 6.當然你也可以在私有或者公有雲上建立一個 Maven 庫然後修改一下依賴和上傳路徑這樣也能順利的運作起來。

四、高效的跨語言資料協定protocol

  • 1.protocol 是一款和 json、xml 類似的跨語言資料傳輸協定,是 google 開發的。
  • 2.他由三部分構成:
    • 1.proto 定義檔案:相當于一種新的語言,用于定義某種資料結構,比如 java 中的 Person Bean。定義完成了之後,google 提供了各種轉化程式,可以直接将這種資料結構轉化成相應語言的類檔案。現在支援java、c++、python、go 等等。我們用 proto 語言定義好了資料結構轉化為相應語言之後,可以內建到相應語言的項目中。
    • 2.proto 庫:這個是對應語言的代碼庫,對應到 java 就是 jar 包。這個庫有很多 proto 相關的工具,比如說1中生成的類檔案中就會依賴 proto 庫中的代碼。
    • 3.proto 資料:當我們要用 proto 在 c++ 項目和 java 項目之間傳輸資料的時候應該怎麼做呢?
      • 1.定義好 proto 定義檔案
      • 2.将 proto 定義檔案轉化為 jar 和 c++庫檔案,然後與 proto 庫一起內建到兩個項目中。
      • 3.在 java 項目中初始化 proto 定義檔案中定義的對象,然後用 proto 庫的 api 将對象資料寫入到檔案中。
      • 4.将檔案傳給 c++ 項目然後用 proto 庫來讀取檔案中的資料,最後恢複成 c++ 的對象。
      • 5.上面我們傳輸的資料就是 proto 資料。
  • 3.我們為什麼要使用 proto,簡單來講他有下面這些優勢:
    • 1.他簡單快速,需要注意到的是, proto 資料中并不會包含任何的對象的類資訊,裡面有的隻是對象字段的值。僅這一點他占用的空間就比 json 和 xml 小上一半多。
    • 2.proto 資料序列化後所生成的二進制消息非常緊湊,他利用了Varint來非常緊湊的表示數字。比如說不是所有的 int 都占4個位元組,小的數可能隻占1個位元組。這種技術和哈夫曼編碼很像。
    • 3.proto 定義檔案生成的類的 api 非常完善,有各種最佳實踐的代碼,省去了我們在解析 json 和 xml 的時候寫的大量模闆判斷代碼。
  • 4.protocol 在 android 項目中的使用,這裡的話我就直接上一個連結了: protocol 在 android 中的使用 ,感謝這位部落客的部落格,我們的項目中已經內建了 protocol。
  • 5.今天這裡隻是對 protocol 進行一個簡單的介紹,後面的話我會針對 protocol 在項目中的實際應用專門開一篇部落格進行講解,希望大家能持續關注我的部落格!

五、尾巴

本篇文章是從零開始寫一個抖音App系列文章的第三篇,篇幅比較長能看到這裡的同學非常感謝你們對我的認可。給一個看到這裡的同學的小福利吧:在未來我會開放本項目在 github 上權限,隻要對本項目了解比較深的同學都能參與項目的開發,看到這句話的同學我會優先考慮,但是隻限前5名,記得加QQ群然後在群裡小窗我。

連載文章