迴歸初心:極簡 Android 組件化方案 — AppJoint

Android 組件化的概念大概從兩年前開始有人討論,到目前爲止,技術已經慢慢沉澱下來,愈來愈多團隊開源了本身組件化框架。本人所在團隊從去年開始調研組件化框架,在瞭解社區衆多組件化方案以後,決定自研組件化方案。爲何明明已經有不少輪子能夠用了,卻仍是決定要本身造個新輪子呢?java

主要的緣由是在調研了諸多組件化方案以後,發現儘管它們都有各自的優勢,可是依然有一些地方不是使人十分滿意。而其中最重要的一個因素就是引入組件化方案成本較高,對已有項目改造過大。我想這一點應該不少人都有相同的體會,不少時候 咱們對於項目的重構是須要與新需求的迭代同步進行的 ,幾乎很難停下來只作項目的組件化。android

另一點,我不太但願本身的項目和某一款組件化框架 強耦合。 Activity 的路由方案也好,跨模塊的同步或異步方法調用也好,我但願可以沿用項目已有的調用方式,而不是使用某款組件化框架本身特定的調用方式。例如某個接口已經基於 RxJava 封裝爲了 Observable 的接口,我就不太但願由於組件化的關係,這個接口位於另外一個模塊以後,我就不得不用這個組件化框架定義的方式去調用,我仍是但願以 RxJava 的方式去調用。git

迴歸初心

我認爲目前想要進行組件化的項目應該能夠分爲兩類:github

  • 包含有一個 application 模塊,以及一些技術組件的 library 模塊(業務無關)。
  • 除了 application 模塊之外,已經存在若干包含業務的 library 模塊和技術的 library 模塊。

不管是哪一種類型的項目,面臨的問題應該都是相似的,那就是項目大起來之後,編譯實在是太慢了數據庫

除此之外,就是 跨模塊的功能調用很是不便 ,這個問題主要體如今上面列舉的第二種類型的項目。本人所在的項目在組件化以前就是上面列舉的第二種類型的項目,application 模塊最先用來承載業務邏輯代碼,隨着業務發展,大概是某位開發人員以爲, 「不行,這樣下去 application 模塊代碼數量會失控的」,因而後續新的業務模塊都會新開一個 library 模塊進行開發,就這樣斷斷續續目前有了大概 20+ 個 library 模塊(業務相關模塊,技術模塊不包含在內)。api

這種作法是符合軟件工程思想的,可是也帶來了一些棘手的問題,因爲 application 模塊裏的業務功能和 library 模塊裏的業務功能在邏輯地位上是平等的,因此不免會有互相調用的狀況,可是它們在項目依賴層次上卻不是處於相等的地位,application 調用 library 倒沒事,可是反過來調用就成了問題。另外,剩下這 20 + 個 library 模塊在依賴層次中也不全是屬於同一層次的,library 模塊之間互相依賴也很複雜。數組

因此我指望的組件化方案要求解決的問題很簡單:瀏覽器

  • 業務模塊單獨編譯,單獨運行,而不是耗費大量時間全量編譯整個 App
  • 跨模塊的調用應該優雅,不管兩個模塊在依賴樹中處於什麼樣的位置,均可以很簡單的互相調用
  • 不要有太多的學習成本,沿用目前已有的開發方式,避免代碼和具體的組件化框架綁定
  • 組件化的過程能夠是漸進的,當即拆分代碼不是組件化的前置條件
  • 輕量級,不要引入過多中間層次(例如序列化反序列化)致使沒必要要的性能開銷以及維護複雜度

基於上述的思想,咱們開發了 AppJoint 這個框架用來幫助咱們實現組件化。安全

AppJoint 是一個很是簡單有效的方案,引入 AppJoint 進行組件化全部的 API 只包含 3 個註解,加 1 個方法,這多是目前最簡單的組件化方案了,咱們的框架不追求功能要多麼複雜強大,只專一於框架自己實用、簡單與高效。並且總體實現也很是簡單,核心源碼 不到500行bash

模塊獨立運行遇到的問題

本人接觸最先的組件化方案是 DDComponentForAndroid,學習這個方案給了我不少啓發,在這個方案中,做者提出,能夠在 gradle.properties 中新增一個變量 isRunAlone=true ,用來控制某個業務模塊是 以 library 模塊集成到 App 的全量編譯中 仍是 以 application 模塊獨立編譯啓動 。不知道是否是不少人也受了相同的啓發,後面不少的組件化框架都是使用相似的方案:

if (isRunAlone.toBoolean()) {    
    apply plugin: 'com.android.application'
} else {  
    apply plugin: 'com.android.library'
}
複製代碼

根據我本人的實踐,這種方式有一些缺點。首先有一些開源框架在 library 模塊中和在 application 模塊中使用方法是不同的,例如 ButterKinfe , 在 application 中使用 R.id.xxx,在 library 模塊中使用 R2.id.xxx ,若是想組件化,代碼必須保證在兩種狀況下均可用,因此基本只能拋棄 ButterKnife 了,這會給項目帶來巨大的改形成本。

除此之外,還有一些開源框架是隻能在 application 模塊中配置的,配置完之後對整個項目的全部 library 模塊都生效的,例如一些字節碼修改的框架(好比 AOP 一類的),這是一種狀況。還有一種狀況,若是原先項目已是多模塊的狀況下,可能多個模塊的初始化都是放在 application 模塊裏,由於 application 模塊是 上帝模塊,他能夠訪問到項目中任意一塊代碼,因此在這裏作初始化是最省事的。可是如今拆分爲模塊以後,由於每一個模塊須要獨立運行,因此模塊須要負責自身的初始化,但是有時候這個模塊的初始化是隻能在 application 模塊裏才能夠作的,咱們把這段邏輯下放到 library 以後,如何初始化就成了問題。

