编者按:从去年开始,关于 iOS 组件化的讨论和分享非常多,也形成了几种比较成熟的方案。组件多了,它们的依赖关系、版本等的管理成为问题,但这方面的分享很少。京东 iOS 不但实施了组件化,还专门开发了一套组件管理系统。希望京东的实践可以给大家一些参考思路。
前言
先大概交代下背景:京东的 iOS 客户端从 2011 年 2 月发布至今已历经 6 年 + 的时间,研发团队也从最终的几个人变成了 N 多人,业务的复杂度早已不可想象。
我个人认为一个超过了 10 人的团队做组件化是合适的,也有必要。当然少于 10 个人也应该去思考一下应用框架该如何演变,组织的这件事。
目标
对于每家应用还得结合实际业务来考虑,毕竟技术最终也是为了业务而服务。京东 iOS 组件化的目的从业务层面来讲主要是为了解决:多业务的并行集成,多部门的业务输出。
从技术层面来讲我们做组件化的目标是为了可以做到减缓代码腐化过程,加快编译时间,模块分权管理,代码规范,bug 减少,独立开发、调试、自动化编译,集成等等工作(好像很厉害的样子)。更近一步的讲,我们需要一套自动化的系统来帮助我们完成所有的组件管理工作,让开发人员能更专注于代码层面,无需关心应用配置,渠道,以及如何集成等问题。
组件管理演进之路
任何事物都有一个演进的过程,就像罗马不是一天建成的一样。iOS 这些年各种技术,花样层出不穷,好多公司,好多大牛在 iOS 组件化方面分享出了好多宝贵的经验,也让我们少走了许多弯路。关于 iOS 组件化的做法每家公司都大同小异,真正需要我们去深挖的应该是怎么把组件有序的管理起来,这也是我们想和大家探讨的内容,在讨论组件管理内容之前我们先简单说明下组件化实现的大概思路。
- 代码解耦
- Cocoapods 管理
代码解耦
首先入手的工作就是代码的解耦,这里其实没有太多的技术含量,只要胆大心细,有计划,一个工程总能被拆成一个个独立的模块。我们把每一个独立的模块就称之为组件,相互之间不能通过硬编码引用的方式进行调用。通信通过自定义的协议进行。
组件原则
- 组件被定义为两种类型的组件:基础组件,业务组件。
- 基础组件可以被业务组件依赖,基础组件不可依赖业务组件。
- 业务组件不可依赖业务组件。
自定义协议
组件之间通信遵循一套自定义的协议通信,实现的方式网上有些开源的项目,我们综合各家实现考虑,最后定出的一个方案:组件间的通信应该是轻量级的,调用完就走,不留痕迹,不需要维护通信数据。
大致实现如下:
(点击放大图像)
我们实现一个JDRouter 的组件,用来实现组件与组件之间的通信。只暴露一个头文件,两个方法:输入,输出(宏规范)。
输入(A 调用B)
router://JDBClass/getString?name=Steven
输出(B 被 A 调用)
+(id)getDataWithString:(NSString *)name { NSString *str = [NSString stringWithFormat:@"HI, %@", name]; return str; }
输入说明
通过 JDRouter 调用,类似于有这样一个方法,完成 a 到 b 的通信
id g = [JDRouter openURL:@"router://JDBClass/getString?name=steven" arg:nil error:nil completion:nil];
输出统一
输入的方法统一了,输出也得统一,没有规矩不成方圆,但又不能通过说教的方式要求大家去提供输出方法。如果有一个统一的办法可以不需要协议注册,协议管理的机制,直接写一个类方法可以让 JDRouter 通过 URI 里的内容可以映射过去就好办了,我们使用宏替换,在 JDRouter 里提供一个输出的规范,类似这样:
#define JDROUTER_EXTERN_METHOD(m,i,p,c) + (id) routerHandle_##m##_##i:(NSDictionary*)arg callback:(Completion)callback
输出规范统一,将上面的类方法变为宏替换输出:
JDROUTER_EXTERN_METHOD(JDBClass, eat, getString, callback) { NSString *str = [NSString stringWithFormat:@"HI, %@", name]; return str; }
AppDelegate 入口解耦
页面容器,第三方 SDK 初始化工作需要在启动时完成,我们通过 hook AppDelegate,将入口所做的工作交给一个组件完成,在该组件中注册其它需要在启动时调用 AppDelegate 方法的组件,让每个组件都可以拥有一类似 didFinishLaunchingWithOptions 方法。
- AppDelegateModule 组件实现 hook AppDelegate
- 入口组件
+ (void) load { NSArray *modules = @[@"MainModule"]; NSString *url = @"router://AppDelegateModule/setDidFinishLaunchingModules"; [JDRouter openURL:url arg:modules error:nil completion:nil]; // NSString *urlrun = @"router://AppDelegateModule/run"; [JDRouter openURL:urlrun arg:nil error:nil completion:nil]; }
在 MainModule 中实现 AppDelegate 的方法
static UIWindow *gWindow = nil; static UIViewController *gTempViewController = nil; + (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { gWindow = [[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]]; [[[UIApplication sharedApplication] delegate] setWindow:gWindow]; gTempViewController = [[UIViewController alloc] init]; gTempViewController.view.backgroundColor = [UIColor redColor]; gWindow.rootViewController = gTempViewController; [gWindow makeKeyAndVisible]; return YES; }
把实例方法改为类方法
将需要调用 appdelegate 方法的组件,在 MainModule 中注册,这里的 modules 是个数据,可解决调用 delegate 方法的顺序问题。
NSArray *modules = @[@“MainModule”, @“需要调用的组件”];
Cocoapods 管理
代码解耦很简单,只要遵循几个原则即可,最根本的问题就是业务与业务之间不能有耦合,不然组件化这件事就没有意义。我们通过 Cocoapods 把每一个组件都拆成独立的 pod 库。代码库管理选择 gitlab(开源,提供 API,可二次开发),后续需要对每一个组件进行权限管理,比如有一些涉及到安全的组件只有安全组的开发人员具体源码权限,其他人只能拿到二进制,再比如组件需要具有 master 的权限才可以进行发布,集成等工作。
关于 Cocoapods,ruby,gitlab 环境大家网上搜索一下。
准备工作
- 通过 Cocoapods 搭建私有库,创建相应的模版。
- 不推荐 Cocoapods 编译二进制文件,自己写脚本编起来更灵活。
- 自定义 gem,完成 podspec 源码二进制切换。
- 二进制文件(组件编译为静态包)存储到内部云。
- 使用工具/脚本管理 podfile。
- 系统管理 pod 库。
- 工程结构
- 通过 Cocoapods 搭的自定义库,自定义模版。
每个同学拿到的组件都是一个相同结构的工程,所需要做的工作就是在相应的 Pods/Developemnt Pods/ 组件 /Classes 下编码,组件输出类通过模版创建,可在相应的类里使用 JDROUTER_EXTERN_METHOD 提供接口。
iBiu 组件管理系统
以上是行业中对 iOS 组件化的一个大体思路。如果我们完全手动做这些工作的话,成本会很大,Cocoapods 配置说明查询,组件版本依赖,统一集成等等。为了解决这些人为干预所引发的各种问题,我们研发了组件管理系统iBiu。
主 App 解耦工作和系统设计是同时进行的,所以最初系统只是为京东 iOS 主 App 所设计,在独立出一些组件后,我们就在思考一件事,让系统可管理京东其它的 App。
去应用化
如果公司的所有应用全部都组件化,并且组件间统一协议通信,那么应用最终的输出方式应该就像工厂加工一样,加工过程就是组件组合过程,出厂时贴上标签。听起来很理想,事实上是可以做到的,从 iBiu 系统上线到目前一个月时间我们接入管理着三个应用包括主 App,超过 100 个组件。
组件配置表
组件如何组合被抽象出一个组件配置表,记录了不同应用的组件配置,对应到具体的组件责任人,版本,对接产品,测试以及开发,通过工具一键完成组件的发布与集成等工作。
(点击放大图像)
上图解释起来就是,假如有一个虚拟的App-A,在它下边有包含了一系列的组件,我们可以通过“组件配置表”(配置表里记录了组件的版本,是否为二进制,依赖等) 对组件进行组合,最终输出我们想要的App。
再将上边的设计升级一下:
(点击放大图像)
让应用可以包含应用(这里所说的应用是一个虚拟的应用,或者叫做 Collection 更合适)。Collection 可以任意包含另外的 Collection,同时可以拿到 Collection 下的组件,如上图,App-A 这个 Collection 最终是有另外三个 Collection 下的组件所组合而成。
系统设计
主要由三大块内容构成:
- 脚本(提供开发环境,如果 pod 管理,git 管理,文件管理等)
- iBiu 工具(可视化)
- iBiu Server(后台管理,API)
(点击放大图像)
我们希望化繁为简,最终开发同学只需要安装一个可视化的工具就可以做到:组件注册,组合,发布,集成等工作,但基础工作还得一步步的来。
第一步:
通过 pod 命令创建相应的组件对应库
pod lib create $lib_name --template-url=${TEMPLATE_URL}
第二步:
将创建组件的脚本封装,我们希望把脚本放到 iBiu Server 这台机器上,让这个过程成为: 开发者注册 -> 审核 -> 自动生成组件 -> 代码提交 Gitlab-> 通知开发者。
问题一个接一个的出现了:
坑:由于公司网络原因,不同网段同学无法访问一台工作网络环境中的机器。
坑:脚本创建组件这个过程依赖 Xcode 环境。
也就是说我们无法用一个 Mac 机器充当服务器,但又必须要 Xcode 环境。第一想法就是把 iBiu Server 部到线上环境,找一台 Mac 机器把脚本放上去,让 iBiu Server 访问这台 Mac,但线上环境是完全返向不回来工作网络的。
坑:换线下环境,部署 iBiu Server,还是访问不了工作网络中的这台 Mac。
最后只能在 iBiu Server 这台机器上部了套 Jenkins,生成组件的脚本部在 Mac 这台节点上,问题解决。(这块一直是个不太理想的做法,为了解决问题也只能这么做了)
第三步:
统一用户体系,ERP 账号与 iBiu、Gitlab 打通。
第四步:
修改 Podfile
如果是手动引入组件的话,每一个组件对应的 Podfile 可能会是下图这样,该组件所有的开发者,对该组件引入修改都有可能造成冲突。
(点击放大图像)
如果可以通过Podfile 读取到“组件配置表”,而配置表又可以同步的话,能解决的问题就不只是冲突的问题了。
我们创建了一个biu gem,用来去处理配置表解析的工作,Podfile 最终变成下图的样子,开发者无需手动修改它。
(点击放大图像)
所有工作都通过mekeup_pods 这个方法完成。
第五步:
我们需要把每个组件在发布时都对应二进制文件输出,有关xcodebuild 打包二进制的脚本就不在这里描述了。
将二进制统一输出为一个xxxx.framework,也方便查看
(点击放大图像)
脚本会把每次编译的负责人信息写到xxxx-umbrella.h 文件中,方便问题跟踪。
(点击放大图像)
第六步:
将所有shell 脚本通过Packages 打包,提供给所有开发者可以直接使用。
第七步:
iBiu 可视化工具 iBiu Server 端开发,工具主体功能的开发时间并没有花了太多的时间,主要时间还是花在了梳理整个管理流程上。
第八步:
合并 iBiu 脚本,可视化工具,Packages 打包。
(点击放大图像)
组件管理实践
系统及可视化工具
通过工具申请组件
(点击放大图像)
开发者选择对应的App,输入相应的组件名等信息,将注册信息提交给iBiu Server,系统管理员会收到注册邮件,在后台完成审核。同时会系统邮件发给相应的开发者。
审核过程通过前面提到的,系统会自动完成创建pod 库,git 库,开发者权限分配等操作。
组件申请人即是该组件的负责人,拥用开发者权限分配,组件发布,集成,协议管理等权限。
后台管理开发者权限分配:
(点击放大图像)
开发调试组合安装
开发者收到组件审核邮件后,根据git 地址拉取代码,通过iBiu 可视化工具打开组件工程,勾选要组合的组件进行安装。
所勾选的组件可以根据当前用户的权限分源码与二进制两种方式,是否依赖。
如果某用户所组合的某个组件需要调试,但没有源码权限,可找该组件的负责人要求开通权限。
(点击放大图像)
生成组件配置表
(点击放大图像)
根据所勾选的组件,生成一张组件配置表,这时所组合安装生成的App 即是一个根据配置表生成的工程。
坑:配置表是脱离组件工程独立存在的一个JSON 描述文件,因为每个开发者对每一个组件所拥有的权限不一样,如果把配置表随工程代码一并提交,很可能造成表冲突,安装不了组件。解决办法就是使拥用master 权限的用户可以将表内容同步至服务端,组件参与者下载同步表。然后在几个关键点验证表中组件对应权限,没有源码权限的提示用户自动切为二进制方式。
组件安装过程
(点击放大图像)
配置版本组合安装
拥有组件master 权限的用户可以将相应组件发布,集成到某个应用下的某个版本。
我们的应用迭代周期一般为一个月左右,对应到不同的项目,不同的产品,研发及测试。通过iBiu 系统可以做到并行版以本开发,测试,集成。
组件的发布,集成不受项目的时间限制,随时可以发布。
App 整体集成,组件的集成需要按项目流程进行,组件需要在 App 回归测试时前两天集成。点击 iBiu 可视化工具右上角集成按扭,选择组件版本,集成到某个应用的某个版本中。
每个应用的每个版本,在组件集成阶段生成相应的组件配置表,超过这个时间,应用版本锁定,组件将无法再集成到该应用版本中。
例如应用“京东”当前要发布 AppStore 的版本为 6.1.3,发布时间 7 月 19,倒推时间,灰度 1 周,集成测试为 1 周,那么所有该版本的组件必须在两周前两天完成集成。过期不候,只能按组件上一个稳定版本集成,如果有特殊情况需特殊对待。
JDRouter 协议管理
每个业务组件都应该提供 JDRouter 接口,通过后台管理这些接口,方便所有开发者查询。
(点击放大图像)
持续集成改造
应用以前是可以持续集成的,iBiu 系统组件管理后需要对持续集成系统做一些改造。在相应的节点机器上安装iBiu 安装包,运行后自动安装环境。
CI 通过 iBiu Server 所提供的 API,脚本检测创建 XXXXAppModule(空组件),获取某个应用的某个版本的组件配置表安装组件,完成打包工作。
统一配置
组件化带来的一个问题,每个组件独立存在,工程结构类似于这样:
(点击放大图像)
Example for XXXX 是当前组件在开发过程中所用来测试,验证的 demo 代码。
Pods 为最终组件输出实际工程区域。
编译组合的组件,输出最终的 App,如果在某个版本需要更新 icon 或启动图怎么办?总不可能让每个组件在 Example 中修改这些资源或数据。
有没有可以统一这些配置的办法,而又没必要让开发者人为的替换管理?
答案是肯定的,我们在 iBiu 可视化工具中加入了应用版本的配置拉取脚本功能。简单点讲就是安装组合时执行远程脚本。在 iBiu 后台针对应用版本配置相应的脚本,可以修改工程任意配置。
(点击放大图像)
这是一段检测替换icon 的脚本,通过安装组件时可执行远程脚本,能做的事情不仅限于替换个图片这么简单,由于可执行远程脚本权限太大,该功能使用还是需要谨慎。
最后
该文章主要描述京东iOS 组件管理的解决思路,由于内容过多,无法展开细讲每个环节,读者朋友如果对某些细节感兴趣可以留言,我们有针对性的另开专题。
感谢徐川对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论 3 条评论