Android開發組件化架構設計原理到實戰

為什麼需要組件化

小項目是不需要組件化的。當一個項目有數十個人開發,編譯項目要花費10分鐘,修改一個bug就可能會影響到其他業務,小小的改動就需要進行回歸測試,如果是這種項目,那麼我們需要進行組件化瞭

組件化和模塊化

在技術架構演進的過程一定是先出現模塊化後出現組件化,因為組件化就是解決瞭模塊化的問題。

模塊化架構

創建一個 Project 後可以創建多個 Module,這個 Module 就是所謂的模塊。一個簡單的例子,可能在寫代碼的時候我們會把首頁、消息、我的模塊拆開,每個 tab 所包含的內容就是一個模塊,這樣可以減少 module 的代碼量,但是每個模塊之間的肯定是有頁面的跳轉,數據傳遞等,比如 A 模塊需要 B 模塊的數據,於是我們會在 A 模塊的 gradle 文件內通過 implementation project(':B')依賴 B 模塊,但是 B 模塊又需要跳轉到 A 模塊的某個頁面,於是 B 模塊又依賴瞭 A 模塊。這樣的開發模式依然沒有解耦,改一個bug依然會改動很多模塊,並不能解決大型項目的問題。

如下圖所示,一開始我們定義的module之間並沒有過多耦合:

然後,隨著項目的不斷迭代,相互調用的情況會增多,也會增加一些庫的擴展和調用,工程的架構可能變為:

可以看出,各種業務之間的耦合非常嚴重,導致代碼非常難以維護,更難以測試,擴展和維護性非常差,這樣的架構肯定會被替代。

隨著時間的推移,出現瞭組件化、插件化等組織架構。

組件化架構

這裡先提幾個概念,我們日常業務需求開發的組件叫做業務組件,如果這個業務需求是可以被普遍復用的,那麼叫做業務基礎組件,譬如圖片加載、網絡請求等框架組件我們稱為基礎組件。搭建所有組件的app組件稱為殼組件/工程。接下來看一張架構圖:

實線表示直接依賴關系,虛線表示間接依賴。比如殼工程肯定是要依賴業務基礎組件、業務組件、module_common公共庫的。業務組件依賴業務基礎組件,但並不是直接依賴,而是通過”下沉接口“來實現間接調用。業務組件之間的依賴也是間接依賴。最後common組件依賴所有需要的基礎組件,common也屬於基礎組件,它隻是統一瞭基礎組件的版本,同時也提供瞭給應用提供一些抽象基類,比如BaseActivity、BaseFragment,基礎組件初始化等。

組件化帶來的優勢

  • 加快編譯速度:每個業務組件都可以單獨運行調試,速度提升好幾倍。舉個例子:video組件單獨編譯運行時間為3s,因為此時AS隻會運行video組件以及video組件依賴的組件的task,而如果集成編譯時間為10s,app所引用的所有的組件的task都會執行。可見,效率提升瞭3倍。

  • 提高協作效率:每個組件都有專人維護,不用關心其他組件是怎麼實現的,隻需要暴露對方需要的數據。測試也不需要整個回歸,隻需要重點測試修改的組件即可。

  • 功能重用:一次編碼處處復用,再也不需要瞭。尤其是基礎組件和業務基礎組件,基本上調用者根據文檔就可以一鍵集成和使用。

  • 確保瞭整體技術方案的統一性,為未來插件化公用一套底層模型做準備。

前面有提到非大型項目一般不會進行組件化,但是就像上面提到的功能重用,這個優勢並不是隻能用到大型項目 。我們可以在寫需求或庫時完全可以擁有組件化思想,把它們單獨寫成一個基礎組件或業務基礎組件。當第二個項目來的時候正好也需要這個組件,那我們就省去瞭拆出這個組件的時間(因為寫需求的時候很可能會造成大量耦合,後續拆分要花費時間),比如登錄組件,分享組件等等都是可以在一開始就寫成組件的。

組件化需解決的問題

  • 如何解決資源配置沖突問題?

  • 業務組件如何實現單獨調試?

  • 業務組件間沒有依賴,如何實現頁面跳轉?

  • 業務組件間沒有依賴,如何實現數據通信?

  • 業務組件間沒有依賴,如何實現消息通信

  • 殼工程Application生命周期如何下發?