這兩種狀況,若是咱們使用 gradle.properties 中的變量來切換 applicationlibrary 的話,咱們勢必須要在這個模塊中維護兩套邏輯,一套是在 application 模式下的啓動邏輯,一套是在 library 模式下的啓動邏輯。原先這個模塊是專一本身自己的業務邏輯的,如今不得不爲了可以獨立做爲 application 啓動,而加入許多其餘代碼。一方面 build.gradle 文件中會充滿不少 if - else,另外一方面 Java 源碼中也會加入許多判斷是否獨立運行的邏輯。

最終 Release App 打包時,這些模塊是做爲 library 存在的,可是咱們爲了組件化已經在這個模塊中加入了不少幫助該模塊獨立運行(以 application 模式)的代碼(例如模塊須要單獨運行,須要一個屬於這個模塊的 Laucher Activity),雖然這些代碼在線上不會生效,但是從潔癖的角度來說,這些代碼其實不該該被打包進去。其實說了這麼多無非就是想說明,若是咱們但願經過某個變量來控制模塊以 application 形式仍是以 library 形式存在,那麼咱們確定要在這個模塊中加入維護二者的差別的代碼,並且可能代碼量還很多,最後代碼呈現的狀態多是不太優雅的。

此外模塊中的 AndroidManifest.xml 也須要維護兩份:

if (isRunAlone.toBoolean()) {
    manifest.srcFile 'src/main/runalone/AndroidManifest.xml'
} else {
    manifest.srcFile 'src/main/AndroidManifest.xml'
}
複製代碼

可是 xml 畢竟不是代碼,沒有封裝繼承這些面向對象的特性,因此每當咱們增長、修改、刪除四大組件的時候,都須要記得要在兩個 AndroidManifest.xml 都作對應的修改。除了 AndroidManifest.xml 之外,資源文件也存在這個問題,雖然工做量不至於特別巨大,但這樣的作法其實已經違背了面向對象的設計原則。

最後還有一個問題,每當模塊在 application 模式和 library 模式之間進行切換的時候,都須要從新 Gradle Sync 一次,我想既然是須要組件化的項目那確定已是那種編譯速度極慢的項目了,即便是 Gradle Sync 也須要等待很多時間,這點也是咱們不太能接收的。

建立多個 Application 模塊

咱們最後是如何解決模塊的單獨編譯運行這個問題的呢?答案是 爲每一個模塊新建一個對應的 application 模塊 。也許你會對此表示懷疑:若是爲每一個業務模塊配一個用於獨立啓動的 application 模塊,那模塊會顯得特別多,項目看起來會很是的亂的。可是其實咱們能夠把全部用於獨立啓動業務模塊的 application 模塊收錄到一個目錄中:

projectRoot
  +--app
  +--module1
  +--module2
  +--standalone
  |  +--module1Standalone
  |  +--module2Standalone   
複製代碼

在上面這個項目結構圖中,app 模塊是全量編譯的 application 模塊入口,module1module2 是兩個業務 library 模塊, module1Standalonemodule2Standalone 是分別使用來獨立啓動 module1module2 的 2 個 application 模塊,這兩個模塊都被收錄在 standalone 文件夾下面。事實上,standalone 目錄下的模塊不多須要修改,因此這個目錄大多數狀況下是屬於摺疊狀態,不會影響整個項目結構的美觀。

這樣一來,在項目根目錄下的 settings.gradle 裏的代碼是這樣的:

// main app
include ':app'
// library modules
include ':module1'
include ':module2'
// for standalone modules
include ':standalone:module1Standalone'
include ':standalone:module2Standalone'
複製代碼

在主 App 模塊(app 模塊)的 build.gradle 文件裏,咱們只須要依賴 module1module2 ,兩個 standalone 模塊只和各自對應的業務模塊的獨立啓動有關,它們不須要被 app 模塊依賴,因此 app 模塊的 build.gradle 中的依賴部分代碼以下:

dependencies {
    implementation project(':module1')
    implementation project(':module1')
}
複製代碼

那些用於獨立運行的 application 模塊裏的 build.gradle 文件中,就只有一個依賴,那就是須要被獨立運行的 library 模塊。以 standalone/module1Standalone 爲例,它對應的 build.gradle 中的依賴爲:

dependencies {
    implementation project(':module1')
}
複製代碼

在 Android Studio 中建立模塊,默認模塊是位於項目根目錄之下的,若是但願把模塊移動到某個文件夾下面,須要對模塊右鍵,選擇 "Refactor -- Move" 移動到指定目錄之下。

當咱們建立好這些 application 模塊以後,在 Android Studio 的運行小三角按鈕旁邊,就能夠選擇咱們須要運行哪一個模塊了:

這樣一來,咱們首先能夠感覺到的一點就是模塊再也不須要改 gradle.properties 文件切換 libraryapplication 狀態了,也再也不須要忍受 Gradle Sync 浪費寶貴的開發時間,想全量編譯就全量編譯,想單獨啓動就單獨啓動。

