從零開始的Android新專案11 - 元件化實踐(1)

宅一帆markzhai發表於2016-10-26

這裡的元件化,指的是 MDCC 2016 上馮森林提出的《迴歸初心,從容器化到元件化》。

我個人一直是比較反感黑科技的,其中首當其衝的就是 外掛化 以及 保活。作為一個開發者,除了研究技術,提高自己以外,是否應該考慮些其他東西呢?尤其是我們這些嵌入式系統(客戶端)開發者,在依賴、受哺於系統生態下,是不是應該考慮一下,怎麼反哺?怎麼去更好地維護這個生態環境,而不是一味破壞、消耗它呢?

想一想那些黑科技帶來的。外掛化導致線上可以執行任何程式碼且不留下痕跡,使用者安全性和信任感何在?保活導致應用長時間不釋放,搶佔系統資源,讓使用者產生 Android 越用越卡的感覺。全家桶互相喚醒,確定不是逼著使用者刪除應用?至少我在 Android 手機上是不敢裝某些知名應用的。

Greenify —— 綠色守護 幫助我們解決了應用死不掉的問題。那其他的呢?作為一個 Android 開發者,我不敢在我的 Android 手機上裝一些應用 —— 支付寶、淘寶、閒魚(Web 上還不讓用)、天貓、京東、百度貼吧。有朋友找我推薦手機的時候,我從不會推薦 iPhone,但給他們推薦 Android 後,又會擔心他們能不能 hold 住國內生態下的 Android 手機。有一個買了 Sony Z5 的女孩子,當時問我為啥用電那麼快後,我實在無言以對。只能給她指導了一些姿勢和黑科技。

從零開始的Android新專案11 - 元件化實踐(1)
Conversation

幸而時至半年後的今天,她用得還挺順手,而 iOS10 也順利給自己抹黑了一把。

然而——
今天你在消耗這個生態,明天你就得為此承擔結果。

元件化是什麼

元件化,相對於容器化(外掛),是一種沒有黑科技的相互隔離的並行開發方式。為了瞭解元件化,不得不先說一下外掛化。

為什麼我們需要外掛化

現代 Android 開發中,往往會堆積很多的需求進專案,超過 65535 後,MultiDex、外掛化都是解決方案。但方法數不是引入外掛化的唯一原因,更多的時候,引入外掛化有另外幾個理由:

  • 滿足產品經理隨時上線的需求(注意,這在國外是命令禁止的,App store 和 Google Play 都不允許這種行為,支付寶因此被 Google Play 下架過,仔細想想,如果任何應用都能線上上替換原來的行為,審查還有什麼用?)。
  • 團隊比較有錢,願意養人做這個。技術人員覺得不做業務簡直太棒了,可以安心研究技術。
  • 並行開發,常見於複雜的各種東西往裡塞的大型應用,比如 —— 手Q、手空、手淘、支付寶、大眾點評、攜程等等。這些團隊的 Android 開發動輒是數百人,並分成好幾個業務組,如此要並行開發便需要解耦各個模組,避免互相依賴。而且程式碼一多吧,編譯也會很慢(我們公司現在的工程已經需要 5 - 6 分鐘了,手空使用 ant 都需要 5 分鐘,而 手Q 使用 ant 則需要 10 分鐘,改成 gradle 的話姑且乘個2,都是幾十分鐘的級別)。外掛化可以加快編譯速度,從而提高開發效率。

其實真正的理由就只有第三個(我相信業務技術人員也不會真的想無休止地發版本,除了一些分 架構組/業務組 的地方,架構組會不考慮業務組的感受)。在知乎上,小樑也有對此作出回答:怎麼將 Android 程式做成外掛化的形式?,建議去讀一下。

本篇裡不多說外掛化的工作原理,建議移步去別處學習,直接看原始碼也可以,像 atlas 這樣 Hook 構成的外掛框架可能閱讀起來會有些困難,其他還好。

