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

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

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

前言

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

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

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

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

从零开始的Android新项目11 - 组件化实践(1)_第1张图片

幸而时至半年后的今天,她用得还挺顺手,而 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 的时候,且并不是必现):

从零开始的Android新项目11 - 组件化实践(1)_第2张图片

经过分析和猜测,发现每次都是同一个 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 包名都应该使用 “business” 形式,资源使用业务名开头,比如 “feed_ic_like.png”。

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

从零开始的Android新项目11 - 组件化实践(1)_第3张图片

简化了不少,有些省略了,因为实在懒得画。对模块来说,通用的东西放在底层 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 proxy;    @Override
    public final T getUiInterface() {        return getProxy().getUiInterface();
    }    @Override
    public final C getServiceInterface() {        return getProxy().getServiceInterface();
    }    public abstract String getModuleClassName();    public abstract Module getDefaultModule();    protected Module getProxy() {        if (proxy == null) {
            String module = getModuleClassName();            if (!TextUtils.isEmpty(module)) {                try {
                    proxy = (Module) 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 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)

你可能感兴趣的:(从零开始的Android新项目11 - 组件化实践(1))