因爲專門用於單獨啓動的 standalone 模塊 的存在,業務的 library 模塊只須要按本身是 library 模塊這一種狀況開發便可,不須要考慮本身會變成 application 模塊,因此不管是新開發一個業務模塊仍是從一個老的業務模塊改形成組件化形式的模塊,所要作的工做都會比以前更輕鬆。而以前提到的,爲了讓業務模塊單獨啓動所須要的配置、初始化工做均可以放到 standalone 模塊 裏,而且不用擔憂這些代碼被打包到最終 Release 的 App 中,前面例子中提到的用來使模塊單獨啓動的 Launcher Activity,只要把它放到 standalone 模塊 模塊便可。

AndroidManifest.xml 和資源文件的維護也變輕鬆了。四大組件的增刪改只須要在業務的 library 模塊修改便可,不須要維護兩份 AndroidManifest.xml 了,standalone 模塊 裏的 AndroidManifest.xml 只須要包含模塊獨立啓動時和 library 模塊中的 AndroidManifest.xml 不一樣的地方便可(例如 Launcher Activity 、圖標等),編譯工具會自動完成兩個文件的 merge。

推薦在 standalone 模塊 內指定一個不一樣於主 App 的 applicationId,即模塊單獨啓動的 App 與主 App 能夠在手機內共存。

咱們分析一下這個方案,和原先的比,首先缺點是,引入了不少新的 standalone 模塊,項目彷佛變複雜了。可是優勢也是明顯的,組件化的邏輯更加清晰,尤爲是在老項目改造狀況下,所須要付出的工做量更少,並且不須要在開發期間頻繁 Gradle Sync。 總的來講,改造後的組件化項目更符合軟件工程的設計原則,尤爲是開閉原則(open for extension, but closed for modification)。

介紹到這裏爲止,咱們尚未使用任何 AppJoint 的 API,咱們之因此沒有藉助任何組件化框架的 API 來實現模塊的獨立啓動,是由於本文一開始提出的,咱們不但願項目和任何組件化框架強綁定, 包括 AppJoint 框架自己,AppJoint 框架自己的設計是與項目鬆耦合的,因此使用了 AppJoint 框架進行組件化的項目,若是從此但願能夠切換到其它更優秀的組件化方案,理論上是很輕鬆的。

爲每一個模塊準備 Application

在組件化以前,咱們經常把項目中須要在啓動時完成的初始化行爲,放在自定義的 Application 中,根據本人的項目經驗,初始化行爲能夠分爲如下兩類:

  • 業務相關的初始化。例如服務器推送長鏈接創建,數據庫的準備,從服務器拉取 CMS 配置信息等。
  • 與業務無關的技術組件的初始化。例如日誌工具、統計工具、性能監控、崩潰收集、兼容性方案等。

咱們在上一步中,爲每一個業務模塊創建了獨立運行的 standalone 模塊 ,可是此時還並不能把業務模塊獨立啓動起來,由於模塊的初始化工做並無完成。咱們在前面介紹 AppJoint 的設計思想的時候,曾經說過咱們但願組件化方案最好 『不要有太多的學習成本,沿用目前已有的開發方式』,因此這裏咱們的解決方案是,在每一個業務模塊裏新建一個自定義的 Application 類,用來實現該業務模塊的初始化邏輯,這裏以在 module1 中新建自定義 Application 爲例:

@ModuleSpec
public class Module1Application extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        // do module1 initialization
        Log.i("module1", "module1 init is called");
    }
}
複製代碼

如上面的代碼所示,咱們在 module1 中新建一個自定義的 Application 類,名爲 Module1Application。那咱們是否是應該把與這個模塊有關的全部初始化邏輯都放在這個類裏面呢?並不徹底是這樣。

首先,對於前面提到的當前模塊的 業務相關的初始化 ,毫無疑問應該放在這個 Module1Application 類中,可是針對前面提到的該模塊的 與業務無關的技術組件的初始化 放在這裏就不是很合適了。

首先,從邏輯上考慮,業務無關的技術組件的初始化應該放在一個統一的地方,把它們放在主 App 的自定義 Application 類中比較合適,若是每一個模塊爲了本身能夠獨立編譯運行,都要本身初始化一遍,那麼全部代碼最後一塊兒全量編譯的時候,這些初始化行爲就會在代碼中出現好幾回,這樣既不合理,也可能會形成潛在問題。

那麼,若是咱們在 Module1Application 中作判斷,若是它自身處於獨立編譯運行狀態,就執行技術組件的初始化,反之,若它處於全量編譯運行狀態中,就不執行技術組件的初始化,由主 App 的 Application 來實現這些邏輯,這樣是否能夠呢?理論上這種方案可行,可是這麼作就會遇到和前面提到的 『在 gradle.properties 中維護一個變量來控制模塊是否獨立編譯』一樣的問題,咱們不但願把和業務無關的邏輯(用於業務模塊獨立啓動的邏輯)打包進最終 Release 的 App。

那應該如何解決這個問題呢?解決方案和前面一小節相似,咱們不是爲 module1 模塊準備了一個 module1Standalone 模塊嗎?既然技術相關的組件的初始化並非 module1 模塊的核心,只和 module1 模塊的獨立啓動有關,那麼放在 module1Standalone 模塊裏是最合適的,由於這個模塊只會在 module1 的獨立編譯運行中使用到,它的任何代碼都不會被打包到最終 Release 的 App 中。咱們能夠在 module1Standalone 中定義一個 Module1StandaloneApplication 類,它從 Module1Application 繼承下來:

public class Module1StandaloneApplication extends Module1Application {

    @Override
    public void onCreate() {
        // module1 init inside super.onCreate()
        super.onCreate();
        // initialization only used for running module1 standalone
        Log.i("module1Standalone", "module1Standalone init is called");
    }
}
複製代碼