外掛化的惡

躺不完的坑。
—— 即便是一些做了很多年的外掛化框架,依然在不斷躺坑,更何況是使用他們的開發者,簡直是花式中槍。

發不完的版本。
—— 什麼?趕不上?沒事,遲些可以單獨發版本。這回你可真是搬磚的碼農了。

這個在我的外掛裡是好的呀。
—— 在各自的殼裡執行很完美,然而整合後各種問題不斷,甚至一啟動就 ANR。

版本帶來的問題。
—— 因為要動態發版本,所以每個外掛自然需要有各種版本。什麼?那個不對?肯定是你引用的版本錯啦。更何況發版本本身就是個讓人很心累的事情。

等等等等,不贅述。垃圾外掛,還我青春。

元件化 VS 外掛化

元件化帶來的,是一個沒有黑科技的外掛化。應用了 Android 原有的技術棧以及 Gradle 的靈活性,失去的是動態發版本的能力,其他則做得比外掛化更好。因為沒有黑科技,所以不會有那麼多黑科技和各種 hook 導致的坑,以及為了規避它們必須小心翼翼遵守的開發規範,幾乎和沒有使用外掛化的 Android 開發一模一樣。

而我們需要關心的,只是如何做好隔離,如何更好地設計,以及提高開發效率與產品體驗。

Take Action

Gradle

元件化的基本就是通過 gradle 指令碼來做的。

通過在需要元件化的業務 module 中:

if (isDebug.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}複製程式碼

並在業務 module 中放一個 gradle.properties:

isDebug=false複製程式碼

如此,當我們設定 isDebug 為 true 時,則這個 module 將會作為 application module 編譯為 apk,否則 為 library module 編譯為 aar。

下面的 gradle 是我們的一個元件化業務 module 的完整 build.gralde:

println isDebug.toBoolean()

if (isDebug.toBoolean()) {
    apply plugin: 'com.android.application'
} else {
    apply plugin: 'com.android.library'
}

apply plugin: 'me.tatarka.retrolambda'
apply plugin: 'com.neenbedankt.android-apt'

