
前言
组件化对于任何一个业务场景复杂的 APP 以及经过多次迭代之后的产品来说都是必经之路,组件化是指解耦复杂系统时将多个功能模块拆分、重组的过程。组件化要做的不仅仅是表面上看到的模块拆分解耦,其背后还有很多工作来支撑组件化的进行,例如结合业务特性的模块拆分策略、模块间的交互方式和构建系统等等。
本文主要讲述爱奇艺知识 APP 如何结合自身的业务特点,探索和实践了一套高效的移动端组件化方案。
01 背景与目标
1.1 背景
爱奇艺知识目前有多个业务承载端,包括爱奇艺移动端 APP 的知识插件、爱奇艺 iPad 端 APP 的知识插件、随刻移动端 APP 的知识插件和爱奇艺知识移动端独立 APP。由于各个端上线的时间不同,所承载的业务功能也不完全一致,造成了多端多套代码的情况,维护成本很高。首先当相同或类似的功能需要迭代升级时,开发和测试都需要在多端同步进行,成本成指数级增长;其次随着业务的快速发展,业务模块在不断增加,模块间的依赖关系也变得越来越模糊,代码耦合度和复杂度都在增大;另外在现有人力成本的基础上如果想增加更多的业务端,就会变得非常困难。因此长期看非常不利于业务的高效迭代。下图描述了组件化之前,爱奇艺知识各端的业务模块架构。

从上图我们也能看出其实每个端之间是存在很多公共业务模块的,各个业务模块的底层支撑模块也几乎相同,所以结合爱奇艺知识自身的业务特点,我们提出了适合于爱奇艺知识移动端的组件化方案。
1.2 目标
我们将组件化的目标定义为以下几个:
解决多端代码维护问题
根据业务特点,横向和纵向划分组件,以组件为单位承接迭代需求,各端进行组件复用;
解决跨组件调用和组件间路由的问题
业务划分更加清晰、组件间解耦更加彻底、组件间通信更高效,对原有业务模块进行抽离和整合,明确组件间的业务边界;
提升开发效率,方便开发调试
组件可以单独编译和调试,使模块开发者更聚焦本模块业务;
提升集成和提测效率
各端项目需要哪个组件,可以直接通过工具快速集成和提测。
基于以上目标,我们设计了适合爱奇艺知识业务的组件划分策略,下图为组件化之后的功能架构图,横向分为基础组件、功能组件和业务组件,纵向对每个层级的组件又进行细分;从划分粒度上看,组件不仅包括功能性的 sdk,还可能包括业务 UI,宗旨就是业务模块独立,边界清晰,方便扩展和维护。

02 整体技术架构
基于功能架构,知识组件化的技术架构如下图所示。

最下层是基础组件,包括 baselib 和 componentService,我们将网络库、pingback、数据库、日志和工具类等公共底层实现构成基础组件,屏蔽了系统和各端的差异,位于功能和业务组件的下层。所有功能和业务组件都使用同一套基础组件,可以保证公共部分的统一性。基础组件比较稳定,不会频繁迭代。
再往上一层是功能组件,如承载播放能力的播放器和历史记录组件、承载支付能力的支付和营销组件,、承载多端定制化分享能力的分享 &海报组件等各个端都有的基础功能,功能组件位于基础组件和业务组件之间,功能组件会根据业务组件的需要而不断迭代升级。
接下来是业务组件,这层是各个端有可能包含也有可能不包含核心业务模块,为了开发和维护方便,我们将核心业务模块抽取为业务组件,如搜索、筛选、发现 feed 流、评论、评价、作业作品等,业务组件位于基础组件和功能组件的上层,迭代较频繁,但业务本身比较独立,边界清晰。
最上层是壳工程,各端都需要一个主工程负责集成所需要的组件,我们统称为壳工程,壳工程包括了各端的基础框架,比如组件注册和初始化逻辑,平台相关性处理逻辑等,还有各端特有的业务模块,不适合抽离和拆解的部分。
右侧是负责管理组件间交互和跳转的 MoudleRouter 和 UIRouter。这部分是公共基础设施,各端都要集成。
左侧是构建系统,它不在组件化代码中,属于辅助系统,负责组件和各端应用包的构建。
03 核心技术实现
组件化实践中比较核心的两个技术点是,组件间交互和组件间路由。
3.1 组件交互
组件间交互的难点是降低组件耦合度,最好能达到完全无侵入式的调用。经过调研,iOS 端使用 ModuleManager 的方式,它被定义为最底层的服务组件,每个组件都需要对外提供被调用的服务接口,接口的定义存在于 ModuleManager 组件。ModuleManager 的代码对其他组件代码来说是无侵入的,只负责对传递过来的数据进行解析,并将调用消息传递给对应组件。

