组件化基本概念#
典型的安卓工程目录结构如下,我们将根据图例介绍基本的概念术语,下文不再重复解释概念术语的含义。
灰色区域是工程根目录,根目录通常代指灰色区域
名称 用途 settings.gradle 用于指示 Gradle 在构建应用时应将哪些模块包含在内
多模块项目需要指定应包含在最终 build 中的每个模块顶层build.gradle 用于定义项目中所有模块的构建配置 buildscript
代码块定义项目中所有模块共用的Gradle
代码库和依赖项gradle属性文件 gradle.properties
和local.properties
绿色区域是module目录,模块目录通知代指绿色区域
名称 用途 模块级build.gradle 为其所在的特定模块配置构建设置
自定义打包选项(如额外的构建类型和产品变种)
替换main/ AndroidManifest.xml
或顶层 build.gradle
文件中的设置
蓝色区域是源代码集[3]
名称 用途 src/main/ 此源代码集包含所有构建变体共用的代码和资源。 src/buildType/ 创建此源代码集可加入特定构建变体专用的代码和资源。 src/productFlavor/ 创建此源代码集可加入特定产品变种专用的代码和资源。
注意:如果配置构建以组合多个产品变种,则可以为变种维度之间的每个产品变种组合创建源代码集目录:src/productFlavor1ProductFlavor2/
src/productFlavorBuildType/ 创建此源代码集可加入特定构建变体专用的代码和资源。
gradle属性文件#
gradle.properties#
配置项目全局 Gradle 设置,如 Gradle 守护程序的最大堆大小。如需了解详情,请参阅构建环境
local.properties#
为构建系统配置本地环境属性,其中包括:
ndk.dir
- NDK 的路径。此属性已被弃用。NDK 的所有下载版本都将安装在 Android SDK 目录下的ndk
目录中。sdk.dir
- SDK 的路径。cmake.dir
- CMake 的路径。ndk.symlinkdir
- 在 Android Studio 3.5 及更高版本中,创建指向 NDK 的符号链接,该符号链接的路径可比 NDK 安装路径短
顶层build.gradle#
顶层build.gradle
用途是配置项目全局属性:有两种配置方式直接配置和外部导入
直接配置#
直接在顶层build.gradle
形式写入配置ext{}
1 | buildscript {...} |
外部导入#
在根目录下 定义config.gradle文件,写入配置ext{}
,接着在顶级build.gradle中导入该配置
第一步:创建根目录/config.gradle
文件,并编写配置
1 | ext { |
第二步:在顶级build.gradle
中导入全局配置apply from: "config.gradle"
1 | buildscript { |
第三步:访问方式,在绿色区域代表的module区域内,有一个模块级build.gradle
文件,在该文件内可以通过rootProject.ext.property_name
方式访问全局配置
1 | android { |
模块级build.gradle#
1 | /** |
源代码集Sourceset#
在蓝色区域[3]已经描述过源代码集的4种类型,以下是四种源代码集的示例,构建系统需要合并来自以下源代码集的代码、设置和资源:
src/fullDebug/
(构建变体源代码集)src/debug/
(构建类型源代码集)src/full/
(产品变种源代码集)src/main/
(主源代码集)
记住,构建系统会合并上述四种代码集,如果同一文件在上述不同源代码集有冲突,则Gradle会按照以下优先级使用该冲突的文件:
构建变体 > 构建类型 > 产品变种 > 主源代码集 > 库依赖项
更多合并规则可以参考这里[4]
组件化实践#
组件化目标#
每个module可以独立运行也可以合并至某一个宿主apk
组件化步骤#
根目录创建
config.gradle
,在里面定义数组,数组存储键值对,key是包名,value是版本根目录创建gradle属性文件
module.build.gradle
,在里面定义所有module
的配置,包括android类型(应用、依赖库),android属性{defaultconfi、sourcesets、buildtypes}根目录编辑
gradle.properties
,定义isComponent
,isLibraries
,处理宿主module模块,在
buid.gradle
中,根据isComponent
属性,来决定是否引入其他组件,否则不由宿主生成apk- 宿主module如何引用工程根目录的
config.gradle
定义的数组答:在根目录build.gradle 加入apply from: "config.gradle"
- 宿主module如何引用工程根目录的
module模块,在
main/module目录
下创建新的AndroidManifest.xml
- 在新的
xml
中定义作为module启动的默认Activity
、Application
- 在新的
module模块,在
main目录
下编辑作为非Application
时的AndroidManifest.xml
在默认的
xml
中,应该删除启动的默认Activity
、关掉Application
的配置作为非
Application
时,配置app-name
有什么用?-答:可能要参考清单文件的优先级作为非
Application
时,写的默认Activity
有什么用?-答:参考优先级
module模块,在
build.gralde
中,导入config.gradle,在android{defaultConfig
中,根据isComponent
开启applictionId}
,在dependecies{}
按需导入依赖库
根目录结构#
build.gradle#
1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. |
config.gradle#
1 | ext { |
gradle属性文件#
1 | isComponent=true |
module.build.gradle#
1 | /** |
setting.gradle#
1 | include ':lib-common-ui' |
module-news#
news\shop\live同理
build.gradle#
1 | apply from: "../module.build.gradle" |
module/AndroidManifest.xml#
1 |
|
AndroidManifest.xml#
1 |
|
lib-common-ui#
module/AndroidManifest.xml#
1 | <?xml version="1.0" encoding="utf-8"?> |
AndroidManifest.xml#
1 | <?xml version="1.0" encoding="utf-8"?> |
build.gradle#
1 | apply from: "../module.build.gradle" |
app-vip#
AndroidManifest.xml#
1 |
|
build.gradle#
1 | plugins { |
app-normal#
AndroidManifest.xml#
1 |
|
build.gradle#
1 | plugins { |
实践总结#
根目录 | module目录 | lib目录 | |
---|---|---|---|
build.gradle | |||
AndroidManifest.xml | |||
module.build.gradle | |||
gradle.properties | |||
config.gradle | |||
settings.gradle |
组件化注意事项#
- 分别处理独立运行的情况、合并的情况
- 在模块中引用根目录的
config.gradle
,需要在根目录的build.gradle
通过apply
导入才可以,否则会报错,找不到config.gradle
中的属性 module.build.gradle
中定义了defaultConfig
,在module
模块中的build.gradle
也写了defaultConfig
,然后通过apply导入了module.build.gradle
,两个build.gradle
的defaultCOnfig
中的内容会如何处理?- 答:目前看上去是合并了如果值不冲突,则合并成一份;如果值冲突,则不清楚谁先谁后
- 编写
sourceSets
的语法,对于jnilibs
和srcfile
赋值方式有所差异,一个带等号,一个不带等号jniLibs.srcDirs=['libs']
-
manifest.srcFile 'src/main/module/AndroidManifest.xml'
组件化技术选型#
组件化技术实现了在Debug调试阶段,每个功能模块可以独立变成APP调试,但在打包编译阶段,其最终还是将所有模块打包成一个APK。
插件化:也叫动态加载技术,分宿主APK和插件APK,宿主APK可以理解为就是安装到手机的主APK(诸如手机淘宝),各个功能模块抽取变成插件APK(诸如饿了么,淘票票),这些插件APK可以随着宿主APK一起编译打包安装到手机上,也可以变成远程APK放在服务器,按需下载安装,实现功能的动态配置。从广义上理解,可以把Android系统当成一个宿主APK,各个安装到手机上的软件当成插件APK,从而组成一个插件化系统。
组件化:组件化技术实现了在Debug调试阶段,每个功能模块可以独立变成APP调试,但在打包编译阶段,其最终还是将所有模块打包成一个APK。
热修复:热修复技术有助于我们在用户无感知的时候修复APK,悄无声息的将Bug修复掉,我们希望热修复它是不新增资源文件,四大组件等操作,只是单纯的解决代码逻辑上的Bug,可以简单理解插件化技术是热修复的高级版
下表格摘自 多个维度对比一些有代表性的开源android组件化开发方案
对比项 | CC | 得到DDComponentForAndroid | ModularizationArchitecture | 阿里Arouter (网上很多组件化方案的路由引擎,如AndroidModulePattern) |
聚美组件化方案 (基于聚美Router) |
ActivityRouter |
---|---|---|---|---|---|---|
开源时间 | 2017-11 | 2017-9 | 2017-1 | 2016-12 | 2016-9 | 2016-4 |
介绍文章 | wiki | Android彻底组件化方案实践 | Android架构思考(模块化、多进程) | 开源最佳实践:Android平台页面路由框架Arouter | 聚美组件化实践之路 | ActivityRouter路由框架:通过注解实现URL打开Activity |
通信机制 | 组件总线 | 路由 + 接口下沉 | 组件总线 | 路由 + 接口下沉 | 路由 + 接口下沉 | 路由 + 静态方法 |
activity跳转 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
是否支持降级处理 | ✅ | ❌ | ✅ | ✅ | ✅ | ✅ |
activity变量自动注入 | ❌ | 1. 通过apt生成自动注入代码 2. 在onCreate中调用 AutowiredService.Factory.getInstance().create().autowire(this); 或者继承BaseActivity |
❌ | 1. 通过apt生成解析参数的代码 2. 在onCreate方法中调用 ARouter.getInstance().inject(this); 实现自动注入 |
❌ | ❌ |
startActivityForResult | 支持Activity/Fragment,但不建议使用 建议使用统一的组件调用方式 |
仅支持Activity | 仅支持Activity | 仅支持Activity | 支持Activity/Fragment | 仅支持Activity |
调用方式(页面跳转) | 同步直接返回结果或异步回调结果:CCResult result = CC.obtainBuilder("ComponentA").build().call(); 或 String callId = CC.obtainBuilder("ComponentA").build().callAsync(new IComponentCallback(){...}); |
onActivityResult返回结果:UIRouter.getInstance().openUri(getActivity(), url, bundle); |
RouterResponse response = LocalRouter.getInstance(MaApplication.getMaApplication()) .route(MainActivity.this, RouterRequest.obtain(MainActivity.this) .domain(“com.spinytech.maindemo:music”) .provider(“music”) .action(“shutdown”)); String temp = response.getData(); |
onActivityResult返回结果:ARouter.getInstance().build("/test/activity").navigation(); |
onActivityResult返回结果:Router.create(url).open(context); |
onActivityResult返回结果:Routers.open(context, url); |
调用方式(调用服务) | 与页面跳转相同 | Router.getInstance().getService(ReadBookService.class.getSimpleName()) |
与页面跳转相同 | ARouter.getInstance().navigation(HelloService.class).sayHello(); |
PipeManager.get(LoginPipe.class).logout(); |
与页面跳转相同 |
组件向外提供服务 | 与页面跳转一致,在IComponent中实现 | 接口下沉到base中,组件中实现接口并在IApplicationLike中添加代码注册到Router中 | 与页面跳转一致,实现一个对应的Action并在其所属的Provider中注册即可 | 接口继承IProvider并下沉到base中,组件中实现接口并通过注解来暴露服务 | 接口下沉到base中,组件中实现接口并在ApplicationDelegate中向接口管理类注册PipeManager.register(CorePipe.class, new CorePipeImpl()); |
在静态方法上加注解来暴露服务,但不支持返回值,且参数固定位(context, bundle) |
Fragment组件化支持 | 在IComponent中实现,并支持后续Fragment内部功能调用 | 调用服务的方式实现,未支持后续Fragment内部的功能调用 | 不支持 | 调用服务的方式实现,未支持后续Fragment内部的功能调用 | 调用服务的方式实现,未支持后续Fragment内部的功能调用 | 不支持 |
组件自动注册方案 | TrasnformAPI + ASM扫描组件类(IComponent接口实现类)并注册到ComponentMananger中,无需手动维护组件列表 | apt生成各module的路由表 TrasnformAPI + javassist将IApplicationLike的注册代码生成到自定义application.onCreate方法中,无需手动维护组件列表 |
未实现自动注册, 1. Action列表在其所属的Provider中注册 2. Provider在其所属的ApplicationLogic中注册 3. ApplicationLogic在主app的Application中注册 |
新版本(1.3.0)开始支持通过插件完成路由注册 1. apt生成各module的路由表 2. TrasnformAPI + ASM扫描路由表并注册到LogisticsCenter中,无需手动维护组件列表 |
1. apt生成各module的路由表pkg.RouterRuleCreator类 2. 在ComponentPackages中定义所有RouterRuleCreator的包名 3.在BaseApplication中反射所有的包名找到所有路由表RouterRuleCreator 4. 需要手动维护ComponentPackages类中的包名列表 |
1. apt生成各module的路由表 2. apt在application的module通过Modules注解生成RouterInit进行注册 3. 需要手动维护Modules注解中的组件列表 |
组件单独运行的方式 | 切换library/application方式编译,提供2种方式: 1. module/build.gradle中切换 ext.runAsApp=trueOrFalse 2. 在local.properties中切换 moduleName=trueOrFalse (推荐使用的方式,不会提交到代码仓库中) |
切换library/application方式编译,在module/gradle.properties中切换isRunAlone=trueOrFalse |
切换library/application方式编译,框架本身没有提供切换方式,开发者自行解决 | 切换library/application方式编译,框架本身没有提供切换方式,开发者自行解决 | 组件module始终以library方式编译,额外提供app壳子,可以按需将多个组件依赖进来一起打包。 好处是所有组件调试时包名相同,能满足分享及地图等第三方SDK对包名的要求 |
切换library/application方式编译,框架本身没有提供切换方式,开发者自行解决 |
跨app组件调用支持 | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ |
跨app调用开关及权限设置 | ✅ | / | ❌ | / | / | ❌ |
组件app运行时调用其它组件 | 组件同时安装在设备上即可,实际开发中一般是当前正在开发的组件和主app中的组件互相调用. 通过广播 + Service + LocalSocket实现,没有UrlScheme调用时弹出的选择框 |
将需要调用的组件一起打包才能调用 | 组件同时安装在设备上即可,实际开发中一般是当前正在开发的组件和主app中的组件互相调用. 通过AIDL实现 |
一起打包或者通过urlScheme来统一转发 | 将需要调用的组件一起打包才能调用 | UrlScheme原生支持跨app调用,组件同时安装在设备上即可 通过中介Activity转发:RouterActivity |
组件依赖隔离 | 无需依赖、完全隔离 | 通过插件实现只在打apk包时才添加依赖,编码期间不能直接调用其它组件的代码,想知道如何实现可以戳这里 | 无需依赖、完全隔离 | 未隔离 | 未隔离 | 无需依赖、完全隔离 |
AOP支持 | 拦截器 + 组件内部Action进行AOP | ❌ | 组件内部Action进行AOP | 拦截器AOP | ❌ | ❌ |
拦截器 | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ |
组件调用的超时设置 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
组件调用的取消 | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
动态注册/注销组件 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
特点 | 1. 可以跨app调用,初期改造时即可单独编译组件运行 2. 提供统一的组件调用及实现方式(不管是否跨app调用、页面跳转、服务调用、同步/异步调用) 3. 组件自动注册,无需维护 4. 提供了ActionProcessor按需加载的支持 |
1. 编码期间组件依赖通过插件进行隔离,避免直接调用其它组件的代码 2. 提供了兼容ARouter的方案 3. 组件自动注册,无需维护 |
1. 可以跨app、app内跨进程调用 2. 组件运行在各自进程中,单独运行与联合打包切换时需要修改进程名称 3. 组件需指定同步实现还是异步实现,调用组件时统一拿到RouterResponse作为返回值,可以自行决定同步还是异步方式调用RouterResponse.getData()来获取结果,但异步获取时需要自己维护线程 |
1. 阿里出品,使用者众多,QQ群里交流比较活跃 2. 支持分级按需加载 3. 是一个路由框架,并不是完整的组件化方案,可作为组件化架构的通信引擎 |
组件module可以始终以library方式编译,由统一的app壳子来安装调试,不需要切换library/application编译方式、避免了第三方SDK需要指定包名的问题、自定义权限重复导致安装冲突的问题、误操作导致apk upload到maven仓库的问题等 | 1. 业内最早的组件化支持库 2. 通过注解静态方法的方式暴露服务 |
组件定义代码侵入性 | 新增IComponent接口的实现类来定义组件,侵入性低 | 注解定义路由及参数自动注入,侵入性高 | 新增接口实现类,侵入性低 | 注解定义路由及参数自动注入,侵入性高 | 注解定义路由,侵入性高 | 注解定义路由,侵入性高 |
组件调用代码侵入性 | 高 | 高 | 高 | 高 | 高 | 高 |
混淆配置 | 无 | 所有下沉接口、框架中相关接口的实现类等 | -dontwarn com.spinytech.** |
框架中的所有类及框架相关接口的实现类 | 所有RouterRuleCreator类 | 框架中的所有类 |
老项目改造成本评估 | 低 | 一般 | 高 | 一般 | 高 | 低 |
方案使用的学习成本评估 | 低 | 一般 | 高 | 一般 | 一般 | 一般 |
后续维护成本评估 | 低 | 一般 | 高 | 低 | 一般 | 一般 |
QQ群 | 686844583 | 693097923 | 无 | 592278657 / 336755078 | 108895031 | 无 |
其他理论型组件化技术
对
对比项 | ArmsComponent | JIMU |
---|---|---|
开源时间 | 2018年 | 2018年 |
相关文章 | Wiki Documents (开发前必看!!!) 文章介绍 辅助代码模板: ArmsComponent-Template |
Android彻底组件化方案实践 |
架构图 |
组件化参考文章#
名称 | 大纲 |
---|---|
安居客 Android 项目架构演进 - 知乎 (zhihu.com) | |
滴滴国际化项目 Android 端演进 (trinea.cn) | |
CC: 业界首个支持渐进式组件化改造的Android组件化开源框架,支持跨进程调用 | 组件化改造的Android组件化开源框架wiki CC框架实践(1):实现登录成功再进入目标界面功能 CC框架实践(2):Fragment和View的组件化 CC框架实践(3): 让jsBridge更优雅 |
得到DDComponentForAndroid | Android彻底组件化方案实践 组件化设计思路 浅谈Android组件化 原理解释文章Android彻底组件化方案实践 demo解读文章Android彻底组件化demo发布 |
ModularizationArchitecture | Android架构思考(模块化、多进程) ModularizationArchitecture 使用教程 |
阿里ARouter | 开源最佳实践:Android平台页面路由框架Arouter |
聚美组件化方案Demo (基于聚美Router) |
聚美组件化实践之路 Router:一款单品、组件化、插件化全支持的路由框架 |
ActivityRouter | ActivityRouter路由框架:通过注解实现URL打开Activity 通过 URL 打开 Activity |
美柚路由方案RouterKit | 这个方案的特别之处在于其组件自动注册的方案:通过apt生成每个module的路由表,然后复制到app的assets目录,运行的时候遍历asset目录,反射对应的activity |
组件总线方案ModuleBus | 组件化开发跨module交互方式—ModuleBus交互 |