android {
    compileSdkVersion rootProject.ext.compileSdkVersion
    buildToolsVersion rootProject.ext.buildToolsVersion

    defaultConfig {
        minSdkVersion rootProject.ext.minSdkVersion
        targetSdkVersion rootProject.ext.targetSdkVersion
        versionCode rootProject.ext.versionCode
        versionName rootProject.ext.versionName
        multiDexEnabled true

        if (isDebug.toBoolean()) {
            ndk {
                abiFilters "armeabi-v7a", "x86"
            }
        }
    }
    compileOptions {
        sourceCompatibility rootProject.ext.javaVersion
        targetCompatibility rootProject.ext.javaVersion
    }
    lintOptions {
        abortOnError rootProject.ext.abortOnLintError
        checkReleaseBuilds rootProject.ext.checkLintRelease
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    dataBinding {
        enabled = true
    }
    if (isDebug.toBoolean()) {
        splits {
            abi {
                enable true
                reset()
                include 'armeabi-v7a', 'x86'
                universalApk false
            }
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile project(':lib_stay_base')
    apt rootProject.ext.libGuava
    apt rootProject.ext.libDaggerCompiler
}複製程式碼

各位根據實際需要參考修改即可。

這裡另外提供一個小訣竅,為了對抗 Android Studio 的坑爹,比如有時候改了 gradle,sync 後仍然沒法直接通過 IDE 啟動 module app,可以修改 settings.gradle,比如:

include ':app'
include ':data'
include ':domain'
include ':module_setting'
include ':module_card'
include ':module_discovery'
include ':module_feed'
include ':lib_stay_base'
// 省略一堆 sdk 庫複製程式碼

可以把不需要的 module 都給先註釋了(只留下需要的 module,lib_base,以及 sdk),尤其是 app module。然後基本上就沒問題。

Manifest

一個很常見的需求就是,當我作為獨立業務執行的時候,manifest 會不同,比如會多些 activity(用來套的,或者測試除錯用的),或者 application 不同,總之會有些細微的差別。

一個簡單的做法是:

sourceSets {
    main {
        if (isDebug.toBoolean()) {
            manifest.srcFile 'src/debug/AndroidManifest.xml'
        } else {
            manifest.srcFile 'src/release/AndroidManifest.xml'
        }
    }
}複製程式碼

這樣在編譯時使用兩個 manifest,但是這樣一來,兩者就有很多重複的內容,會有維護、比較的成本。

我們可以利用自帶 flavor manifest merge,分別對應 debug/AndroidManifest.xml, main/AndroidManifest.xml, 以及 release/AndroidManifest.xml。

main 下的 manifest 寫通用的東西,另外 2 個分別寫各自獨立的,通常 release 的 manifest 只是一個空的 application 標籤,而 debug 的會有 application 和除錯用的 activity(你總得要有個啟動 activity 吧)及許可權。

這裡有一個小 tip,就是在 release 的 manifest 中,application 標籤下儘量不要放任何東西,只是佔個位,讓上面去 merge,否則比如一個 module supportsRtl 設定為了 true,另一個 module 設定為了 false,就不得不去做 override 了。

Wrapper

看一個 debug manifest 的例子:

<manifest package="com.amokie.stay.module.card"
          xmlns:android="http://schemas.android.com/apk/res/android">

    <application
        android:name="com.amokie.stay.base.BaseApplication"
        android:allowBackup="true"
        android:alwaysRetainTaskState="true"
        android:hardwareAccelerated="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:largeHeap="true"
        android:sharedUserId="com.amokie.stay"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">

        <activity android:name=".WrapActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>
                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>

    </application>

</manifest>複製程式碼

這裡的 WrapActivity 就是我們所謂的 wrapper 了。

因為入口頁可能是一個 fragment,所以就需要一個 activity 來包一下它,並作為啟動類。

Application

BaseApplication 繼承了 MultiDexApplication,而真正最後整合的 Application 則繼承自
BaseApplication,並新增了一些整合時需要做的事情(比如監控、埋點、Crash上報的初始化)。

但大部分的仍會放在 BaseApplication,比如圖片庫、React Native、Log 等。然後各個 Module 則直接使用 BaseApplication,免去各自去寫初始化的程式碼。

當然,如果一定想複雜化,也可以專門搞個 library module 做初始化,但我個人不建議過度複雜的設計。

可以先閱讀阿布的總結文章:專案元件化之遇到的坑,也感謝小樑拋磚引玉的 Demo

我這邊簡單也講一講。

Data Binding

見我上一篇寫到的記一次 Data Binding 在 library module 中遇到的大坑,簡單說起來就是 data binding 在 library module 的支援有一個 bug,就是不支援 get ViewModel 的方法,只能 set 進去,從而導致做好模組化的 module 在作為 application 可以獨立執行後,作為 library module 無法通過編譯。

另外碰到一個問題,就是時不時會有如下的報錯(出現在整合 application 的時候,且並不是必現):

10:26:29.622 [ERROR] [org.gradle.BuildExceptionReporter]
10:26:29.622 [ERROR] [org.gradle.BuildExceptionReporter] FAILURE: Build completed with 3 failures.
10:26:29.622 [ERROR] [org.gradle.BuildExceptionReporter]
10:26:29.622 [ERROR] [org.gradle.BuildExceptionReporter] 1: Task failed with an exception.
10:26:29.622 [ERROR] [org.gradle.BuildExceptionReporter] -----------
10:26:29.622 [ERROR] [org.gradle.BuildExceptionReporter] * What went wrong:
10:26:29.623 [ERROR] [org.gradle.BuildExceptionReporter] Execution failed for task ':module_user:dataBindingProcessLayoutsRelease'.
10:26:29.623 [ERROR] [org.gradle.BuildExceptionReporter] > -1
10:26:29.623 [ERROR] [org.gradle.BuildExceptionReporter]
10:26:29.623 [ERROR] [org.gradle.BuildExceptionReporter] * Exception is:
10:26:29.624 [ERROR] [org.gradle.BuildExceptionReporter] org.gradle.api.tasks.TaskExecutionException: Execution failed for task ':module_user:dataBindingProcessLayoutsRelease'.
10:26:29.624 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:69)
10:26:29.625 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.execute(ExecuteActionsTaskExecuter.java:46)
10:26:29.625 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.api.internal.tasks.execution.PostExecutionAnalysisTaskExecuter.execute(PostExecutionAnalysisTaskExecuter.java:35)
10:26:29.626 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.api.internal.tasks.execution.SkipUpToDateTaskExecuter.execute(SkipUpToDateTaskExecuter.java:66)
10:26:29.626 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.api.internal.tasks.execution.ValidatingTaskExecuter.execute(ValidatingTaskExecuter.java:58)
10:26:29.627 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.api.internal.tasks.execution.SkipEmptySourceFilesTaskExecuter.execute(SkipEmptySourceFilesTaskExecuter.java:52)
10:26:29.627 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.api.internal.tasks.execution.SkipTaskWithNoActionsExecuter.execute(SkipTaskWithNoActionsExecuter.java:52)
10:26:29.627 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.api.internal.tasks.execution.SkipOnlyIfTaskExecuter.execute(SkipOnlyIfTaskExecuter.java:53)
10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.api.internal.tasks.execution.ExecuteAtMostOnceTaskExecuter.execute(ExecuteAtMostOnceTaskExecuter.java:43)
10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:203)
10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter$EventFiringTaskWorker.execute(DefaultTaskGraphExecuter.java:185)
10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.processTask(AbstractTaskPlanExecutor.java:66)
10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.execution.taskgraph.AbstractTaskPlanExecutor$TaskExecutorWorker.run(AbstractTaskPlanExecutor.java:50)
10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.execution.taskgraph.ParallelTaskPlanExecutor.process(ParallelTaskPlanExecutor.java:47)
10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.execution.taskgraph.DefaultTaskGraphExecuter.execute(DefaultTaskGraphExecuter.java:110)
10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.execution.SelectedTaskExecutionAction.execute(SelectedTaskExecutionAction.java:37)
10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:37)
10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.execution.DefaultBuildExecuter.access$000(DefaultBuildExecuter.java:23)
10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.execution.DefaultBuildExecuter$1.proceed(DefaultBuildExecuter.java:43)
10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.execution.DryRunBuildExecutionAction.execute(DryRunBuildExecutionAction.java:32)
10:26:29.628 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:37)
10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.execution.DefaultBuildExecuter.execute(DefaultBuildExecuter.java:30)
10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.initialization.DefaultGradleLauncher$4.run(DefaultGradleLauncher.java:153)
10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.internal.Factories$1.create(Factories.java:22)
10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:91)
10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:53)
10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.initialization.DefaultGradleLauncher.doBuildStages(DefaultGradleLauncher.java:150)
10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.initialization.DefaultGradleLauncher.access$200(DefaultGradleLauncher.java:32)
10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.initialization.DefaultGradleLauncher$1.create(DefaultGradleLauncher.java:98)
10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.initialization.DefaultGradleLauncher$1.create(DefaultGradleLauncher.java:92)
10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:91)
10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.internal.progress.DefaultBuildOperationExecutor.run(DefaultBuildOperationExecutor.java:63)
10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.initialization.DefaultGradleLauncher.doBuild(DefaultGradleLauncher.java:92)
10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.initialization.DefaultGradleLauncher.run(DefaultGradleLauncher.java:83)
10:26:29.629 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.exec.InProcessBuildActionExecuter$DefaultBuildController.run(InProcessBuildActionExecuter.java:99)
10:26:29.630 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.tooling.internal.provider.ExecuteBuildActionRunner.run(ExecuteBuildActionRunner.java:28)
10:26:29.630 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.exec.ChainingBuildActionRunner.run(ChainingBuildActionRunner.java:35)
10:26:29.630 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:48)
10:26:29.630 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.exec.InProcessBuildActionExecuter.execute(InProcessBuildActionExecuter.java:30)
10:26:29.630 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.exec.ContinuousBuildActionExecuter.execute(ContinuousBuildActionExecuter.java:81)
10:26:29.630 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.exec.ContinuousBuildActionExecuter.execute(ContinuousBuildActionExecuter.java:46)
10:26:29.630 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.exec.ExecuteBuild.doBuild(ExecuteBuild.java:52)
10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)
10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.exec.WatchForDisconnection.execute(WatchForDisconnection.java:37)
10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.exec.ResetDeprecationLogger.execute(ResetDeprecationLogger.java:26)
10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.exec.RequestStopIfSingleUsedDaemon.execute(RequestStopIfSingleUsedDaemon.java:34)
10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.call(ForwardClientInput.java:74)
10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.exec.ForwardClientInput$2.call(ForwardClientInput.java:72)
10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.util.Swapper.swap(Swapper.java:38)
10:26:29.631 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.exec.ForwardClientInput.execute(ForwardClientInput.java:72)
10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.health.DaemonHealthTracker.execute(DaemonHealthTracker.java:47)
10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.exec.LogToClient.doBuild(LogToClient.java:60)
10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)
10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.exec.EstablishBuildEnvironment.doBuild(EstablishBuildEnvironment.java:72)
10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.exec.BuildCommandOnly.execute(BuildCommandOnly.java:36)
10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.health.HintGCAfterBuild.execute(HintGCAfterBuild.java:41)
10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.api.DaemonCommandExecution.proceed(DaemonCommandExecution.java:120)
10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.exec.StartBuildOrRespondWithBusy$1.run(StartBuildOrRespondWithBusy.java:50)
10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.launcher.daemon.server.DaemonStateCoordinator$1.run(DaemonStateCoordinator.java:237)
10:26:29.632 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.internal.concurrent.ExecutorPolicy$CatchAndRecordFailures.onExecute(ExecutorPolicy.java:54)
10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.internal.concurrent.StoppableExecutorImpl$1.run(StoppableExecutorImpl.java:40)
10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter] Caused by: java.lang.ArrayIndexOutOfBoundsException: -1
10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter]        at com.sun.xml.internal.bind.v2.util.CollisionCheckStack.pushNocheck(CollisionCheckStack.java:117)
10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter]        at com.sun.xml.internal.bind.v2.runtime.XMLSerializer.childAsRoot(XMLSerializer.java:472)
10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter]        at com.sun.xml.internal.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:308)
10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter]        at com.sun.xml.internal.bind.v2.runtime.MarshallerImpl.marshal(MarshallerImpl.java:236)
10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter]        at android.databinding.tool.store.ResourceBundle$LayoutFileBundle.toXML(ResourceBundle.java:629)
10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter]        at android.databinding.tool.LayoutXmlProcessor.writeXmlFile(LayoutXmlProcessor.java:252)
10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter]        at android.databinding.tool.LayoutXmlProcessor.writeLayoutInfoFiles(LayoutXmlProcessor.java:239)
10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter]        at com.android.build.gradle.internal.tasks.databinding.DataBindingProcessLayoutsTask.processResources(DataBindingProcessLayoutsTask.java:110)
10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.internal.reflect.JavaMethod.invoke(JavaMethod.java:75)
10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$IncrementalTaskAction.doExecute(AnnotationProcessingTaskFactory.java:245)
10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.execute(AnnotationProcessingTaskFactory.java:221)
10:26:29.633 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$IncrementalTaskAction.execute(AnnotationProcessingTaskFactory.java:232)
10:26:29.634 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.api.internal.project.taskfactory.AnnotationProcessingTaskFactory$StandardTaskAction.execute(AnnotationProcessingTaskFactory.java:210)
10:26:29.634 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeAction(ExecuteActionsTaskExecuter.java:80)
10:26:29.634 [ERROR] [org.gradle.BuildExceptionReporter]        at org.gradle.api.internal.tasks.execution.ExecuteActionsTaskExecuter.executeActions(ExecuteActionsTaskExecuter.java:61)
10:26:29.634 [ERROR] [org.gradle.BuildExceptionReporter]        ... 68 more
10:26:29.634 [ERROR] [org.gradle.BuildExceptionReporter]複製程式碼