資源沖突解決

AndroidManifest

每個module都有一份AndroidManifest文件來記載信息,最終生成一個App的時候,隻會有一份AndroidManifest來知道APP應該去如何配置,Manifest Merge Tools 會將多個AndroidManifest合成一個,但是又沖突需要解決。

在build/intermediates/manifest_merge_blame_file下會生成一份合並報告

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
   package="com.liang.mosic"
   android:versionCode="1"
   android:versionName="1.0" >
   <uses-sdk
       android:minSdkVersion="21"
->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml
       android:targetSdkVersion="30" />
->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml
    <uses-permission android:name="android.permission.INTERNET" />
-->[:b] C:\Users\Administrator\Desktop\mosic-master\mosic-master\b\build\intermediates\library_manifest\debug\AndroidManifest.xml:12:5-67
-->[:b] C:\Users\Administrator\Desktop\mosic-master\mosic-master\b\build\intermediates\library_manifest\debug\AndroidManifest.xml:12:22-64
    <application
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:6:5-48:19
        android:name="com.liang.mosic.ModuleApplication"
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:10:9-42
        android:allowBackup="true"
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:7:9-35
        android:appComponentFactory="androidx.core.app.CoreComponentFactory"
-->[androidx.core:core:1.5.0] C:\Users\Administrator.gradle\caches\transforms-2\files-2.1\4c9b62de2468f1520f5d232befb24ab8\core-1.5.0\AndroidManifest.xml:24:18-86
        android:debuggable="true"
        android:icon="@mipmap/ic_launcher"
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:8:9-43
        android:label="@string/app_name"
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:9:9-41
        android:supportsRtl="true"
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:11:9-35
        android:testOnly="true"
        android:theme="@style/AppTheme" >
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:13:9-40
        <!-- <activity android:name=".MainActivity"> -->
        <!-- <intent-filter> -->
        <!-- <action android:name="android.intent.action.MAIN" /> -->
        <!-- <category android:name="android.intent.category.LAUNCHER" /> -->
        <!-- </intent-filter> -->
        <!-- </activity> -->
        <activity
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:21:9-28:20
            android:name="com.liang.mosic.AdaviceActivity"
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:21:19-50
            android:theme="@style/AppWelcome" >
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:22:13-46
            <intent-filter>
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:23:13-27:29
                <action android:name="android.intent.action.MAIN" />
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:24:17-69
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:24:25-66
                <category android:name="android.intent.category.LAUNCHER" />
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:26:17-77
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:26:27-74
            </intent-filter>
        </activity>
        <activity android:name="com.liang.mosic.ModuleMainActivity" >
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:29:9-40:20
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:29:19-53
            <!-- <intent-filter> -->
            <!-- <action android:name="android.intent.action.MAIN" /> -->
            <!-- <category android:name="android.intent.category.LAUNCHER" /> -->
            <!-- </intent-filter> -->
            <intent-filter>
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:35:13-39:29
                <action android:name="com.liang.main" />
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:36:17-60
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:36:25-57
                <category android:name="android.intent.category.DEFAULT" />
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:38:17-76
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:38:27-73
            </intent-filter>
        </activity>
        <activity android:name="com.liang.mosic.ModuleExampleActivity" >
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:41:9-47:20
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:41:19-56
            <intent-filter>
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:42:13-46:29
                <action android:name="com.liang.moduleFragment" />
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:43:17-70
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:43:25-67
                <category android:name="android.intent.category.DEFAULT" />
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:38:17-76
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:38:27-73
            </intent-filter>
        </activity>
        <activity android:name="com.liang.a.MainActivity" >
-->[:a] C:\Users\Administrator\Desktop\mosic-master\mosic-master\a\build\intermediates\library_manifest\debug\AndroidManifest.xml:17:9-23:20
-->[:a] C:\Users\Administrator\Desktop\mosic-master\mosic-master\a\build\intermediates\library_manifest\debug\AndroidManifest.xml:17:19-61
            <intent-filter>
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:23:13-27:29
                <action android:name="android.intent.action.MAIN" />
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:24:17-69
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:24:25-66
                <category android:name="android.intent.category.LAUNCHER" />
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:26:17-77
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:26:27-74
            </intent-filter>
        </activity>
        <activity android:name="com.liang.b.BActivity" >
