從零開始的Android新項目11 - 組件化實踐(1)

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

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

想想那些黑科技帶來的。插件化致使線上能夠執行任何代碼且不留下痕跡,用戶安全性和信任感何在?保活致使應用長時間不釋放,搶佔系統資源,讓用戶產生 Android 越用越卡的感受。全家桶互相喚醒,肯定不是逼着用戶刪除應用?至少我在 Android 手機上是不敢裝某些知名應用的。android

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

Conversation

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

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

組件化是什麼

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

爲何咱們須要插件化

現代 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"。

另外,在組件化實踐過程當中可能碰到的就是依賴的問題了,然而由於咱們項目自己就設計得還算不錯,因此並無在這方面須要作任何修改,整個項目的架構圖以下:

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)
相關文章
相關標籤/搜索