經過分析和猜測後,發現每次都是同一個 module 堵住的,進去看了看...竟然幾乎是空的,是個還沒有進行元件化重構的模組(只有一個 manifest 和 string.xml),然而 build.gradle 卻使用了 data binding。看來又是個 Google 埋下的坑。心很累,就不去報 bug 了。

Dagger2

幾個月前寫過從零開始的Android新專案4 - Dagger2篇 ,用了快一年時間的 Dagger2 後,越來越覺得這種注入方式很不錯。

然而沒想到在元件化改造中會這麼坑,但是也不能怪 Dagger2,而是原先隔離就做的不夠好。

從設計上來說,Component 和獨有的 Module 都只能放在對應的業務 module 中。module 之間不能互相訪問彼此的 Dagger Module。且 data 和 domain 兩個 module 中各種業務獨有的類也應該放在業務 module 中,或者至少應該分拆出來。否則在 Module A 進行元件化開發的時候,卻能引用 Module B 的 Api 類以及資料 Bean,簡單來說也就是知道得太多。

所以如果使用了 Dagger2,這裡就需要把原來的 scope 更進一步做到極致,理清所有依賴的可見區域。

最佳實踐

每個 module 包名都應該使用 "$packageName.module.$business" 形式,資源使用業務名開頭,比如 "feed_ic_like.png"。