而且咱們在 module1Standalone 模塊的 AndroidManifest.xml 中把 Module1StandaloneApplication 設置爲 Standalone App 使用的自定義 Application 類:

<application android:icon="@mipmap/module1_launcher" android:label="@string/module1_app_name" android:theme="@style/AppTheme" android:name=".Module1StandaloneApplication">
        <activity android:name=".Module1MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>
複製代碼

在上面的代碼中,咱們除了設置了自定義的 Application 之外,還設置了一個 Launcher Activity (Module1MainActivity),這個 Activity 即爲模塊的啓動 Activity,因爲它只存在於模塊的獨立編譯運行期間,App 全量打包時是不包含這個 Module1MainActivity 的,因此咱們能夠在裏面定義一些方便模塊獨立調試的功能,例如快速前往某個頁面以及建立 Mock 數據。

這樣,只要咱們單獨運行 module1Standalone 這個模塊的時候,使用的 Application 類就是 Module1StandaloneApplication。在開發時,咱們須要單獨調試 module1 時,咱們只須要啓動 module1Standalone 這個模塊進行調試便可;而在 App 須要全量編譯時,咱們則正常啓動原來的主 App 。不管是哪一種狀況, module1 這個模塊始終是以 library 形式存在的,這意味着,若是咱們但願把原先的業務模塊改形成組件化模塊,須要的改造量縮小不少,咱們改造的過程主要是在 增長代碼,而不是 修改代碼,這點符合軟件工程中的『開閉原則』。

寫到這裏,咱們其實還有一個問題沒有解決。Module1Application 目前除了被 Module1StandaloneApplication 繼承之外,沒有被任何其它地方引用到。您可能會有疑問:那咱們如何保證 App 全量編譯運行時,Module1Application 裏的初始化邏輯會被調用到呢?細心的您可能早就已經發現了:咱們在上面定義 Module1Application 時,同時標記了一個註解 @ModuleSpec:

@ModuleSpec
public class Module1Application extends Application {
    ...
}
複製代碼

這個註解的做用是告知 AppJoint 框架,咱們須要確保當前模塊該 Application 中的初始化行爲,可以在最終全量編譯時,被主 App 的 Application 類調用到。因此對應的,咱們的主 App 模塊(app 模塊)的自定義 Application 類也須要被一個註解 -- AppSpec 標記,代碼以下所示:

@AppSpec
public class App extends Application {
    ...
}
複製代碼

上面代碼中的 App 爲主 App 對應的自定義 Application 類,咱們給這個類上方標記了 @AppSpec 註解,這樣系統在執行 App 自身初始化的同時會一併執行這些子模塊的 Application 裏對應聲明週期的初始化。即:

  • App 執行 onCreate 方法時,保證也同時執行 Module1ApplicationModule2ApplicationonCreate 方法 。
  • App 執行 attachBaseContext 方法時,保證也同時執行 Module1ApplicationModule2ApplicationattachBaseContext 方法。
  • 依次類推,當 App 執行某個生命週期方法時,保證子模塊的 Application 的對應的生命週期方法也會被執行。

這樣,咱們經過 AppJoint@ModuleSpec@AppSpec 兩個註解,在主 App 的 Application 和子模塊的 Application 之間創建了聯繫,保證了在全量編譯運行時,全部業務模塊的初始化行爲都能被保證執行。

到這裏爲止,咱們已經處理好了業務模塊在 獨立編譯運行模式全量編譯運行模式 這兩種狀況下的初始化問題,目前關於 Application 還有一個潛在問題,咱們的項目在組件化以前,咱們常常會在 Applictaion 類的 onCreate 週期保存當前 Appliction 的引用,而後在應用的任何地方均可以使用這個 Application 對象,例以下面這樣:

public class App extends Application {

    public static App INSTANCE;

    @Override
    public void onCreate() {
        super.onCreate();
        INSTANCE = this;
    }
}
複製代碼

這麼處理以後,咱們能夠在項目任意位置經過 App.INSTANCE 使用 Application Context 對象。可是,如今組件化改造之後,以 module1 爲例,在獨立運行模式時,應用的 Application 對象是 Module1StandaloneApplication 的實例,而在全量編譯運行模式時,應用的 Application 對象是主 App 模塊的 App 的實例,咱們如何能像以前同樣,作到在項目中任何一個地方都能獲取到當前使用的 Application 實例呢?

咱們能夠把項目中全部自定義 Application 內部保存的自身的 Application 實例的類型,從具體的自定義類,改成標準的 Application 類型,以 Module1Application 爲例:

@ModuleSpec
public class Module1Application extends Application {
    
    public static Application INSTANCE;

    @Override
    public void onCreate() {
        super.onCreate();
        INSTANCE = (Application)getApplicationContext()
        // do module1 initialization
        Log.i("module1", "module1 init is called");
    }
}
複製代碼

咱們能夠看到,若是按原來的寫法, INSTANCE 的類型通常是具體的自定義類型 Module1Application,如今咱們改爲了 Application。同時 onCreate 方法裏爲 INSTANCE 賦值的語句再也不是 INSTANCE = this,而是 INSTANCE = (Application)getApplicationContext()。這樣處理之後,就能夠保證 module1 裏面的代碼,不管是在 App 全量編譯模式下,仍是獨立編譯調試模式下,均可以經過 Module1Application.INSTANCE 訪問當前的 Application 實例。這是因爲 AppJoint 框架 保證了當主 App 的 App 對象被調用 attachBaseContext 回調時,全部組件化業務模塊的 Application 也會被調用 attachBaseContext 這個回調

