1. 程式人生 > >從零開始的Android新專案11

從零開始的Android新專案11

最近更新不太頻繁,一方面工作上比較忙,除了 Android 也在負責前端,另外週末和深夜也在幫人做 Go 後臺、設計技術方案、管進度的事情(因為報酬不錯沒忍心拒絕,而且確實對個人成長還有幫助),所以實在對不住。

另外,文章最底下有捐款啊,最近真是都沒錢吃飯了。。。

前言

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

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

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

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

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”。

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

Conversation

簡化了不少,有些省略了,因為實在懶得畫。對模組來說,通用的東西放在底層 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)