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

最近更新不太频繁,一方面工作上比较忙,除了 Android 也在负责前端,另外周末和深夜也在帮人做 Go 后台、设计技术方案、管进度的事情(因为报酬不错没忍心拒绝,而且确实对个人成长还有帮助),所以实在对不住。

另外,文章最底下有捐款啊,最近真是都没钱吃饭了。。。

前言

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

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

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

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

幸而时至半年后的今天,她用得还挺顺手,而 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”。

另外,在组件化实践过程中可能碰到的就是依赖的问题了,然而因为我们项目本身就设计得还算不错,所以并没有在这方面需要做任何修改,整个项目的架构图如下:

简化了不少,有些省略了,因为实在懒得画。对模块来说,通用的东西放在底层 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)

时间: 2024-10-10 13:38:56

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

从零开始的Android新项目1 - 架构搭建篇

记录一下新项目的搭建. 试想一下,如果没有历史负担,没有KPI压力,去新搭建一个项目,你会怎么设计和实现呢? 本系列文章不是教你怎么从0开始学Android,从0开始怎么建一个项目,而定位于零负担的情况下,在2016年怎么去创建一个好的Android项目,其中一部分技术并不太适合刚入门的初学者. Application specific 类似clean architecture,分为三层 presentation - data - domain. 关于Clean Architecture由于国内

从零开始的Android新项目10 - React Native &amp; Redux

本篇来讲讲 React Native 和 Redux,和其他一上来就啪啪啪丢上来一堆翻译的东西不同,本文会从简单的例子入手,让大家能快速地明白 React Native 是什么,Redux 和常见的 MVC.MVP 等有什么区别,怎么去组织一个 Redux 架构的 React Native 项目. 为避免大家还没入门就放弃,预计下一篇才会从我们项目中的实践出发,讲讲更复杂的应用场景. 什么是React Native React Native 使你能够基于 JavaScript 和 React 在

从零开始学android&lt;ImageSwitcher图片切换组件.二十六.&gt;

ImageSwitcher组件的主要功能是完成图片的切换显示,例如用户在进行图片浏览的时候,可以通过按钮点击一张张的切换显示的图片,而且使用ImageSwitcher组件在每次切换的时候也可以为其增加一些动画的效果,此类定义如下: java.lang.Object ? android.view.View ? android.view.ViewGroup ? android.widget.FrameLayout ? android.widget.ViewAnimator ? android.wid

从零开始学android&lt;Toast土司提示组件.二十.&gt;

在系统之中,通过对话框可以对用户的某些操作进行提示,但是在Android平台之中也提供了另外一套更加友好的提示界面效果,而且这种界面在提示用户的时候不会打断用户的正常操作,这种对话框可以通过Toast组件实现. Toast是一个以简单提示信息为主要显示操作的组件,在Android之中android.widget.Toast的继承结构如下所示: java.lang.Object ? android.widget.Toast 常用的方法: No. 方法及常量 类型 描述 1 public stati

从零开始学android&lt;ListView数据列表显示组件.二十一.&gt;

与滚动视图(ScrollView)类似的还有一种列表组件(ListView),可以将多个组件加入到ListView之中以达到组件的滚动显示效果,ListView组件本身也有对应的ListView类支持,可以通过操作ListView类以完成对此组组件的操作,ListView类的继承结构如下所示: java.lang.Object ? android.view.View ? android.view.ViewGroup ? android.widget.AdapterView<T extends a

Android热修复与插件化实践之路

第1章 class文件与dex文件解析本章通过从java最基本的class文件与android最基本的dex文件进行对比,并不借助IDE去生成及执行class与dex文件,通过讲解class与dex的手动生成,执行, 格式对比,让学生明白二者的相同与不同.1-1 课程项目整体介绍1-2 本章概述1-3 class文件详解上1-4 class文件详解下1-5 dex文件详解上1-6 dex文件详解下 第2章 虚拟机深入讲解本章主要介绍jvm,dvm,art.通过对这三个虚拟机的介绍让学生明白,an

Android项目模块化/组件化开发(非原创)

文章大纲 一.项目模块化初步介绍二.项目模块化的两种模式与比较三.大型项目模块化的演进四.项目模块化总结五.参考文章 一.项目模块化初步介绍 1. 前言 在Android开发中,随着项目的不断扩展,项目会变得越来越庞大,而随之带来的便是项目维护成本与开发成本的增加!每次调试时,不得不运行整个项目:每当有新成员加入团队时,需要更多的时间去了解庞大的项目...而为了解决这些问题,团队通常会将项目模块化,以此来降低项目的复杂度和耦合度,让团队可以并行开发与测试,让团队成员更加专注于自己所负责的功能模块

创建一个ArcGIS for Android 新项目并显示出本地的地图

1,我的Android版本是:Android4.4.2. 2,我的ArcgisAndroidSDK是:arcgis-android-sdk-v10.2.3. 一,地图发布 首先需要在ArcCatalog(或Arcgis Server Manager,这个貌似需要将文件夹共享)中,发布地图服务.如何发布就不用说了,需要注意的是: 1)坐标系统选择'Projected Coordinate System'->'World'下的'WGS_1984_Web_Mercator'; 2)需要选择'Servi

Android新项目GBSS:第3篇 Tomcat连接数据库[天坑,耗了1天才过关]

搭建好了开发环境,设计好了数据库,接下来该连接数据库了,这一块细节也基本上忘光,只能求助于Google了,大天朝最近围墙建的越来越高,Google基本处于不可用状态,SBBD又只能用来搜广告,谁能拯救我等屌丝码农. 步骤1:将mysql-connector-java-5.1.34-bin.jar复制到$CATALINA_HOME/lib文件夹下.下表列出了MySQL JDBC官方驱动和MySQL Server的版本对应关系[突然发现:有时候上QQ只是为了截图] 步骤2:配置JNDI数据源和连接池