這樣,咱們在 module1 這個模塊裏的任何位置使用 Module1Application.INSTANCE 總能正確地得到 Application 的實例。對應的,咱們使用相同的方法在 module2 這個模塊裏,也能夠在任何位置使用 Module2Application.INSTANCE 正確地得到 Application 的實例,而不須要知道當前處於獨立編譯運行狀態仍是全量編譯運行狀態。

必定不要 依賴某個業務模塊自身定義的 Application 類的實例(例如 Module1Application 的實例),由於在運行時真正使用的 Application 實例可能不是它。

咱們已經解決業務模塊在 單獨編譯運行模式 下和在 App 全量編譯模式 下,初始化邏輯應該如何組織的問題。咱們沿用了咱們熟悉的自定義 Application 方案,來承載各個模塊的初始化行爲,同時利用 AppJoint 這個膠水,把每一個模塊的初始化邏輯集成到最終全量編譯的 App 中。而這一切和 AppJoint 有關的 API 僅僅是兩個註解,這裏很好的說明了 AppJoint 是個學習成本低的工具,咱們能夠沿用咱們已有的開發方式而不是改造咱們原有的代碼邏輯致使項目和組件化框架形成過分耦合。

跨模塊方法的調用

雖然目前每一個模塊已經有獨立編譯運行的可能了,可是開發一個成熟的 App 咱們還有一個重要的問題沒有解決,那就是跨模塊的方法調用。由於咱們的業務模塊不管是從業務邏輯上考慮仍是從在依賴樹上的位置考慮,都應該是具備同等的地位的,體如今依賴層次上,這些業務模塊應該是平級的,且互相之間沒有依賴:

上圖是咱們比較理想狀況下的組件化的最終狀態,App 模塊不承載任何業務邏輯,它的做用僅僅是做爲一個 application 殼把 Module1 ~ Module(n) 這個 n 個模塊的功能都集成在一塊兒成爲一個完整的 App。Module1 ~ Module(n) 這 n 個模塊互相之間不存在任何交叉依賴,它們各自僅包含各自的業務邏輯。這種方式雖然完成了業務模塊之間的解耦,可是給咱們帶來的新的挑戰:業務模塊之間互相調用彼此的功能是很是常見且合理的需求,可是因爲這些模塊在依賴層次上位於同一層次,因此顯然是沒法直接調用的。

此外,上圖的這種形態是組件化的最終的理想狀態,若是咱們要將項目改造以達到這種狀態,毫無疑問須要付出巨大的時間成本。在業務快速迭代期間,這是咱們沒法承擔的成本,咱們只能逐漸地改造項目,也就是說,App 模塊內的業務代碼是被逐漸拆解出來造成新的獨立模塊的,這意味着在組件化過程的至關長一段時間內,App 內仍是存在業務代碼的,而被拆解出來的模塊內的業務邏輯代碼,是有可能調用到 App 模塊內的代碼的。這是一種很尷尬的狀態,在依賴層次中,位於依賴層次較低位置的代碼反而要去調用依賴層次較高位置的代碼。

針對這種狀況,咱們比較容易想到,咱們再新建一個模塊,例如 router 模塊,咱們在這個模塊內定義 全部業務模塊但願暴露給其它模塊調用的方法,以下圖:

projectRoot
  +--app
  +--module1
  +--module2
  +--standalone
  +--router
  |  +--main
  |  |  +--java
  |  |  |  +--com.yourPackage
  |  |  |  |  +--AppRouter.java
  |  |  |  |  +--Module1Router.java
  |  |  |  |  +--Module2Router.java
複製代碼

在上面的項目結構層次中,咱們在新建的 router 模塊下定義了 3 個 接口

  • AppRouter 接口聲明瞭 app 模塊暴露給 module1module2 的方法的定義。
  • Module1Router 接口聲明瞭 module1 模塊暴露給 appmodule2 的方法的定義。
  • Module2Router 接口聲明瞭 module2 模塊暴露給 module1app 的方法的定義。

AppRouter 接口文件爲例,這個接口的定義以下:

public interface AppRouter {

    /** * 普通的同步方法調用 */
    String syncMethodOfApp();

    /** * 以 RxJava 形式封裝的異步方法 */
    Observable<String> asyncMethod1OfApp();

    /** * 以 Callback 形式封裝的異步方法 */
    void asyncMethod2OfApp(Callback<String> callback);
}
複製代碼

咱們在 AppRouter 這個接口內定義了 1 個同步方法,2 個異步方法,這些方法是 app 模塊須要暴露給 module1module2 的方法,同時 app 模塊自身也須要提供這個接口的實現,因此首先咱們須要在 appmodule1module2 這三個模塊的 build.gradle 文件中依賴 router 這個模塊:

dependencies {
    // Other dependencies
    ...
    api project(":router")
}
複製代碼

這裏依賴 router 模塊的方式是使用 api 而不是 implementation 是爲了把 router 模塊的信息暴露給依賴了這些業務模塊的 standalone 模塊app 模塊因爲沒有別的模塊依賴它,不受上面所說的限制,能夠寫成 implementation 依賴。

而後咱們回到 app 模塊,爲剛剛在 router 定義的 AppRouter 接口提供一個實現:

@ServiceProvider
public class AppRouterImpl implements AppRouter {

    @Override
    public String syncMethodOfApp() {
        return "syncMethodResult";
    }

