组件化是什么
组件化,相对于容器化(插件),是一种没有黑科技的相互隔离的并行开发方式。为了了解组件化,不得不先说一下插件化。
为什么我们需要插件化
现代 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 中:
1 2 3 4 5 | if (isDebug.toBoolean()) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' } |
并在业务 module 中放一个 gradle.properties:
1 | isDebug=false |
如此,当我们设置 isDebug 为 true 时,则这个 module 将会作为 application module 编译为 apk,否则 为 library module 编译为 aar。
下面的 gradle 是我们的一个组件化业务 module 的完整 build.gralde:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 | 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,比如:
1 2 3 4 5 6 7 8 9 | 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 不同,总之会有些细微的差别。
一个简单的做法是:
1 2 3 4 5 6 7 8 9 | 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 的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <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 的时候,且并不是必现):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 | 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”。
另外,在组件化实践过程中可能碰到的就是依赖的问题了,然而因为我们项目本身就设计得还算不错,所以并没有在这方面需要做任何修改,整个项目的架构图如下:
简化了不少,有些省略了,因为实在懒得画。对模块来说,通用的东西放在底层 library(utils、widget),而只有自己用的则放在自己 module 就行了。
作为一个善意提醒,如果一个模块分拆为三个模块,那 clean build 的速度肯定会变慢,要有心理准备。
模块隔离
可参考上图,关键的点就是高内聚,低耦合。
通用的东西按照其功能性划分在不同 library 模块中。见上图(已经省略了不少了,实际 module 更多一些)。
改进点在于,从组件化角度来讲,data 和 domain 并不是一个 public 的 scope,也应该放在各个业务模块中,但因为目前的实现,进行重构代价太大,只能放在以后新模块进行实践。
RPC
RPC 在广义上指的是一种通信协议,允许运行于一台计算机的程序调用另一台计算机的子程序,而开发者无需额外地为这个交互作用编程。Android 上的 AIDL 也是一种 RPC 的实现。
这里指的 RPC 并没有跨进程或者机器,而是一种类似的 —— 在彼此无法互相访问的时候的接口定义和调用。
Proxy
通用的 Proxy
抽象类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | 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; } } |
实现类则集成并重载两个抽象方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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
这样的类了,使用的时候
1 | FeedProxy.g.getUiInterface().goToUserHome(context, userId); |
根据灵活性和需要,也可以把 intent 本身作为初始参数传入。
注册
即每个页面自行去中央 Navigator 注册自己的 Url。
中央 Navigator 维护一个 Hashmap 用于查询跳转。
如此,我们就依然可以通过 Android 原生的 Bundle/Intent 来传 Parcelable 数据。
scheme
Android 原生的 scheme。当我们在浏览器或者一个应用呼起另一个应用,使用的就是这个机制。
与上一个方法不同的是,这是 Android 原生支持的,我们需要在 manifest 进行注册:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <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> |
跳转调用更简单:
1 | 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,在项目逐渐变大后,团队人数变大,需求复杂度上升,组件化的开发形式可以隔绝模块间耦合,降低中大型团队的开发成本,而且编译速度也能提升(独立模块编译运行)。