为了解决 URL 硬编码 ,以及字典参数类型不明确等问题,iOS 端在组件化方案中选用了 Protocol 方案,在程序开始运行时将自身的 Class 注册到 ModuleManager 中,并将 Protocol 反射为字符串当做 key,Class 遵守协议并实现协议定义的方法,外界通过 Protocol 获取的 Class 并实例化为对象,调用服务方实现的协议方法。独立 APP 和各个插件的服务注册的时机不同,独立 APP 是在程序启动时,而插件则是外部调用插件时,在插件退出时去需要解除注册释放资源。Protocol 方案描述如下:

在 Android 端,组件间交互使用的是 ZRouter 组件,实现思路和 iOS 端类似,是参考了 java 中 SPI 机制(服务提供发现机制),每个组件对外提供一个服务接口 service,接口的实现交给对应组件内。在组件初始化注册时候,会同时注册该 service 接口和对应 service 实现。业务方使用时,只需要通过 service 接口调用组件功能。这样组件间就没有了直接依赖关系,实现了组件间解耦隔离。具体调用如下图所示:

3.2 组件路由
组件间路由跳转方面,iOS 端采用了注册 URL 的方式,注册的时机分为静态、动态和懒加载三种,懒加载方式即为在调用跳转方法时检查 URL 与 ClassName 是否已经注册绑定,如果未绑定则从模块静态信息表中获取并完成注册绑定,Handler 可以在动态注册时进行指定,这样跳转逻辑即可实现完全自定义而不走底层的统一跳转逻辑,同样要注意的是插件端需要在退出插件时释放资源并取消注册。

Android 端针对组件间 UI 跳转的实现方面,虽然前面讲到的 ZRouter 也能做到 Activity、Fragment、View 之间跳转,但是代码实现过于复杂。所以我们借鉴了业内组件化的优秀思想,专门开发了一个用于组件间 UI 跳转的 UIRouter。在编译期间,通过 Activity 上添加的 @RouterPath 注解,生成一张 Key 为 Scheme 或页面短码,Value 为 Activity 的路由表。跳转任何一个 Activity 都交由路由框架,根据路由表决定启动哪个 Activity。

为了提升开发效率,减少 UIRouter 初始化时重复开发的代码,我们开发了一个插入自动注册代码的 gradle 插件,利用此插件在编译期通过 ASM 向指定方法中注入初始化代码。
同时,在组件库注册的时候也有用到这个技术;组件初始化类在 debug 模式下通过反射加入内存,在 release 模式下则通过 ASM 插入注册代码。这样在 debug 模式下可以缩短编译时间提升开发效率,在 release 正式包中运行时可减少反射带来的消耗。
整个优化流程如下:

04 构建系统
有了层次清晰的组件划分,那么如何快速构建组件和项目成了必需要解决的事情。针对组件化,爱奇艺知识团队结合公司已有的构建系统,开发了一套适用于组件化的快速构建子系统。
为了解决多端共用一套代码和在各个插件端都有包大小的限制的前提下,在组件库中存在的差异代码通过宏分割来控制,实现差异代码隔离,编译时仅编译当前指定的某一端代码,打包时通过指定打包参数来设置宏配置,完成指定端的构建。
iOS 端每个组件都是一个单独的工程,由不同的 git 私有仓库来管理,各个组件是在主项目中通过 CocoaPods 来集成,将所有组件当做二方库集成到主项目中。爱奇艺知识 APP 与各端插件虽然都采用了 Cocoapods 集成的方案,但是在版本依赖上有所差异,为提升开发效率,知识 APP 作为独立应用程序直接采用了指定 git 仓库 tag 号的方式来依赖组件库,插件则需要通过插件库的 podsec 设置依赖来集成组件库的,这就需要将组件库打成二进制的库文件上传到云,并上传组件库的 podspec 到私有库中。iOS 插件端在主项目中集成组件主要分为两种方式分为源码和 framework,在开发调试阶段采用源码方式,可以直接修改代码完成需求开发,在打包提测和发布时采用生成 framework,可以加快编译速度不会对外暴露源码。

