从工程领域来看, 模块化, 组件化, 插件化三种技术都是指将复杂代码进行拆分, 达到解偶分层, 便于管理的目的. 普遍意义上, 将代码按照业务模块划分就是模块化, 如果再进一步从模块化代码中抽出通用于所有 App 的组件, 作为一个独立的 module 或者 maven 依赖(比如一些比较有名的第三方 SDK), 这个组件生成的过程就叫组件化. 插件化则是指将 App 按一定规则拆分成几个若干个 APK, 除了主 APK, 其他 APK 均可以通过网络下发然后通过主 APK 加载. 通过加载, 修改, 卸载非主 APK, 一定程度上给予了 App 热修复的功能. 然而随着 Android 9.0 上私有 API 的限制, 插件化受到了极大的限制, 主流方案慢慢向稳定, 务实的的组件化方案演进.
组件化架构
比较传统的一些架构是利用 MVC,MVP,MVVM 对项目进行分包, 然而随着项目代码量越来越多, 修改的时候会牵一发而动全身, 而且不利于并行开发和回归测试. 通过将一些通用的代码进行拆分, 然后使用 maven 依赖进来, 可以减少本地的代码量并解偶了一部分维护工作. 随着业务的日渐复杂, aar 级别的组件化还不够, 每次修改部分业务都会需要对其他相关业务进行进行回归测试, 一些当前版本不会用到的代码仍然会打包进 APk, 这样不利于后续的维护. 所以业界相继提出了 "组件化架构" 的思想, 也就是在整个 Project 层面对代码按照模业务块, 功能模块, 基础模块进行划分, 然后在具体的 Module 中用 MVC,MVP,MVVM 等思想构建具体的代码. 如下图所示:
其中基础组件层主要包括: 网络库, 日志库, 路由库等. 但在实际的开发过程中, 往往需要对这些基础库封装一层, 比如对网络库用观察者模式封装一层来实现 UI 移步加载, 路由需要自定义拦截器等, 这一层就是我们的 service 层, 也就是功能组件层. Service 层的主要目的是向外提供服务, 而业务组件则是具体的业务逻辑. 具体的分包示意图如下:
为了避免循环依赖和业务逻辑之间的交叉, 同一层的组件是不能直接相互引用的.
这是因为:
1)除了主 Module, 其他任何一个业务组件都可能是处于加载或者不加载的状态, 比如有 2 个模块 A 和 B, 如果相互依赖, 且假设没有加载 B, 而线上模块 A 使用了 B 中功能, 那么 A 可能会 crash;
2)任何一个业务组件都是独立的, 也就是说可能是由不同的部门并行开发的, 不应该互相依赖.
鉴于这两个规则, 同一层之间是不能直接通信(如果只考虑第 1 点, 不考虑独立依赖, 可以使用反射, 但是不够美观且会影响性能).
这个通信包括两方面:
1)界面之间的相互跳转;
2)服务之间及业务之间的相互调用. 同时组件如何注册, 加载, 卸载, 这些都是组件化架构需要解决的.
实践方案
结合上述的理论基础, 在实践过程中需要解决的技术难点主要有: 模块间的通信, 路由表的自动维护, 组件的生命周期管理, 主包管理及进程间通信等.
1. 通信
说到通信, 我们能想到的方案有两种, 路由和事件总线. 路由可以解决界面的跳转和一些 dialog,toast 的显隐, 但是不能解决服务之间的相互调用和回调. 业界提出了类似于 Android 中四大组件之一 ServiceManager 的处理方法 --"接口下沉", 也就是在基础组件层新建一个 ServiceManager, 并提供通用服务接口 IService, 在需要暴露服务的地方实现该接口并手动 / 自动注册到 ServiceManager 中, 这样任何需要该服务的地方都可以通过: ServiceManager.get(Class<t style="margin: 0px; padding: 0px; max-width: 100%; box-sizing: border-box; word-wrap: break-word !important;">clz)静态工厂方法取得, 类似于 ServiceManager 中的 addService(String name, IBinder service)和 IBinder getService(String name)方法, 当然 ServiceManager 类完全可以由注解来生成. 第二种方案就是使用事件总线, 比如 EventBus 或者 RxBus, 因为事件总线本身是通过观察者模式实现的同时可以支持跳转, 所以也可以用来替代路由 +"接口下沉" 的方案.</t>
2. 自动注册
在实践过程中, 现有的方案都需要维护 3 个 HashMap, 分别是路由表, 服务表以及组件表, 当服务多到一定的程度, 手动维护 3 个哈希表是一场灾难. 以小赢理财现有的 Scheme 路由库为例, 初代版本中是在内存中维护一个静态的 HashMap 路由表, key 表示路径, value 代表 calss. 虽然这样方便省事但是可维护性较差, 一旦跳转规则变化或者忘记及时更新则会失效. 秉承着 "能让机器完成绝不自己动手" 的原则, 在迭代中改成了通过反射去扫描 AndroidManifest, 自动生成维护 HashMap, 这样可以做到 map 的自动注册. 但是由于扫描是在 Application 的 onCreare()方法中完成, 用 AOP 测试发现扫描过程比较耗时, 这种自动化的方式是以牺牲启动时间为代价的. 联想到注解, 可以通过编译时注解插入代码动态生成 HashMap, 但尝试过后发现部分场景下行不通, 因为编译时注解的特性只在源码编译时生效, 无法扫描到 aar 包里的注解, 这种情况不适合远程预埋 aar, 动态下发的场景. 运行时注解也会不可避免的会造成性能的缺失. 幸运的是, Android 官方提供了 Transform API, 可以用来在. class 转换为. dex 前操作 class 文件. 这样配合 ASM(如果觉得 javap 命令生成字节码太麻烦, 可以使用 IntelliJ IDEA 插件'Bytecode outline', 可方便快捷根据 java 类生成字节码)或 javassist 就可以动态的修改字节码, 从而动态生成 HashMap 而不需要损耗性能. 这种方式配合后台下发的方式, 就能保住灵活性.
大致步骤如下:
新建 buildSrc 工程, 或者独立工程;
接入 Transform 依赖: implementation 'com.android.tools.build:gradle:3.2.1', 值得注意的是这个包里面包含了 ASM 库;
新建一个 class 实现 Transform 类, 将扫描范围设置为: TransformManager.SCOPEFULLPROJECT, 并在 tranform(TransformInvocation transformInvocation)中遍历目录输入和 jar 输入, 并使用 ClassVisitor 操作字节码;
最后在自定义 Plugin 中注册这个自定义 Transform.
具体的操作细节可以参考官方文档.
3. 组件生命周期管理
一个进程对应着一个虚拟机, 虚拟机需要管理 APk 的生命周期. 同理, 如果把 App 看作一个虚拟机, 把各个业务组件看成是小型的 App, 那么 App 是需要妥善管理各个业务组件的生命周期的. 也就是说我们需要同步资源初始化, 使用, 销毁的时机. 可以模拟虚拟机的工作流程: 加载 - 验证 - 准备 - 解析 - 初始化 - 使用 - 卸载, 同时使用 ApplicationDelegate 代理, hook 住 Application 的各个生命周期, 这样就可以实现组件的同步加载, 需要主动销毁时, 则可以将 module 手动卸载.
4. 依赖和主包管理
随着拆分出来的 Module 和 aar 越来越多, 每次都需要重复配置依赖和项目基本参数. 由于每层 (主 Module 层, 业务组件层, 功能组件层, 基础组件层) 之间的依赖都大同小异的, 因此抽出一层 gradle_component, 专门用来配置通用 gradle, 这样就可以统一依赖, 比如:
然后把这些 gradle 分别 apply 到对应的层级, 当然为了方便管理减少. gradle 文件, 可以将具体的依赖通过自定 Plugin 的形式注入. 由于 module 和 aar 比较多, 当真正进行 build 的时候, 需要检查 settings.gradle 并对每个 Module 进行初始化配置, 再运行具体的 task 进行打包. 这个操作随着 Module 个数的增多, 执行的耗时会直线上升. 实际情况是, 假设只修改了某个 module, 并只想运行这一部分增量代码, 这个时候可以通过切换主 App 完成. 由于 Android 项目是通过 plugin 来识别的:
- apply plugin:'com.android.application' // 主 module
- apply plugin: 'com.android.library' // module
这样我们可以在本地自定义一个'isMainAPP'参数来控制主 Module 和 Module 的切换.
- if(isMainAPP) {
- // 切换
- apply plugin:'com.android.application' // 主 module
- } else {
- apply plugin: 'com.android.library' // module
- }
5. 多进程通信
进程是最小的资源分配管理单位, 当业务组件多大一定的程度时, 会需要考虑使用多进程通信. 如果只是简单的跳转不涉及到数据的获取, 那么路由组件是可以胜任的, 因为 Android 内置的 Intent 机制本来就是跨进程的. 如果需要同步数据, 则需要考虑进程通信中出现的脏数据, 比如同时操作 sharepreferences 是比较棘手的, 因为 sharepreferences 在文件和内存中各有一份数据, 且有时候不相同. Android 系统提供了基于 mmap 的 Binder 通信机制, 落实到工程代码就是实现 AIDL, 生成远程 Binder 类和当前进程的 Proxy 类, 并定义相关的 Service 和 BinderPool. 但是这样稍微有点重, 可以考虑使用现有的 ContentProvider + SQLite(ContentProvider 本身也是 AIDL 通信机制, 只是系统对其进行了一层封装), 在路由库中增加对多进程通信拦截链的支持.
已有的解决方案
当前的一些大公司都先后开源了自己的组件化架构框架, 比较知名的有美团的 modular-event, 阿里的 ARouter 以及得到 (逻辑思维主打 App) 的 DDComponentForAndroid. 其设计思想大同小异, 基本也是机遇以上的设计要点, 辅助一些同步和异步功能. 美团舍弃了之前开发的 "WMRouter" 路由, 转而使用了 modular-event, 阿里和得到则是使用路由 + 接口下沉的方式去构建整个架构."组件化架构" 能够清晰的划分项目结构, 严格的将代码根据 "业务组件","模块组件","基础组件" 进行划分, 各个项目组成员可以并行开发 module 而互不干扰, 而且其可扩展性也比较强, 对业务不断扩大的项目是一个不错的选择.
最后相关架构及资料
组件化框架设计. PNG
Android 高级技术大纲
领取方式:
点赞 + 加群免费获取 Android IoC 架构设计
加群 Android IoC 架构设计领取获取往期 Android 高级架构资料, 源码, 笔记, 视频. 高级 UI, 性能优化, 架构师课程, NDK, 混合式开发 (ReactNative+Weex) 微信小程序, Flutter 全方面的 Android 进阶实践技术, 群内还有技术大牛一起讨论交流解决问题.
来源: http://www.jianshu.com/p/63db9b9d5244