导语 | 企业微信 iOS 端作为代码超过800万行的大型项目,接入了腾讯会议、腾讯文档、企业邮箱等功能插件。要融合多个异构系统、支撑多个团队同时协作开发一个 APP 是极大的挑战。同时,迅速膨胀的代码量和功能模块数量给企微团队带来了编译耗时大增、模块耦合严重等负担。为了适应业务的高速发展,企微团队进行了组件化、插件集成能力建设工作。本文将进行详细介绍。
目录
1 问题与挑战
2 组件化探索与实践
2.1 架构介绍
2.2 组件化工作拆解
2.3 组件化基础能力建设
2.4 组件目录拆分
2.5 组件依赖关系分析
3 插件集成
3.1 背景及方案
3.2 插件开发壳工程
3.3 WeComKit 介绍
3.4 插件开发流程
4 总结思考
2.1架构介绍
2.2 组件化工作拆解
// 文件:WWKUtilityServiceProtocol.h
@protocol WWKUtilityServiceProtocol
/// 获取 string 类型的 systemconfig
- (std::string)stringSystemConfigForKey:(NSString *)key;
@end
// 文件:WWKUtilityService.mm
@implementation WWKUtilityService
- (std::string)stringSystemConfigForKey:(NSString *)key {
return "config";
}
@end
// 调用方
[WWKFindService(WWKUtilityServiceProtocol) stringSystemConfigForKey:@"key"];
2.4 组件目录拆分
完成组件管理中心后,为实施组件解耦,首先要将组件代码从物理路径上分隔开。根据之前架构的梳理,企微团队将代码分为若干个组件,每个组件为一个独立文件夹,将代码移动到对应目录。挪动文件的物理路径会遇到头文件找不到的编译报错,企微团队编写了一个工具自动修正头文件路径来辅助完成拆分工作。 2.5 组件依赖关系分析 组件物理目录拆分之后,就要进行代码逻辑的解耦合,如果是一个新项目或小型项目可以直接封装接口。但是企微有大量历史代码要处理,需要一套可行的方案来获取修改列表、评估解耦每个组件的工作量、确定改造工作需要投入多少人力和时间完成,并辅助开发进行修改工作,通过分析组件的依赖关系可以获取到组件代码逻辑解耦列表。 分析组件的依赖关系,企微团队可以从组件内文件的依赖关系入手,依赖关系分为两种,第一种是组件暴露给其它组件依赖的符号,第二种是组件依赖其它组件的符号,在探索分析依赖关系方案时,我们共想到三种方案,分别是:分析头文件依赖、分析链接日志、解析 AST,前两种方案简单易实现,但是得到的结果精度不够,不能满足企微团队的需求,最终企微团队选择了解析AST方案,使用 Clang LibTooling 编写工具,通过解析 AST 来分析依赖关系。下面展开讲讲三个方案的流程及优缺点。 方案一:分析头文件依赖
企微团队首先想到的方案是解析源码依赖的头文件,解析流程如下图所示。 首先,执行一次完整的编译,得到编译中间产物“.d文件”,它包含了编译一个文件所需的所有头文件;其次,解析“.d 文件”,得到源码文件直接依赖、间接依赖的所有头文件,这里的解析比较简单,用脚本逐行读取就可以完成;最后,过滤组件内部头文件、系统 SDK 头文件,得到组件外部依赖的头文件列表,通过分析头文件所属组件得到组件间的依赖关系。 编译中间产物示意图: .d 文件内容示意图: 该方案的优点是原理和实现方式比较简单,只需对编译产物进行简单的解析即可得到结果;缺点是得到的数据粒度太粗,依赖关系只能精确到文件,不能精确到具体符号。对于改造工作有一定指导意义,可以得到一个模糊的关系图,细节还得人工筛选一次,不能满足企微团队的需求。 方案二:分析链接日志
企微团队在开发过程中经常遇到“Undefined symbols”类型的链接报错:
Undefined symbols for architecture arm64:
"_OBJC_CLASS_$_XXX", referenced from:
objc-class-ref in XXX.o
ld: symbol(s) not found for architecture arm64
这个报错原因是链接过程中符号缺失,报错日志会把所有缺失的符号列出来,企微团队可以利用这个报错信息获得组件链接过程中依赖的符号,间接分析出依赖信息。 举个例子,要分析“组件A”对外依赖、被外部依赖的符号信息,可以按照以下步骤完成: 首先,构造一个子工程。子工程仅包含“组件A”的代码,工程的产物是一个动态库,由于“组件A”依赖了其它组件的符号,但是其它组件没有参与编译链接,所以在链接时会报错,错误类型是 “Undefined symbols”,用脚本解析日志可以得到“组件A”对外依赖的所有符号;然后,同理,将“组件A”源码从主工程中去掉,形成一个子工程,然后编译工程,链接时同样会报错 “Undefined symbols”,用脚本解析报错日志可以得到“组件A”被外部依赖的所有符号。 该方案优点是粒度能精确到具体符号,实现也比较简单。通过构造特殊的工程,解析链接报错日志就能得到结果。缺点是方案不够通用,如果要解析整个工程组件间依赖关系,需要构造大量的子工程,且结论要编译、链接完成后才能得到,效率很低;同时该方案得到的结论粒度不够细,只能精确到符号,没有符号所属源码文件、行号列号等信息,不能满足需求。 最终方案:解析 AST。LibTooling 是 LLVM 工具链里的接口,它提供了强大的 AST 解析和控制能力,用于编写基于 Clang 能力的独立工具。企微团队可以基于它的 ASTMatcher 编写工具解析源码,得到函数定义、函数调用等信息,从中可以分析出组件的依赖关系。
举个例子演示它的能力,假如企微团队有下面一段代码,想要提取出其中的函数调用 ModelA *model = [[ModelA alloc] initWithStr:@”AAAAA”]; // 示例源码
@implementation Demo
- (void)viewDidLoad {
[super viewDidLoad];
ModelA *model = [[ModelA alloc] initWithStr:@"AAAAA"];
}
@end
用下面的 Matcher 语句就可以达到企微团队的目的。 // Matcher
objcMethodDecl(
hasAncestor(
objcImplementationDecl().bind("myClass")
),
forEachDescendant(
objcMessageExpr().bind("funcCaller")
)
).bind("mySelector")
使用工具 clang-query 可以快速验证 matcher 是否符合预期,解析结果如下图所示: clang-query -p /xxx/xxx/compile_commands.json /xxx/xxx/Demo.mm
> set bind-root false
> set print-matcher true
> enable output dump
> set traversal IgnoreUnlessSpelledInSource
> m objcMethodDecl(hasAncestor(objcImplementationDecl().bind("myClass")),forEachDescendant(objcMessageExpr().bind("funcCaller"))).bind("mySelector")
理解了 ASTMatcher 的使用方法,接下来就是编写工具完成解析工作。工具解析流程如下:首先,使用 ASTMatcher 编写 Matchers 从 AST 中匹配企微团队需要的节点,提取出每个文件的函数定义/调用、变量定义/调用、类定义/引用列表,列表中还包含每个符号的代码文本,及所属文件路径,文件行列号等信息;然后,比对符号使用文件与符号定义文件所属组件,可以区分是外部依赖符号还是内部符号,从而分析出文件之间的依赖关系,最终汇总成组件间的依赖信息。 最终每个组件会生成两个表格,对外暴露符号和外部依赖符号,如下图所示,表格中包含符号定义的文件路径、行号、列号,使用符号的文件路径、行号、列号,以及符号的定义代码、使用符号的代码等信息。 6)组件拆分
完成了组件依赖关系分析之后就可以启动组件拆分工作了,组件拆分工作需要投入大量人力完成,开发同事根据依赖关系输出的表格找到需要改造的代码位置,然后动手封装接口,修改接口调用方式,完成代码逻辑的解耦。 企微团队选择了依赖相对简单的组件作为试点验证方案的可行性,在实施过程中不断完善方案,逐步完成整个工程的组件化。在实施过程中企微团队发现有很大一部分接口属于胶水代码,封装工作简单重复,这类简单的接口可以用工具来生成代码,从而进一步减少人工工作量,这是后续的一个优化方向。 插件集成 3.1 背景及方案
企微作为一个平台型 APP ,要具备集成会议、文档、邮箱等多团队协作开发插件的能力,由于这些业务前期不是基于企微架构进行开发,有独立的架构和技术栈。 在组件化的基础上,企微团队为外部插件提供了集成的能力,将新插件看做一个组件集成到企微 APP 中,插件通过 ModuleManager 调用组件暴露出一系列能力接口,插件也可以在 ModuleManager 注册接口,供其它组件调用。 插件开发涉及到多团队协作,不同开发团队有各自的代码仓库、开发工程、规范流程等,如何融合多个插件、让开发流程更顺畅、高效的运转是一个不小的挑战。 传统的 SDK 开发模式如下图所示,SDK 开发同事一般会写一个 Demo 工程来调试 SDK 功能,开发完成后由集成方接入 SDK,调用 SDK 提供的接口,在集成方工程联调接口。SDK 开发环境对于集成方是无感知的,不会依赖集成方的环境和数据。 这种方式在标准化 SDK 场景下是没有问题的。但是企微在集成会议、邮箱、文档插件时,插件侧要进行深度的业务融合和定制化开发,插件开发同事需要使用企微的账号体系、数据进行调试,很难构造一个 Demo 工程模拟联调环境。 针对这种特殊的合作背景,企微团队提出了一种新的开发模式,如下图所示,先将企微的核心能力打包为一个 SDK,集成到插件开发壳工程中,插件开发完成后打包成 SDK 集成到企微工程中。通过双向接入对方 SDK 的方式,实现了开发、联调环境的统一。 3.2 插件开发壳工程
为了解决外部插件开发、联调效率问题,企微团队搭建了一个专门用于插件开发的壳工程,可以做到无企微代码启动企微 APP,具备大部分企微能力,使用真实的环境、数据进行联调。在这个壳工程的基础上就可以开发新的插件。它具备以下特点:不依赖企微代码;开发联调环境对齐企微主工程;工程轻量,编译速度快;跨团队协作开发效率高。 壳工程如下图所示,工程由插件源码、图片/文案等资源文件、WeComKit、动态库组成。 3.3 WeComKit 介绍
WeComKit 是企微基础能力 SDK,它是插件开发壳工程的核心。它将企微主要能力打包成一个动态库,以 API 的方式暴露接口供外部插件调用,插件通过 ModuleManager 可以调用企微组件的接口。 打包 WeComKit 动态库时遇到一个问题,主工程依赖了部分插件的符号,打包 WeComKit 时不会链接插件的符号,因此会报错 Undefined symbols,需要在链接时使用参数 -undefined dynamic_lookup 开启符号动态查找,可以解决这个问题。 3.4 插件开发流程
插件开发流程如下图所示:首先,将主工程组件、组件管理中心、插件、对外能力接口、资源文件等打包为 WeComKit;其次,将 WeComKit、主工程资源文件、主工程依赖的三方动态库接入到壳工程中,在壳工程里开发插件功能;最后,插件开发完成后,将代码、头文件、资源文件打包为 PluginFramework,集成到主工程中。 最终为了让流程自动跑起来,企微团队搭建了两条蓝盾流水线。它们分别用于打包 WeComKit 和 PluginFramework。值得一提的是,流水线定期执行更新主工程、壳工程里使用的 Framework。 总结思考 在组件化的过程中,企微团队发现了面对企微这种体量大、需求复杂的工程,传统的 Xcode 工程显得有些力不从心。它有工程卡顿、配置难以维护、工程不够灵活、编译慢等问题。业界常用方案是使用 CocoaPods 来管理组件化工程,但它是针对 Swift 和 Objective-C 设计的,不支持跨平台,无法满足需求,最终企微团队选择了一条不同的路。如果您感兴趣,欢迎留言并持续关注本公众号,我们将持续输出系列内容。 本文转载自钟亮 腾讯云开发者,原文链接:https://mp.weixin.qq.com/s/RexzyKduzMVf3DpCH-nBfQ。