另外,在元件化實踐過程中可能碰到的就是依賴的問題了,然而因為我們專案本身就設計得還算不錯,所以並沒有在這方面需要做任何修改,整個專案的架構圖如下:

從零開始的Android新專案11 - 元件化實踐(1)
Dependency

簡化了不少,有些省略了,因為實在懶得畫。對模組來說,通用的東西放在底層 library(utils、widget),而只有自己用的則放在自己 module 就行了。

作為一個善意提醒,如果一個模組分拆為三個模組,那 clean build 的速度肯定會變慢,要有心理準備。

模組隔離

可參考上圖,關鍵的點就是高內聚,低耦合。

通用的東西按照其功能性劃分在不同 library 模組中。見上圖(已經省略了不少了,實際 module 更多一些)。

改進點在於,從元件化角度來講,data 和 domain 並不是一個 public 的 scope,也應該放在各個業務模組中,但因為目前的實現,進行重構代價太大,只能放在以後新模組進行實踐。

RPC

RPC 在廣義上指的是一種通訊協議,允許執行於一臺計算機的程式呼叫另一臺計算機的子程式,而開發者無需額外地為這個互動作用程式設計。Android 上的 AIDL 也是一種 RPC 的實現。

這裡指的 RPC 並沒有跨程式或者機器,而是一種類似的 —— 在彼此無法互相訪問的時候的介面定義和呼叫。