iOS 端选择 Jenkins 作为构建系统,在组件化初始阶段,我们组件的构建是通过先构建最基础的组件,然后再构建上层组件来完成的整体构建,随着组件库数量增多,依赖关系变复杂之后手动逐一触发构建成为了构建过程的痛点,于是开始进行构建优化,引入了 Jenkins 的 ParameterizedTrigger 插件并配合 shell 脚本使用,使得我们支持组件的单个构建的同时,在主项目构建时支持触发多个组件,组件单独构建时也支持配置依赖构建的项目,实现了一次触发完成全部组件的构建。

组件库构建时会对当前迭代分支的代码进行更新检查,如果存在更新则会构建组件库,不存在更新则直接跳过此次构建。随着端的增加构建系统支持了多端构建,iPhone 插件和 iPad 插件为不同的插件 Job,通过脚本实现端的区分完成构建。建系统还实现了版本自增和定时构建,每一次构建完成后都会更新 podspec 文件中的版本号,在下一次构建时如果未手动指定构建版本便会获取之前的版本进行加一实现版本自增。构建系统对接了企业内部的即时通讯工具,在构建完成后发送通知给已经订阅的用户。下图描述了组件构建流程:

在 Android 端,同样每一个业务组件都是一个完整的个体,可以当作独立的 App 来运行,需要满足单独运行及测试的要求,这样可以提升编译速度和开发效率。
目前业界常规做法是每一个组件就是一个工程,由 build.properties 中一个常量控制区分不同场景,且在 build.gradle 中 sourceSets 设置单独调试组件时的配置,区别于发布组件 aar 时候的独立运行时的配置;
但是这种方案对于爱奇艺知识客户端来说并不完全适用,因为我们组件化最主要的一个目的是要达到多端组件复用,这样也会存在需要进行多端适配情况,每一个端的基础配置信息、基础 UI 样式等都有所不同,不可能在每一个新的组件工程中都配置一遍。所以,直接使用原本混沌工程的壳工程作为组件调试的 Project,将 runalone 文件夹设置在各自壳工程中,在根目录 build.properties 中通过常量 isModuleType 控制编译模式,动态加载测试所需组件依赖,这样就可以在各个环境中单独测试组件了。

此时的组件单独调试模式其实等价于理想状态下的组件化壳工程模式:只有少量配置相关代码、无其他组件无关页面逻辑、动态按需加载组件。
在壳工程根目录 gradle.properties 中包含各种常量,包括端控制、组件库版本号、编译环境控制、运行时依赖控制和运行模式控制参数等。

isDependenceMaven 用来控制依赖方式是源码还是远程 maven,开发期间 debug 可以使用源码方式方便调试,正式环境使用远程依赖方式节省编译时间,方便复用。
maven_version 用来统一控制组件版本号,在每一个版本升级时候对应的升级组件库版本和依赖的组件库版本,通过自定义的 maven_publish 上传脚本批量编译上传并更新。
对于 Android sdk 版本和第三方库版本,我们统一抽离到外层的一个 dependencies.gradle 中统一控制,这样能方便且直观管理版本号。
总结与展望
爱奇艺知识移动端已经基本实现了组件化的全部目标,组件间业务边界已经变得非常清晰,做到了一个组件升级多端受益,大幅提高了开发和测试效率。另外,组件增减灵活,使得新增一个业务承载端的成本很低,只需要组合已有组件并对组件做针对这个新增端的修改即可,使爱奇艺知识移动端做到了较少的人力能够支撑更多端的能力。组件化过程中遇到的一些问题已经全部解决,目前组件化从底层的基础组件到上层的业务组件都已经全部上线,组件化构建系统也已经投入生产环境。
当然组件化不是一蹴而就的,而是一个持续的过程,未来还需要不断优化和完善,让组件化在知识业务发展中起到更大的作用。
本文转载自公众号(ID:)。
原文链接:
评论