导读
「快手Swift历险记」专题主要讨论Swift 编译模型的一些原理,以及快手在接入Swift过程中所遇到的各种坑和快手客户端基础架构团队对其的解决方案
本文探讨接入Swift后开启Clang Module后所踩的“坑”以及解决方案,方案原理非常简单,主要是打破了接入Swift过程中的一些经验“误区”。
快手在21年5月开始引入Swift,为了让业务同学在各类场景更便捷地使用Swift,一开始采用混编方式接入Swift。然而在接入Swift的过程中,开启Clang Module对编译构建耗时产生了极大的劣化,我们用一种简单的方案解决了这个问题。下面让我们来回顾一下这个解决方案的具体细节。
Clang:Xcode的默认编译器,用来编译Objective-C、C和C++代码。
Swiftc:Swift编译器,目前为swift-frontend。
Clang Module:Clang 中一种模块化的编译方式,不同于单个.h文件的引入,引入一个Module 直接引入了多个.h的集合。在一次构建过程中,一个Module一般只需要编译一次生成pcm产物,后续编译过程中可以直接复用这个pcm产物,从而提高编译效率。
Module Map:一种用于描述Clang Module中包含的.h头文件集合以及它们之间依赖关系的文件。它通常是一个名为module.modulemap的文本文件。
$(SWIFT_MODULE_NAME)-Swift.h:Swift 编译过程中生成供 Objective-C调用的Public API 头文件。
PCH:预处理文件,原理和Clang Module类似,属于Clang Module的前身。
快手工程在开启Clang Module后出现了许多问题。下文将以快手引入Clang Module时遇到的典型问题为例进行阐述。
编译错误:Module 具有传递性,当前Objective-C组件开启Module后,所依赖的库也需要Module化,因为Module Map的原因使得之前只用依赖一个.h变成依赖Module Map中记录的所有.h导致无法编译通过,这种情况在底层c++库中非常频繁。
Module 编译慢:因为Module 具有传递性,在快手的工程中import单个Module会编译大量的隐式依赖Module,而编译参数不同也会导致Module编译多次(是否开启异常、预编译宏不同等会导致生成的pcm产物不同),上线后直接导致快手工程CI打包P50耗时增加50% (4min -> 6min)。
PCH 编译优化失效:快手工程使用定制的PCH文件使得全源码编译耗时降低40%,而Module和PCH存在重复&部分效果冲突,虽然Module和PCH原理类似,但是Module在search path等细节的处理与PCH不同,业界也有针对Module优化的hmap等优化方案,但依然远远无法达到快手使用的定制PCH优化效果。
.o文件级别缓存命中率大幅降低:快手集成了通过分析当前.m以及所依赖的所有.h文件进行文件级别的编译远程缓存,import Module后从import 单个.h扩大到此Module Map文件所有的.h,大幅增加了无效依赖,导致命中率大幅降低。
Module 复用率低 & 过期导致编译失败:相同的编译参数只会产生一个pcm文件,而所依赖的头文件变更后pcm文件就会更新,导致业务Module pcm产物基本无法复用。同时pcm 存在一个过期时间,在CI编译过程中pcm过期后此次编译会直接失败,大幅增加了oncall 成本。实践过程中每次构建前会直接删掉pcm产物,这样CI打包过程中都会重编Module。
因为Swift 使用Module的编译方式,我们往工程中开启了Clang Module。对此我们产生了一个疑问,只是Swift编译需要Clang Module,为何我们需要在Clang 编译过程中也开启Clang Module? 我们能否针对Clang 编译的场景关闭Clang Module 和之前完全保持一致? 因此我们在下述场景中进行了测试实践。
Objective-C call Objective-C
关闭Module后Objective-C call Objective-C 与之前完全一致,正常的#import "OCPodA.h"即可,无需做任何改动。
Swift call Objective-C
针对Objective-C Pod 我们依然保留Module Map & Umbrella Header文件,这样编译Swift文件时,此Objective-C Pod OCPodA依然可以正常生成pcm文件,供Swift 代码调用。
#OCPodA.modulemap
framework module OCPodA {
umbrella header "OCPodA-umbrella.h"
export *
module * { export * }
#OCPodA-umbrella.h
FOUNDATION_EXPORT double OCPodAVersionNumber;
FOUNDATION_EXPORT const unsigned char OCPodAVersionString[];
Swift call Swift
Swift call Swift 不涉及到Objective-C代码,会生成Swift Module文件供Swift代码之间互相调用,因此不受Objective-C Pod关闭Module影响。
Objective-C call Swift
Swift 编译过程中会生成$(SWIFT_MODULE_NAME)-Swift.h文件暴露Public API供Objective-C 调用。通过分析$(SWIFT_MODULE_NAME)-Swift.h文件,我们发现Apple生成的$(SWIFT_MODULE_NAME)-Swift.h文件也支持在不开启Clang Module模式下使用,代码中通过#if __has_feature(modules) 来控制。
开启Clang Module模式下,因为SwiftPodB 依赖 OCPodA,因此走到@import OCPodA代码逻辑中。
#SwiftPodB-Swift.h
@import Foundation;
@import ObjectiveC;
@import UIKit;
@import OCPodA;
关闭Clang Module 模式下,SwiftPodB-Swift.h会采用前置声明暴露所依赖的Objective-C API
@class UITouch;
@class UIEvent;
@class OCPodA;
SWIFT_CLASS("_TtC6SwiftPodB13SwiftPodB")
@interface SwiftPodB : OCPodA
@property (nonatomic, copy) NSString * _Nonnull str;
@end
因此我们只需把Objective-C文件依赖Swift代码的引入方式改造成下面示例即可。
#开启Clang Module的import方式
@import OCPodA;//与#import <OCPodA/OCPodA.h>效果一致,Clang有做自动转换成import Module的功能。
@import SwiftPodB;
#不开启Clang Module的import方式
#if __has_feature(modules)
@import OCPodA;
@import SwiftPodB;
#else
#import "OCPodA.h"
#import "SwiftPodB-Swift.h"
#endif
Objective-C call Swift call Objective-C 等
针对OCPodA call SwiftPodB,SwiftPodB call OCPodD/SwiftPodE ······这种可以无限延伸的多层依赖场景,都可以简化成Objective-C call Swift call Objective-C依赖类型的扩展。通过上述Objective-C call Swift 不使用Module模式方案介绍,$(SWIFT_MODULE_NAME)-Swift.h会把对Objective-C的依赖改造成前置声明的方式,因此在最上层的Objective-C代码中import下层的Objective-C头文件即可。
#比如Swift Pod SwiftPodB依赖了OC pod OCPodA,因此在import SwiftPodB-Swift.h之前需要先import OCPodA.h
@import SwiftPodB;
Objective-C依赖Swift代码经过简单的改造后,Clang关闭Clang Module方案于21年10月上线,上线后CI打包耗时回到接入Swift前的4min,同时上述引入Clang Module带来的问题都迎刃而解。Swift代码开发过程中Pure Swift Pod & 混编Swift Pod使用方式与之前无异。
开始接入Swift的过程中,我们发现网上搜到的博客文章和业界的实践都提到需要开启Clang Module。在实践过程中抱有一定的怀疑态度也许能打开我们解决问题的思路。
快手在接入Swift后遇到了二进制打包、依赖分析、插桩、自定义Toolchain&文件级编译缓存等许多问题。后续会继续介绍我们如何踩过这些“坑”。
欢迎加入
快手主站技术部客户端团队由业界资深的移动端技术专家组成,通过领先的移动技术深耕工程架构、研发工具、动态化、数据治理等多个垂直领域,积极探索创新技术,为亿万用户打造极致体验。团队自2011年成立以来全面赋能快手生态,已经建立起业内领先的大前端技术体系,支撑快手在国内外的亿万用户。
目前团队正在招贤纳士,欢迎2023届校招生投递简历~!
我们期待您的加入!请发简历到:
tangruo@kuaishou.com