Proxy

通用的 Proxy 抽象類:

public abstract class Proxy<T, C> implements IProxy<T, C> {
    private static final String TAG = "Proxy";

    private Module<T, C> proxy;

    @Override
    public final T getUiInterface() {
        return getProxy().getUiInterface();
    }

    @Override
    public final C getServiceInterface() {
        return getProxy().getServiceInterface();
    }

    public abstract String getModuleClassName();

    public abstract Module<T, C> getDefaultModule();

    protected Module<T, C> getProxy() {
        if (proxy == null) {
            String module = getModuleClassName();
            if (!TextUtils.isEmpty(module)) {
                try {
                    proxy = (Module<T, C>) ModuleManager.LoadModule(module);
                } catch (Throwable e) {
                    LogUtils.e(TAG, module + " module load failed", e);
                    proxy = getDefaultModule();
                }
            }
        }
        return proxy;
    }
}複製程式碼

實現類則整合並過載兩個抽象方法:

public class FeedProxy extends Proxy<IFeedUI, IFeedService> {
    public static final FeedProxy g = new FeedProxy();

    // 在沒有獲得真實實現時候的預設實現
    @Override
    public Module<IFeedUI, IFeedService> getDefaultModule() {
      return new DefaultFeedModule();
    }

    // 真實實現的類
    @Override
    public String getModuleClassName() {
        return "com.amokie.stay.module.feed.FeedModule";
    }
}複製程式碼