-->[:b] C:\Users\Administrator\Desktop\mosic-master\mosic-master\b\build\intermediates\library_manifest\debug\AndroidManifest.xml:21:9-27:20
-->[:b] C:\Users\Administrator\Desktop\mosic-master\mosic-master\b\build\intermediates\library_manifest\debug\AndroidManifest.xml:21:19-58
            <intent-filter>
-->[:b] C:\Users\Administrator\Desktop\mosic-master\mosic-master\b\build\intermediates\library_manifest\debug\AndroidManifest.xml:22:13-26:29
                <action android:name="com.liang.b.act" />
-->[:b] C:\Users\Administrator\Desktop\mosic-master\mosic-master\b\build\intermediates\library_manifest\debug\AndroidManifest.xml:23:17-61
-->[:b] C:\Users\Administrator\Desktop\mosic-master\mosic-master\b\build\intermediates\library_manifest\debug\AndroidManifest.xml:23:25-58
                <category android:name="android.intent.category.DEFAULT" />
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:38:17-76
-->C:\Users\Administrator\Desktop\mosic-master\mosic-master\app\src\main\AndroidManifest.xml:38:27-73
            </intent-filter>
        </activity>
    </application>
</manifest>

最終編譯出的app會將其他所有module的AndroidManifest文件合並,合並規則為:

  • 如果功能module有Application,主module沒有聲明,則使用功能module的Application;
  • 如果主module有定義Application,其他module沒有,則使用主module的;
  • 如果功能module有多個自定義Application,解決沖突後使用;
  • 如果主module有Application,功能module也有,則解決沖突後,最後編譯的主module的Application會在AndroidManifest中。

如果子module中聲明icon、theme等屬性,會導致合並沖突,需要申明屬性:

xmlns:tools="http://schemas.android.com/tools"
tools:replace="android:icon,android:theme"

權限申明:

在子module中申明的權限,會集成到主module中,四大組件也是相同的規則。shareUid隻有在主module中申明,才會打包到最終的AndroidManifest中。

獨立調試

單工程方案

所謂的單工程方案就是把所有組件都放到一個工程下,先看一下整體的目錄:

ps

module_ 開頭表示基礎組件,

fun_ 前綴表示業務基礎組件,

biz_前綴表示業務組件,

export_前綴表示業務組件暴露接口。

單工程利弊分析:

  • 利:一個模塊修改後隻需要編譯一下,依賴它的其他模塊就能馬上感知到變化。
  • 弊:沒能做到完全的職責拆分,每個模塊的開發者都有修改其他模塊的權限。

首先在 gradle.properties 文件內聲明一個變量:

// gradle.properties
isModule = true

isModule 為 true 時表示組件可以作為 apk 運行起來,false 表示組件隻能作為 library。我們根據需要改變這個值後同步下gradle即可。

然後在某個 module 的 build.gradle 文件內用這個變量做三個地方的判斷:

// build.gradle
// 區分是應用還是庫
if(isModule.toBoolean()) {
	apply plugin: 'com.android.application'
}else {
	apply plugin: 'com.android.library'
}
android {
	defaultConfig {
		// 如果是應用需要指定application
		if(isModule.toBoolean()) {
			applicationId "com.xxx.xxx"
		}
	}
	sourceSets {
		main {
			// 應用和庫的AndroidManifest文件區分
			if(isModule.toBoolean()) {
				manifest.srcFile 'src/main/debug/AndroidManifest.xml'
			}else {
				manifest.srcFile 'src/main/AndroidManifest.xml'	
			}
		}
	}
}

由於library是不需要 Application 和啟動Activity頁,所以我們要區分這個文件,應用manifest指定的路徑沒有特定,隨意找個路徑創建即可。在應用AndroidManifest.xml裡我們要設置啟動頁:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.sun.biz_home">
    <application
        android:allowBackup="true"
        android:label="@string/home_app_name"
        android:supportsRtl="true"
        android:theme="@style/home_AppTheme">
        <activity android:name=".debug.HomeActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>

