从零开始的Android新项目11 - 组件化实践(1)

原文见:http://blog.zhaiyifan.cn/2016/10/20/android-new-project-from-0-p11/

这里的组件化,指的是 MDCC 2016 上冯森林提出的《回归初心,从容器化到组件化》。

我个人一直是比较反感黑科技的,其中首当其冲的就是 插件化 以及 保活。作为一个开发者,除了研究技术,提高自己以外,是否应该考虑些其他东西呢?尤其是我们这些嵌入式系统(客户端)开发者,在依赖、受哺于系统生态下,是不是应该考虑一下,怎么反哺?怎么去更好地维护这个生态环境,而不是一味破坏、消耗它呢?

想一想那些黑科技带来的。插件化导致线上可以执行任何代码且不留下痕迹,用户安全性和信任感何在?保活导致应用长时间不释放,抢占系统资源,让用户产生 Android 越用越卡的感觉。全家桶互相唤醒,确定不是逼着用户删除应用?至少我在 Android 手机上是不敢装某些知名应用的。

Greenify —— 绿色守护 帮助我们解决了应用死不掉的问题。那其他的呢?作为一个 Android 开发者,我不敢在我的 Android 手机上装一些应用 —— 支付宝、淘宝、闲鱼(Web 上还不让用)、天猫、京东、百度贴吧。有朋友找我推荐手机的时候,我从不会推荐 iPhone,但给他们推荐 Android 后,又会担心他们能不能 hold 住国内生态下的 Android 手机。有一个买了 Sony Z5 的女孩子,当时问我为啥用电那么快后,我实在无言以对。只能给她指导了一些姿势和黑科技。

android-new-project-from-0-11-conversation.png

幸而时至半年后的今天,她用得还挺顺手,而 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-new-project-from-0-11-dependency.png

简化了不少,有些省略了,因为实在懒得画。对模块来说,通用的东西放在底层 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)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,046评论 25 707
  • 不怕跌倒,所以飞翔 组件化开发 参考资源 Android组件化方案 为什么要组件化开发 解决问题 实际业务变化非常...
    笔墨Android阅读 2,943评论 0 0
  • 今天来回味下组件化和模块化,这2种说法时一回事,当然还是有区别的,下面再详细说,其实很简单,只是设计范围的不同,也...
    前行的乌龟阅读 48,323评论 6 94
  • 更新:Android组件化之通信(多模块,多进程) Android项目中代码量达到一定程度,编译将是一件非常痛苦的...
    wutongke阅读 53,380评论 57 364
  • 青春仿佛因我爱你开始,但却令我看破爱这个字——林夕《小城大事》 看着公众号里关于香港金牌填词人林夕...
    无涯先生阅读 1,039评论 0 1