IFeedUI 定義 Feed 模組中的 UI 相關介面,IFeedService 則是 Feed 模組的服務介面。

建議直接暴露 intent 或者 void 方法來提供跳轉,而不是返回 activity。

Router

最 low 的就是用 Class.forName 去拿 activity 或者 fragment 了...其他可以使用 scheme、各自注冊、甚至類 RPC 的呼叫方式。

為什麼說 forClass 去獲取 activity 或者 fragment 很 low ?模組 A 想去模組 B 的一個頁面,拿到 activity 後,難道還要自己去填 intent,還要自己去問人到底需要哪些引數,需要以什麼形式過去?再者如果是要去模組 B 的某個 activity 中的某個 fragment,怎麼表示?

效能問題就不談了。這麼定義後,以後包名類名都不敢換了。

RPC

就是上面提到的類似 IFeedUI 這樣的類了,使用的時候

FeedProxy.g.getUiInterface().goToUserHome(context, userId);複製程式碼

根據靈活性和需要,也可以把 intent 本身作為初始引數傳入。

註冊

即每個頁面自行去中央 Navigator 註冊自己的 Url。

中央 Navigator 維護一個 Hashmap 用於查詢跳轉。

如此,我們就依然可以通過 Android 原生的 Bundle/Intent 來傳 Parcelable 資料。

scheme

Android 原生的 scheme。當我們在瀏覽器或者一個應用呼起另一個應用,使用的就是這個機制。

與上一個方法不同的是,這是 Android 原生支援的,我們需要在 manifest 進行註冊:

<activity
    android:name="com.amokie.stay.module.card.ReactCardDetailActivity"
    android:screenOrientation="portrait">

    <intent-filter>
        <action android:name="android.intent.action.VIEW"/>

        <category android:name="android.intent.category.DEFAULT"/>
        <category android:name="android.intent.category.BROWSABLE"/>

        <data
            android:host="card"
            android:scheme="stayapp"/>
    </intent-filter>
</activity>複製程式碼

跳轉呼叫更簡單:

intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));複製程式碼

引數可以使用類似 url param 的形式,比如:stayapp://feed-detail/?id=1234&guest=true。
簡單情況下也能直接使用 Rest 形式,即 stayapp://feed-detail/1234,但如此就只能傳遞一個資料過去了,畢竟 Rest 是一種資源描述。

Software -> Peopleware,在專案逐漸變大後,團隊人數變大,需求複雜度上升,元件化的開發形式可以隔絕模組間耦合,降低中大型團隊的開發成本,而且編譯速度也能提升(獨立模組編譯執行)。

下一節將會講到元件化實踐中的:

  • 底層 library 設計
  • SharedUserId 共享資料
  • 元件間通訊(Service、EventBus)

相關文章