library 的 AndroidManifest.xml 不需要這些:

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.sun.biz_home">
</manifest>

gradle 依賴 module 的方式主要有兩種:

  • implementation: A implementation B,B implementation C, 但 A 不能訪問到 C 的東西。
  • api:A api B,B api C,A能訪問到C的東西。

一般來說我們隻需要使用 implementation 即可,api 是會造成項目編譯時間變長,而且會引入該模塊不需要的功能,代碼之間耦合變得嚴重瞭。不過 module_common 是統一瞭基礎組件版本的公共庫,所有組件都應需要依賴它並擁有基礎組件的能力,所以基本每個業務組件和業務基礎組件都應該依賴公共庫:

dependencies {
	implementation project(':module_common')
}

而 common 組件依賴基礎組件應該是用 api,因為把基礎組件的能力傳遞給上層業務組件:

dependencies {
	api project(':module_base')
	api project(':module_util')
}

多工程方案

多工程就是每個組件都是一個工程,例如創建一個工程後 app 作為殼組件,它依賴 biz_home 運行,因此不需要 isModule 來控制獨立調試,它本身就是一個工程可以獨立調試。

多工程的利弊就是和單工程相反的:

  • 利:做到職責完全拆分,其他項目復用更加方便,直接一行依賴引入。
  • 弊:修改後需要上傳到maven倉庫,其他工程再次編譯後才能感知到變化,多瞭上傳和編譯的時間。

多工程組件依賴需要用到maven倉庫。把每個組件的aar上傳到公司內網的maven倉庫,然後像這樣去依賴:

implementation 'com.xxx.xxx:module_common:1.0.0'

我們把三方庫統一放到 config.gradle 內管理:

ext {
    dependencies = [
            "glide": "com.github.bumptech.glide:glide:4.12.0",
            "glide-compiler": "com.github.bumptech.glide:compiler:4.12.0",
            "okhttp3": "com.squareup.okhttp3:okhttp:4.9.0",
            "retrofit": "com.squareup.retrofit2:retrofit:2.9.0",
            "retrofit-converter-gson"  : "com.squareup.retrofit2:converter-gson:2.9.0",
            "retrofit-adapter-rxjava2" : "com.squareup.retrofit2:adapter-rxjava2:2.9.0",
            "rxjava2": "io.reactivex.rxjava2:rxjava:2.2.21",
            "arouter": "com.alibaba:arouter-api:1.5.1",
            "arouter-compiler": "com.alibaba:arouter-compiler:1.5.1",
            // our lib
            "module_util": "com.sun.module:module_util:1.0.0",
            "module_common": "com.sun.module:module_common:1.0.0",
            "module_base": "com.sun.module:module_base:1.0.0",
            "fun_splash": "com.sun.fun:fun_splash:1.0.0",
            "fun_share": "com.sun.fun:fun_share:1.0.0",
            "export_biz_home": "com.sun.export:export_biz_home:1.0.0",
            "export_biz_me": "com.sun.export:export_biz_me:1.0.0",
            "export_biz_msg": "com.sun.export:export_biz_msg:1.0.0",
            "biz_home": "com.sun.biz:biz_home:1.0.0",
            "biz_me": "com.sun.biz:biz_me:1.0.0",
            "biz_msg": "com.sun.biz:biz_msg:1.0.0"
    ]
}

這樣方便版本統一管理, 然後在根目錄的 build.gradle 內導入:

apply from: 'config.gradle'

最後在各自的模塊引入依賴,比如在 module_common 中這麼引入依賴即可。

dependencies {
	api rootProject.ext.dependencies["arouter"]
  kapt rootProject.ext.dependencies["arouter-compiler"]
  api rootProject.ext.dependencies["glide"]
  api rootProject.ext.dependencies["okhttp3"]
  api rootProject.ext.dependencies["retrofit"]
  api rootProject.ext.dependencies["retrofit-converter-gson"]
  api rootProject.ext.dependencies["retrofit-adapter-rxjava2"]
  api rootProject.ext.dependencies["rxjava2"]
  api rootProject.ext.dependencies["module_util"]
  api rootProject.ext.dependencies["module_base"]
}

