组件化方案调研
组件化概念
组件化就是将一个app分成多个Module,如下图,每个Module都是一个组件(也可以是一个基础库供组件依赖),开发的过程中我们可以单独调试部分组件,组件间不需要互相依赖,但可以相互调用,最终发布的时候所有组件以lib的形式被主app工程依赖并打包成一个apk。
模块化、插件化和组件化的关系
对于这三者的关系:有不同的说法。
截取一些,以供参考,其实虽然每个人都有每个的不通过的理解和说法,但是内涵其实是一样的。
模块化与组件化
-
得到(张明庆)
在技术开发领域,模块化是指分拆代码,即当我们的代码特别臃肿的时候,用模块化将代码分而治之、解耦分层。具体到 android 领域,模块化的具体实施方法分为插件化和组件化。
《 Java 应用架构设计:模块化模式与 OSGi 》一书中对它的定义是:模块化是一种处理复杂系统分解为更好的可管理模块的方式。
-
《安居客》(张磊)组件和模块做个区别定义
- 组件:指的是单一的功能组件,如地图组件(MapSDK)、支付组件(AnjukePay)、路由组件(Router)等等;
- 模块:指的是独立的业务模块,如新房模块(NewHouseModule)、二手房模块(SecondHouseModule)、即时通讯模块(InstantMessagingModule)等等;模块相对于组件来说粒度更大。
综上所述,模块化与组件化都是对于一个整体功能的封装,只不过粒度不一样。我们本文中所指的组件化,和上述3中提到的模块化是一致的。
插件化与组件化
一套完整的插件化或组件化都必须能够实现单独调试、集成编译、数据传输、UI 跳转、生命周期和代码边界这六大功能。
插件化和组件化最重要而且是唯一的区别的就是:插件化可以动态增加和修改线上的模块,组件化的动态能力相对较弱,只能对线上已有模块进行动态的加载和卸载,不能新增和修改。
插件化让我们的App的运行的时候能够动态的进行组装,就像我们使用chrome的插件一样,非常的方便,甚至演变到后期,插件化越来越像[虚拟机]发展,使用一个类似[boot]的壳,就可以像Java虚拟机加载Java文件一样加载一个App。插件化主要涉及对系统加载dex的依赖,所以适配起来坑比较多。
组件化可以说和插件化有异曲同工之妙,只不过插件化是在[运行时],而组件化是在[编译时]。换句话说,插件化是基于多APK的,而组件化本质上还是只有一个APK。编译一个大型App的时间时间很长,可能2,3分钟都不够.[组件化] 就可以很好的解决这样的问题,此外,由于整个App的各个业务被分离了,所以它们之间的耦合度也就被降低了,各个业务线可以由专门的开发同学进行开发,相互之间也不会有干扰,提升开发效率
组件化详细方案
代码组件之间解耦
按照业务逻辑划分模块,对于一般app来说,可以分为三层。
- 基础库(比如网络库,数据库,utils,图片加载库)
- 基础模块,但是没有独立运行调试的必要,一般只是当作com.android.library被别的独立模块引用(比如登陆模块,分享模块,视频播放模块)
- 业务模块,可以进行单独编译,打包,测试(比如拍摄模块,搜索模块,视频流模块,小视频模块)
这三个层次只能向下依赖,比如3层依赖2层,2层依赖1层。每一层里面互相不能依赖。
这符合依赖倒置原则
业界模块划分的例子
-
安居客
-
滴滴组件化的图
微信
组件工程单独开发调试
先来看看Android组件化需要实现的目标。(什么是组件化构建?)
- 项目模块能够单独启动测试
- 能够根据需求引入或删除某些业务模块
- 通过不同模块的组合,组成不同的App
对于第一点:Android是通过应用com.android.application或com.android.library来决定该模块是以App模式还是以Library模式构建。App模式和Library模式的最大区别就是,App能够启动,而Library不可以。所以如果我们的模块能独立启动的话,我们需要每次手动去改动模块的build.gradle文件。好一点的做法定义一个布尔值来判断是否处于debug模式,但是这里有个问题是,不是每个模块都能独立启动的。所以无论采用何种方案,都需要我们手动管理。
对于第二点:当我们开发好业务模块后,可能我们需要频繁的新增或删除某些业务模块。如果是这样的话,我们也是需要频繁手动修改App的build.gradle。
对于第三点:有时候,我们可能会在不同的App中引用相同的组件(例如:滴滴的普通版和企业版,普通版包含企业版的功能),这个时候,我们也不希望要频繁手动管理组件依赖,特别是在组件还可以独立运行的时候。
所以,在我们实践组件化的时候,最大的问题就是,我们需要频繁的手动build.gradle文件来管理组件应用的插件和App的依赖。
对于这一部分,主要的就是开发一个gradle插件,对工程进行自动化管理,避免人为切换导致的各种错误。
组件之间互相通信
由于组件之间完全解耦,互相无法直接进行调用,所以通过接口+实现的结构进行组件间的通信。每个组件声明自己提供的服务 Service API,这些 Service 都是一些接口,组件负责将这些 Service 实现并注册到一个统一的路由 Router 中去,如果要使用某个组件的功能,只需要向Router 请求这个 Service 的实现,具体的实现细节我们全然不关心,只要能返回我们需要的结果就可以了。在组件化架构设计图中 Common 组件就包含了路由服务组件,里面包括了每个组件的路由入口和跳转。
由于各个模块是严格划分解耦的,各个模块对与彼此的存在是完全不知道的,所以各个组件的互相通信就需要一个通用的协议来进行。于是引入了路由框架。
通过上图可以看到,我们在最基础的Common库中,创建了一个路由Router,中间有n个模块Module,这个Module实际上就是Android Studio中的module,这些Module都是Android Library Module,最上面的Module Main是可运行的Android Application Module。
这几个Module都引用了Common库,同时Main Module还引用了A、B、N这几个Module,经过这样的处理之后,所有的Module之间的相互调用就都消失了,耦合性降低,所有的通信统一都交给Router来处理分发,而注册工作则交由Main Module去进行初始化。这个架构思想其实和Binder的思想很类似,采用C/S模式,模块之间隔离,数据通过共享区域进行传递。模块与模块之间只暴露对外开放的Action,所以也具备面向接口编程思想。
图中的红色矩形代表的是行动Action,Action是具体的执行类,其内部的invoke方法是具体执行的代码逻辑。如果涉及到并发操作的话,可以在invoke方法内加入锁,或者直接在invoke方法上加上synchronized描述。
图中的黄色矩形代表的是供应商Provider,每个Provider中包含1个或多个Action,其内部的数据结构以HashMap来存储Action。首先HashMap查询的时间复杂度是O(1),符合我们对调用速度上的要求,其次,由于我们是统一进行注册,所以在写入时并不存在并发线程并发问题,在读取时,并发问题则交由Action的invoke去具体处理。在每一个Module内都会有1个或多个供应商Provider(如果不包含Provider,那么这个Module将无法为其他Module提供服务)。
途中蓝色矩形代表的是路由Router,每个Router中包含多个Provider,其内部的数据结构也是以HashMap来存储Provider,原理也和Provider是一样的。之所以用了两次HashMap,有两点原因,一个是因为这样做,不容易导致Action的重名,另一个是因为在注册的时候,只注册Provider会减少注册代码,更易读。并且由于HashMap的查询时间复杂度是O(1),所以两次查找不会浪费太多时间。当查找不到对应Action的时候,Router会生成一个ErrorAction,会告之调用者没有找到对应的Action,由调用者来决定接下来如何处理。
一次请求流程
通过Router调用的具体流程是这样的:
任意代码创建一个RouterRequest,包含Provider和Action信息,向Router进行请求。
Router接到请求,通过RouterRequest的Provider信息,在内部的HashMap中查找对应的Provider。
Provider接到请求,在内部的HashMap中查找到对应的Action信息。
Action调用invoke方法。
返回invoke方法生成的ActionResult。
将Result封装成RouterResponse,返回给调用者。
上述描述,引用:Android架构思考
这只是一个原理性的描述,不同的开源框架具体实现细节可能不同,想了解具体实现细节,可以研究一下开源路由代码。
开源框架实现:
- WMRouter(美团方案)
- ARouter(阿里方案)
- Andromeda(支持跨进程)
- DDComponentForAndroid(得到方案)
- ModularizationArchitecture(支持跨进程)
UI 跳转问题
可以说 UI 跳转也是组件间通信的一种,但是属于比较特殊的数据传递。不过一般 UI 跳转基本都会单独处理,一般通过短链的方式来跳转到具体的 Activity。每个组件可以注册自己所能处理的短链的 Scheme 和 Host,并定义传输数据的格式,然后注册到统一的 Router 中,Router 通过 Scheme 和 Host 的匹配关系负责分发路由。但目前比较主流的做法是通过在每个 Activity 上添加注解,然后通过 APT 形成具体的逻辑代码。
目前开源的框架有:
- ARouter 框架,通过注解方式进行页面跳转,阿里出品
- ActivityRouter
- WMRouter 美团开源框架
- DeepLinkDispatch
资源冲突
- 对于多个 Bussines Module 中资源名冲突的问题,可以通过在 build.gradle 定义前缀的方式解决:
defaultConfig {
...
resourcePrefix "new_house_"
...
}
- 多个mainfest问题
每个可以独立运行的组件,都需要自己独立的mainfest,根据组件是否是独立运行,配置不同的Manifest路径
sourceSets {
main {
if (isDebug.toBoolean()) {
manifest.srcFile 'src/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/release/AndroidManifest.xml'
}
}
}
组件加载
- 多个application的问题
可以尝试用两种方式字节码插入模式是在dex生成之前,扫描所有的ApplicationLike类(其有一个共同的父类),然后通过javassist在主项目的Application.onCreate()中插入调用ApplicationLike.onCreate()的代码。这样就相当于每个组件在application启动的时候就加载起来了。
反射调用的方式是手动在Application.onCreate()中或者在其他合适的时机手动通过反射的方式来调用ApplicationLike.onCreate()。之所以提供这种方式原因有两个:对代码进行扫描和插入会增加编译的时间,特别在debug的时候会影响效率,并且这种模式对Instant Run支持不好;另一个原因是可以更灵活的控制加载或者卸载时机。
对于1,2方案,具体可以查看得到(DDComponentForAndroid)方案中的实现。
业内一些组件化开源方案
- 得到方案(DDComponentForAndroid)
- CC(ComponentCaller),渐进式组件方案
- ModularizationArchitecture
- Atlas(阿里方案)
可行性(组件化与****项目如何结合)
综上所述,组件化是一个系统性工程,需要从编译,代码结构,通信框架,打包,测试等各个环节进行重构。
对于已有的app来说,采用渐进式的组件化是比较稳妥地方式。
下面针对****进行一些分析,将****中的module库进行一个划分
-
基础库
baseAppInfoLib common-lib-net
-
功能库
sohuUpload downloadsdk sohuMediaPlayerSdk sohuMediaPlayerLib sohuDanmuLib scaleview sohuVideoEditor sohuPrivilegeLib
-
组件库
qianliyanlib sohuVideoMobile fivesixapp
短期规划
目前拍摄模块(qianliyanlib)相对独立,先从拍摄模块进行解耦
首先与拍摄模块有直接耦合关系的是上传视频组件,上传视频组件中,耦合了一部分视频转码的状态展示。
可以考虑先将拍摄模块与上传模块先行进行组件化。两个模块中的状态互相依赖,则通过注册各自模块提供的service进行接口编程方式实现。
后期如果有新的需求,并且是比较大的功能模块,那么就直接用组件模式进行开发。
通过这个组件化后,整个拍摄模块与上传模块,就可以单独调试与运行,在解耦过程中,逐步探索组件化的一些细节。为后期整体app组件化积累经验。
后期规划
可以按照功能模块进行划分如下:
- 红包模块
- 搜索模块
- 商场订单模块
- 首页框架模块
- 小视频播放模块(包含列表与详情)
- 视频信息流模块(包含首页tab,热点tab,订阅流,包含流播放,视频详情中播放)
- 个人中心模块
参考资料
http://blog.spinytech.com/2016/12/28/android_modularization/
https://github.com/luckybilly/AndroidComponentizeLibs
https://mp.weixin.qq.com/s/6Q818XA5FaHd7jJMFBG60w
http://blog.zhaiyifan.cn/2016/10/20/android-new-project-from-0-p11/
https://mp.weixin.qq.com/s/6Q818XA5FaHd7jJMFBG60w
http://zjutkz.net/2016/10/07/%E5%85%B3%E4%BA%8EAndroid%E4%B8%9A%E5%8A%A1%E7%BB%84%E4%BB%B6%E5%8C%96%E7%9A%84%E4%B8%80%E4%BA%9B%E6%80%9D%E8%80%83/
http://www.trinea.cn/android/didi-internationalization-android-evolution/
http://tangpj.com/2018/07/22/calces-componentization/