导语 | 企业微信 iOS 端作为代码超过 800 万行的大型项目,接入了腾讯会议、腾讯文档、企业邮箱等性能插件。要交融多个异构零碎、撑持多个团队同时合作开发一个 APP 是极大的挑战。同时,迅速收缩的代码量和功能模块数量给企微团队带来了编译耗时大增、模块耦合重大等累赘。为了适应业务的高速倒退,企微团队进行了组件化、插件集成能力建设工作。本文将进行具体介绍。
问题与挑战
随着企业微信业务的疾速迭代,企业微信 iOS 客户端工程成长为一个超过 800 万行代码的大型项目。因为 B 端需要多样化,企业微信不可能实现全副 SaaS 性能,多强联结是将来竞争方向上的必选项,企微团队须要的是一个航母级能够搭载其它业务的平台型 APP。同时企业微信客户端内融合了 腾讯会议、腾讯文档、企业邮箱 等性能,要交融多个异构零碎、撑持多个团队同时合作开发一个 APP 是极大的挑战。
迅速收缩的代码量和功能模块数量带来了一些新的问题:开发编译速度慢,全量编译耗时约80 分钟,更新代码编译耗时通常超过20 分钟;Xcode 工程文件体积迅速收缩,呈现工程加载耗时长,批改工程文件卡顿,编写代码时代码提醒、断点调试响应慢等问题;模块之间耦合重大,相互依赖关系简单,没有明确的架构分层,导致批改组件外部性能影响其它组件性能的问题,减少了代码的保护难度和测试的工作难度。
面对业务倒退带来的问题和挑战,原工程曾经不能满足以后需要。在这个背景之下,去年企微团队启动了企微 iOS 工程专项革新工作,通过一年的致力,实现了企微局部模块组件化、会议 / 文档 / 邮箱插件接入、Bazel 工程迁徙等工作。本篇将具体介绍模块组件化和插件接入的摸索与实际,如果您对其余我的项目感兴趣,欢送留言并继续关注本公众号。
组件化摸索与实际
2.1 架构介绍
针对历史架构的缺点,企微团队梳理了外部业务模块、根底模块、公共模块之间的关系,还思考了会议、文档、邮箱插件和企微平台之间的分割,引入了组件管理中心来做组件解耦,提出了企业微信 iOS 架构框架,如下图所示:
架构分为四层,通用层、通用底层、UI 框架层、功能模块,其中通用层、通用底层用 C++ 编写,次要实现网络、db、日志、线程模型等通用能力,以及通用的业务能力接口,能够做到跨 iOS、Android、Mac、Win、linux 5 平台代码复用。各个平台在通用底层的根底上实现各自的 UI,iOS UI 层用 OC 编写业务组件,组件管理中心 为组件提供生命周期治理、组件间通信、告诉治理等能力,插件能够复用各个组件提供的接口,集成到企微的业务中来。
2.2 组件化工作拆解
通过架构梳理,一共梳理出 70 多个组件 ,其中蕴含约 1.7 万个源码文件 和 800 万行代码,面对如此宏大的工程,重构工作将会带来不少的开发、测试工作量。团队不可能欲速不达,一次性实现整个工程的重构和解耦,须要有一套可行的计划来逐渐实现。
企微团队将组件化工作拆解为 4 个阶段:
第一阶段,根底能力建设 :实现组件治理容器,为组件、插件提供生命周期治理、组件间通信、告诉监听等根底能力;第二阶段, 物理目录拆分 :依据后期布局的组件,为每个组件新建一个独立文件夹,将属于组件的代码归拢到一处,从物理上实现隔离;第三阶段, 剖析组件之间的依赖关系 :依赖关系次要分为两类,组件内部依赖接口和对外裸露的接口。通过梳理依赖关系,企微团队能够分明看到每个组件的耦合水平以及革新的难度和工作量,耦合越重大的组件革新工作量和影响面越大,同时通过依赖关系还能精确定位到须要改变的代码地位;第四阶段, 组件拆分:依据依赖剖析的后果施行组件化,封装组件对外裸露接口,将组件间调用从间接援用形式改为接口调用形式。
2.3 组件化根底能力建设
如下图所示,组件管理中心 ModuleManager 具备以下能力:
组件生命周期治理 :组件须要在 ModuleManager 注册,并实现相应接口,实现组件初始化逻辑、组件生命周期治理逻辑; 组件间通信 :组件提供对外能力接口,并实现这些接口,组件间能够用通道互相调用; 零碎事件 / 利用事件告诉 :零碎事件(利用启动、前后台切换、后盾利用刷新、收到 APNS 等),利用事件(账号切换等)告诉机制。组件能够监听相应事件,在事件产生时执行本人的逻辑; 隐衷权限治理 :例如手机零碎相册权限、定位权限、通讯录权限申请及应用,组件如果须要应用设施隐衷相干的权限,须要向组件管理中心申请,对立治理敏感操作; 多账号数据隔离:多个账号切换时要保障不同账号的数据隔离,由组件管理中心保障不同账号不会串数据。
对于组件间通信计划的抉择,曾经有不少成熟的组件间通信计划,企微团队抉择了基于协定的服务注册计划。组件间通信模型如下图所示,每个组件对外裸露一组 Protocol,而后在组件外部实现对应接口。如果组件 A 须要调用组件 B 的接口,首先通过 ModuleManager 拿到组件 B 的接口实现对象,而后就能够调用组件 B 的接口。
以下代码示例展现了一个接口的定义、实现、调用的残缺流程。
// 文件:WWKUtilityServiceProtocol.h
@protocol WWKUtilityServiceProtocol <NSObject, WWKServiceProtocol>
/// 获取 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 设计的,不反对跨平台,无奈满足需要,最终企微团队抉择了一条不同的路。如果您感兴趣,欢送留言并继续关注本公众号,咱们将继续输入系列内容。
兔年在即
腾讯云开发者福利加码
独家限量红包封面 来啦
关注 腾讯云开发者公众号
后盾回复888
参加福兔红包封面抽奖
你可能感兴趣的腾讯工程师作品
| 你的 2022 年度开发者关键词,请查收 >>
| React 语境下前端 DDD 的长年摸索教训
| 国民级利用:微信是如何避免解体的?
| 从 Linux 零拷贝深刻理解 Linux-I/O
技术盲盒:前端 | 后端 |AI 与算法| 运维 | 工程师文化