個人覺得多工程適合"很大"的工程,每個業務組件可能都需要一個組開發,類似淘寶這樣的app。但這隻是針對業務組件來說的,業務基礎組件和基礎組件修改的頻率不會很大,最好都是單工程上傳至maven倉庫來使用。本文的例子是為瞭方便所以把所有組件寫到一起瞭,最好的方式就是把 fun_ 和 module_ 開頭的組件都拆分成單工程獨立開發,業務組件寫到一個工程內。

頁面跳轉

做完組件之間的隔離後,暴露出來最明顯的問題就是頁面跳轉和數據通信的問題。一般來說,頁面跳轉都是顯示startActivity跳轉,在組件化項目內就不適用瞭,隱式跳轉可以用,但每個Activity都要寫 intent-filter 就顯得有點麻煩,如下所示:

<activity android:name="com.liang.lib_video.videoplayer.VideoActivity">
    <intent-filter>
        <action android:name="com.intent.openVideoActivity"></action>
        <category android:name="android.intent.category.DEFAULT" />
    </intent-filter>
</activity>
Intent intent = new Intent();
intent.setClass("包名","Activity路徑");
intent.setComponent(new ComponentName("包名"));
startActivity(intent);

使用上述方式跳轉會奔潰,提示在AndroidManifest文件中註冊,這裡需要註意的是,setClass/setComponent是APP的包名而不是所在module的包名。 可以參考最終生成的AndroidManifest文件。使用隱式跳轉也可以先用resolveActivity進行驗證。 如果不想要其他應用通過隱式打開,需要設置exported=false。

隱式跳轉是整個系統都能接收到,會相對想好性能,所以最好的方式還是用路由框架。

實際上市面已經有比較成熟的路由框架專門就是為瞭組件化而生的,比如美團的WMRouter,阿裡的ARouter等,本例使用 ARouter 框架,看下ARouter頁面跳轉的基本操作。

首先肯定是引入依賴,以 module_common 引入ARouter舉例,build.gradle 應該添加:

android {
	defaultConfig {
		javaCompileOptions {
       annotationProcessorOptions {
          arguments = [AROUTER_MODULE_NAME: project.getName()]
     	 }
    }
	}
	compileOptions {
      sourceCompatibility JavaVersion.VERSION_1_8
      targetCompatibility JavaVersion.VERSION_1_8
  }
}
dependencies {
	api rootProject.ext.dependencies["arouter"]
  kapt rootProject.ext.dependencies["arouter-compiler"]
}

kapt註解依賴沒有辦法傳遞,所以我們不可避免得需要在每個模塊都聲明這些配置,除瞭 api rootProject.ext.dependencies["arouter"] 這行。然後需要全局註冊 ARouter,我是在 module_common 統一註冊的。

class AppCommon: BaseApp{
    override fun onCreate(application: Application) {
        MLog.d(TAG, "BaseApp AppCommon init")
        initARouter(application)
    }
    private fun initARouter(application: Application) {
        if(BuildConfig.DEBUG) {
            ARouter.openLog()
            ARouter.openDebug()
        }
        ARouter.init(application)
    }
}

接著我們在 module_common 模塊內聲明一個路由表用作統一管理路徑。

// RouterPath.kt
class RouterPath {
    companion object {
        const val APP_MAIN = "/app/MainActivity"
        const val HOME_FRAGMENT = "/home/HomeFragment"
        const val MSG_FRAGMENT = "/msg/MsgFragment"
        const val ME_FRAGMENT = "/me/MeFragment"
        const val MSG_PROVIDER = "/msg/MsgProviderImpl"
    }
}

然後在MainActivity類文件上進行註解:

@Route(path = RouterPath.APP_MAIN)
class MainActivity : AppCompatActivity() {
}

任意模塊隻需要調用 ARouter.getInstance().build(RouterPath.APP_MAIN).navigation() 即可實現跳轉。如果我們要加上數據傳遞也很方便:

ARouter.getInstance().build(RouterPath.APP_MAIN)
            .withString("key", "value")
            .withObject("key1", obj)
            .navigation()

然後在MainActivity使用依賴註入接受數據:

