(点击
上方公众号
,可快速关注)
来源:伯乐在线专栏作者 - markzhai
链接:http://android.jobbole.com/85197/
点击 → 了解如何加入专栏作者
最近更新不太频繁,一方面工作上比较忙,除了 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
]