    @Override
    public Observable<String> asyncMethod1OfApp() {
        return Observable.just("asyncMethod1Result");
    }

    @Override
    public void asyncMethod2OfApp(final Callback<String> callback) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                callback.onResult("asyncMethod2Result");
            }
        }).start();
    }
}
複製代碼

咱們能夠發現,咱們把 app 模塊內的方法暴露給其它模塊的方式和咱們平時寫代碼並無什麼不一樣,就是聲明一個接口提供給其它模塊,同時在本身內部編寫一個這個接口的實現類。不管是同步仍是異步,不管是 Callback 的方式,仍是 RxJava 的方式,均可以使用咱們原有的開發方式。惟一的區別就是,咱們在 AppRouterImpl 實現類上方標記了一個 @ServiceProvider 註解,這個註解的做用是用來通知 AppJoint 框架在 AppRouterAppRouterImpl 之間創建聯繫,這樣其它模塊就能夠經過 AppJoint 找到一個 AppRouter 的實例並調用裏面的方法了。

假設如今 module1 中須要調用 app 模塊中的 asyncMethod1OfApp 方法,因爲 app 模塊已經把這個方法聲明在了 router 模塊的 AppRouter 接口中了,module1 因爲也依賴了 router 模塊,因此 module1 內能夠訪問到 AppRouter 這個接口,可是卻訪問不到 AppRouterImpl 這個實現類,由於這個類定義在 app 模塊內,這時候咱們可使用 AppJoint 來幫助 module1 獲取 AppRouter 的實例:

AppRouter appRouter = AppJoint.service(AppRouter.class);

// 得到同步調用的結果 
String syncResult = appRouter.syncMethodOfApp();
// 發起異步調用
appRouter.asyncMethod1OfApp()
        .subscribe((result) -> {
            // handle asyncResult
        });
// 發起異步調用
appRouter.asyncMethod2OfApp(new Callback<String>() {
    @Override
    public void onResult(String data) {
        // handle asyncResult
    }
});
複製代碼

在上面的代碼中,咱們能夠看到,除了第一步獲取 AppRouter 接口的實例咱們用到了 AppJoint 的 API AppJoint.service 之外,剩下的代碼,module1 調用 app 模塊內的方法的方式,和咱們原來的開發方式沒有任何區別。AppJoint.service 就是 AppJoint 全部 API 裏惟一的那個方法。

也就是說,若是一個模塊須要提供方法供其餘模塊調用,須要作如下步驟:

  • 把接口聲明在 router 模塊中
  • 在本身模塊內部實現上一步中聲明的接口,同時在實現類上標記 @ServiceProvider 註解

完成這兩步之後就能夠在其它模塊中使用如下方式獲取該模塊聲明的接口的實例,並調用裏面的方法:

AppRouter appRouter = AppJoint.service(AppRouter.class);
Module1Router module1Router = AppJoint.service(Module1Router.class);
Module2Router module2Router = AppJoint.service(Module2Router.class);
複製代碼

這種方法不只僅能夠保證處於相同依賴層次的業務模塊能夠互相調用彼此的方法,還能夠支持從業務模塊中調用 app 模塊內的方法。這樣就能夠 保證咱們組件化的過程能夠是漸進的 ,咱們不須要一口氣把 app 模塊中的全部功能所有拆分到各個業務模塊中,咱們能夠逐漸地把功能拆分出來,以保證咱們的業務迭代和組件化改造同時進行。當咱們的 AppRouter 裏面的方法愈來愈少直到最後能夠把這個類從項目中安全刪除的時候,咱們的組件化改造就完成了。

模塊獨立編譯運行模式下跨模塊方法的調用

上面一個小結中咱們已經介紹了使用 AppJoint 在 App 全量編譯運行期間,業務模塊之間跨模塊方法調用的解決方案。在全量編譯期間,咱們能夠經過 AppJoint.service 這個方法找到指定模塊提供的接口的實例,可是在模塊單獨編譯運行期間,其它的模塊是不參與編譯的,它們的代碼也不會打包進用於模塊獨立運行的 standalaone 模塊,咱們如何解決在模塊單獨編譯運行模式下,跨模塊調用的代碼依然有效呢?

module1 爲例,首先爲了便於在 module1 內部任何地方均可以調用其它模塊的方法,咱們建立一個 RouterServices 類用於存放其它模塊的接口的實例:

public class RouterServices {
    // app 模塊對外暴露的接口
    public static AppRouter sAppRouter = AppJoint.service(AppRouter.class);
    // module2 模塊對外暴露的接口
    public static Module2Router sModule2Router = AppJoint.service(Module2Router.class);
}
複製代碼

有了這個類之後,咱們在 module1 內部若是須要調用其它模塊的功能,咱們只須要使用 RouterServices.sAppRouterRouterServices.sModule2Router 這兩個對象就能夠了。可是若是是剛剛提到的 module1 獨立編譯運行的狀況,即啓動的 application 模塊是 module1Standalone, 那麼 RouterServices.sAppRouterRouterServices.sModule2Router 這兩個對象的值均爲 null ,這是由於 appmodule2 這兩個模塊此時是沒有被編譯進來的。

若是咱們須要在這種狀況下保證已有的 module1 內部的經過 RouterServices.sAppRouterRouterServices.sModule2Router 進行跨模塊方法調用的代碼依然能工做,咱們就須要對這兩個引用手動賦值,即咱們須要建立 Mock 了 AppRouterModule2Router 功能的類。這些類因爲只對 module1 的獨立編譯運行有意義,因此這些類最合適的位置是放在 module1Standalone 這個模塊內,以 AppRouter 的 Mock 類 AppRouterMock 爲例:

public class AppRouterMock implements AppRouter {
    @Override
    public String syncMethodOfApp() {
        return "mockSyncMethodOfApp";
    }

    @Override
    public Observable<String> asyncMethod1OfApp() {
        return Observable.just("mockAsyncMethod1OfApp");
    }

    @Override
    public void asyncMethod2OfApp(final Callback<String> callback) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                callback.onResult("mockAsyncMethod2Result");
            }
        }).start();
    }
}
複製代碼

已經建立好了 Mock 類,接下來咱們要作的是,在 module1 獨立編譯運行的模式下,用 Mock 類的對象,去替換 RouterServices 裏面的對應的引用,因爲這些邏輯只和 module1 的獨立編譯運行有關,咱們不但願這些邏輯被打包進真正 Release 的 App 中,那麼最合適的地方就是 Module1StandaloneApplication裏了:

public class Module1StandaloneApplication extends Module1Application {

    @Override
    public void onCreate() {
        // module1 init inside super.onCreate()
        super.onCreate();
        // initialization only used for running module1 standalone
        Log.i("module1Standalone", "module1Standalone init is called");

        // Replace instances inside RouterServices
        RouterServices.sAppRouter = new AppRouterMock();
        RouterServices.sModule2Router = new Module2RouterMock();
    }
}
複製代碼

有了上面的初始化動做之後,咱們就能夠在 module1 內部安全地使用 RouterServices.sAppRouterRouterServices.sModule2Router 這兩個對象進行跨模塊的方法調用了,不管當前是處於 App 全量編譯模式仍是 modul1Standalone 獨立編譯運行模式。

跨模塊啓動 Activity 和 Fragment

在組件化改造過程當中,除了跨模塊的方法調用以外,跨模塊啓動 Activity 和跨模塊引用 Fragment 也是咱們常常遇到的需求。目前社區中大多數組件化方案都是使用自定義私有協議,使用 URL-Scheme 的方式來實現跨模塊 Activity 的啓動,這一塊已經有不少成熟的方案了,有的組件化方案直接推薦使用 ARouter 來實現這塊功能。可是 AppJoint 沒有使用這類方案

本文開頭曾經介紹過,AppJoint 全部的 API 只包含 3 個註解加 1 個方法,而這些 API 咱們在前文中已經都介紹完了,也就是說,咱們沒有提供專門的 API 來實現跨模塊的 Activity / Fragment 調用

咱們回想一下,在沒有實現組件化時,咱們啓動 Activity 的推薦寫法以下,首先在被啓動的 Activity 內實現一個靜態 start 方法:

public class MyActivity extends AppCompatActivity {

    public static void start(Context context, String param1, Integer param2) {
        Intent intent = new Intent(context, MyActivity.class);  
        intent.putExtra("param1", param1);  
        intent.putExtra("param2", param2);  
        context.startActivity(intent);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
    }
}
複製代碼

而後咱們若是在其它 Activity 中啓動這個 MyActivity 的話,寫法以下:

MyActivity.start(param1, param2);
複製代碼

這裏的思想是,服務的提供者應該把複雜的邏輯放在本身這裏,而只提供給調用者一個簡單的接口,用這個簡單的接口隔離具體實現的複雜性,這是符合軟件工程思想的。

那麼若是目前 module1 模塊中有一個 Module1Activity,如今這個 Activity 但願可以從 module2 啓動,應該如何寫呢?首先,在 router 模塊的 Module1Router 內聲明啓動 Module1Activity 的方法:

public interface Module1Router {
    ...
    // 啓動 Module1Activity
    void startModule1Activity(Context context);
}
複製代碼

而後在 module1 模塊裏 Module1Router 對應的實現類 Module1RouterImpl 中實現剛剛定義的方法:

@ServiceProvider
public class Module1RouterImpl implements Module1Router {

    ...

    @Override
    public void startModule1Activity(Context context) {
        Intent intent = new Intent(context, Module1Activity.class);
        context.startActivity(intent);
    }
}
複製代碼

這樣, module2 中就能夠經過下面的方式啓動 module1 中的 Module1Activity 了。

RouterServices.sModule1Router.startModule1Activity(context);
複製代碼

跨模塊獲取 Fragment 實例也是相似的方法,咱們在 Module1Router 裏繼續聲明方法:

public interface Module1Router {
    ...
    // 啓動 Module1Activity
    void startModule1Activity(Context context);

    // 獲取 Module1Fragment
    Fragment obtainModule1Fragment();
}
複製代碼

差很少的寫法,咱們只要在 Module1RouterImpl 裏接着實現方法便可:

@ServiceProvider
public class Module1RouterImpl implements Module1Router {
    @Override
    public void startModule1Activity(Context context) {
        Intent intent = new Intent(context, Module1Activity.class);
        context.startActivity(intent);
    }

    @Override
    public Fragment obtainModule1Fragment() {
        Fragment fragment = new Module1Fragment();
        Bundle bundle = new Bundle();
        bundle.putString("param1", "value1");
        bundle.putString("param2", "value2");
        fragment.setArguments(bundle);
        return fragment;
    }
}
複製代碼

前面提到過,目前社區大多數組件化方案都是使用 自定義私有協議,利用 URL-Scheme 的方式來實現跨模塊頁面跳轉 的,即相似 ARouter 的那種方案,爲何 AppJoint 不採用這種方案呢?

緣由其實很簡單,假設項目中沒有組件化的需求,咱們在同一個模塊內進行 Activity 的跳轉,確定不會採用 URL-Scheme 方式進行跳轉,咱們確定是本身建立 Intent 進行跳轉的。其實說到底,使用 URL-Scheme 進行跳轉是 不得已而爲之,它只是手段,不是目的,由於在組件化以後,模塊之間彼此的 Activity 變得不可見了,因此咱們轉而使用 URL-Scheme 的方式進行跳轉。

如今 AppJoint 從新支持了使用代碼進行跳轉,只須要把跳轉的邏輯抽象爲接口中的方法暴露給其它模塊,其它模塊就能夠調用這個方法實現跳轉邏輯。除此之外,使用接口提供跳轉邏輯相比 URL-Scheme 方式還有什麼優點呢?

  1. 類型安全。充分利用 Java 這種靜態類型語言的編譯器檢查功能,經過接口暴露的跳轉方法,不管是傳參仍是返回值,若是類型錯誤,在編譯期間就能發現錯誤,而使用 URL-Scheme 進行跳轉,若是發生類型上的錯誤,只能在運行期間才能發現錯誤。

  2. 效率高。即便是使用 URL-Scheme 進行跳轉,底層仍然是構造 Intent 進行跳轉,可是卻額外引入了對跳轉 URL 進行構造和解析的過程,涉及到額外的序列化和反序列化邏輯,下降了代碼的執行效率。而使用接口提供的跳轉邏輯,咱們直接構造 Intent 進行跳轉,不涉及到任何額外的序列化和反序列化操做,和咱們平常的 Activity 跳轉邏輯執行效率相同。

  3. IDE 友好。使用 URL-Scheme 進行跳轉,IDE 沒法提供任何智能提示,只能依靠完善的文檔或者開發者自身檢查來確保跳轉邏輯的正確性,而經過接口提供跳轉邏輯能夠最大限度發揮 IDE 的智能提示功能,確保咱們的跳轉邏輯是正確的。

  4. 易於重構。使用 URL-Scheme 進行跳轉,若是遇到跳轉邏輯須要重構的狀況,例如 Activity 名字的修改,參數名稱的修改,參數數量的增刪,只能依靠開發者對使用到跳轉邏輯的地方一個一個修改,並且沒法確保所有都修改正確了,由於編譯器沒法幫咱們檢查。而經過接口提供的跳轉邏輯代碼須要重構時,編譯器能夠自動幫助咱們檢查,一旦有地方沒有改對,直接在編譯期報錯,並且 IDE 都提供了智能重構的功能,咱們能夠方便地對接口中定義的方法進行重構。

  5. 學習成本低。咱們能夠沿用咱們熟悉的開發方式,不須要去學習 URL-Scheme 跳轉框架的 API。這樣還能夠保證咱們的跳轉邏輯不與具體的框架強綁定,咱們經過接口隔離了跳轉邏輯的真正實現,即便使用 AppJoint 進行跳轉,咱們也能夠在隨時把跳轉邏輯切換到其餘方案,包括 URL-Scheme 方式。

我我的的實踐,目前項目中同一進程內的頁面跳轉已經所有由 AppJoint 的方式實現,目前只有跨進程的頁面啓動交給了 URL-Scheme 這種方式(例如從瀏覽器喚醒 App 某個頁面)。

最後再提一點,因爲跨模塊啓動 Activity 沿用了跨模塊方法調用的開發方式,在業務模塊單獨編譯運行模式下,咱們也須要 Mock 這些啓動方法。既然咱們是在獨立調試某個業務模塊,咱們確定不是真的但願跳轉到那些頁面,咱們在 Mock 方法裏直接輸出 Log 或者 Toast 便可。

如今就開始組件化

到這裏爲止,使用 AppJoint 進行組件化的介紹就已經結束了。AppJoint 的 Github 地址爲:github.com/PrototypeZ/… 。核心代碼不超過 500 行,您徹底能夠快速掌握這個工具加速您的組件化開發,只要 Fork 一份代碼便可。若是您不想本身引入工程,咱們也提供了一個開箱即用的版本,您能夠直接經過 Gradle 引入。

  1. 在項目根目錄的 build.gradle 文件中添加 AppJoint插件 依賴:
buildscript {
    ...
    dependencies {
        ...
        classpath 'io.github.prototypez:app-joint:{latest_version}'
    }
}
複製代碼
  1. 在主 App 模塊和每一個組件化的模塊添加 AppJoint 依賴:
dependencies {
    ...
    implementation "io.github.prototypez:app-joint-core:{latest_version}"
}
複製代碼
  1. 在主 App 模塊應用 AppJoint插件
apply plugin: 'com.android.application'
apply plugin: 'app-joint'
複製代碼

寫在最後

經過本文的介紹,咱們其實能夠發現 AppJoint 是個思想很簡單的組件化方案。雖然簡單,可是卻直接並且夠用,儘管沒有像其它的組件化方案那樣提供了各類各樣強大的 API,可是卻足以勝任大多數中小型項目,這是咱們一以貫之的設計理念。

若是您感受這個項目對您有幫助,但願能夠點一個 Star ,謝謝 : ) 。文章很長,感謝您耐心讀完。因爲本人能力有限,文章可能存在紕漏的地方,歡迎各位指正,再次謝謝你們!


若是您對個人技術分享感興趣,歡迎關注個人我的公衆號:麻瓜日記,不按期更新原創技術分享,謝謝!:)

相關文章
相關標籤/搜索