class MainActivity : AppCompatActivity() {
    @Autowired
    String key = ""
}

Arouter 實現組件間方法調用

在 export_biz_msg 組件下聲明 IMsgProvider,此接口必須實現 IProvider 接口:

interface IMsgProvider: IProvider {
    fun onCountFromHome(count: Int = 1)
}

然後在 biz_msg 組件裡實現這個接口:

@Route(path = RouterPath.MSG_PROVIDER)
class MsgProviderImpl: IMsgProvider {
    override fun onCountFromHome(count: Int) {
    	  // 這裡隻是對數據進行分發,有監聽計數的對象會收到
        MsgCount.instance.addCount(count)
    }
    override fun init(context: Context?) {
        // 對象被初始化時調用
    }
}

在 biz_home 首頁組件中發送計數:

val provider = ARouter.getInstance().build(RouterPath.MSG_PROVIDER).navigation() as IMsgProvider
provider.onCountFromHome(count)

可以看到其實和頁面跳轉的方式基本雷同,包括獲取 Fragment 實例的方式也是這種。ARouter把所有通信的方式都用一種api實現,讓使用者上手非常容易。

組件化的消息通信方式選擇

廣播

作為Android中四大組件之一,Broadcast的職責是用於Android系統通信,但是普通的廣播是全局廣播,會造成安全泄露以及效率問題,如果隻是在應用內部通知,可以使用更為高效的LocalBroadCast,相對於全局廣播,本地廣播隻會在APp內部傳播,不會造成隱私泄露,同時無法接受其他應用發送的廣播,相對於全局廣播來說更加高效。

事件總線

由於系統級別的廣播傳遞比較耗時,消息通信科使用通過記錄對象、使用監聽者模式實現的事件總線框架,比如EventBus、LivaData等。

通過將消息的公用部分 ,如自定義消息的bean放入到baseMOdule下的單獨模塊來實現組件間消息的傳遞。組件化的數據庫存儲和消息通信的實現方式大同小異,都是將公用的東西放入到baseModule,如果內容比較多或者對於職責界限劃分要求高的話可在base下新建一個DataBaseModule

Application生命周期分發

當 app 殼工程啟動Application初始化時要通知到其他組件初始化一些功能。這裡提供一個簡單的方式。

首先我們在 module_common 公共庫內聲明一個接口 BaseApp:

public interface BaseAppInit {
    boolean onInitCreate(Application application);
    boolean onInitTerminal(Application application);
}

然後每個組件都要創建一個 App 類實現此接口,比如在某個業務組件:

public class AudioInit implements BaseAppInit {
    @Override
    public boolean onInitCreate(Application application) {
        return false;
    }
    @Override
    public boolean onInitTerminal(Application application) {
        return false;
    }
}

剩下最後一步就是從 app 殼工程分發 application 的生命周期瞭,這裡用到反射技術:

val moduleInitArr = arrayOf(
    "com.liang.lib_audio.app.AudioInit",
    "com.liang.lib_audio.app.VideoInit",
    "com.liang.lib_audio.app.LoginInit",
)
class App: Application() {
    override fun onCreate() {
        super.onCreate()
        initModuleApp(this)
    }
    private fun initModuleApp(application: Application) {
        try {
            for(appName in moduleInitArr) {
                val clazz = Class.forName(appName)
                val module = clazz.getConstructor().newInstance() as BaseApp
                module.onCreate(application)
            }
        }catch (e: Exception) {
            e.printStackTrace()
        }
    }
}

我們隻需要知道的每個實現 BaseApp 接口的類的全限定名並寫到moduleInitArr數組裡,然後通過反射獲取 Class 對象從而獲取構造函數創建實體對象,最後調用 BaseApp 的 onCreate 方法將 application 傳入,每個Application生命周期的方法都可以通過這種方式傳遞。由於反射會消耗一定的性能,這個操作可以放在子線程,然後線程間通信。 當然,在每個module定義相對應的初始化方法,然後主module 調用也可以實現初始化,此處使用反射是為瞭最大程度的解耦。

以上就是Android開發組件化架構設計原理到實戰的詳細內容,更多關於Android組件化架構設計的資料請關註WalkonNet其它相關文章!

推薦閱讀: