@zb5228126
2017-08-14T07:34:03.000000Z
字数 3591
阅读 3276
作者Brian Zhang,译者:张斌
Airbnb认为开发人员的体验是良好工程设计的关键,特别是移动开发者Infra团队,该团队的目标是优化移动应用程序的构建时间。
6月份,我们通过Buck(buckbuild.com)成功构建了iOS应用。这对我们来说是一个巨大的里程碑:当开始工作的时候,Buck并不支持混合语言的iOS项目,而我们的iOS代码库由Swift和Objective-C均匀混合组成。但由于成功使用Buck,我们的CI构建速度提高了50%,而应用程序的大小也缩小了30%。
达到这一里程碑是一个复杂的过程。在本文中,我们将分享所面临的挑战的技术细节,以及我们如何在iOS代码库中使用Buck。希望这对对类似工作感兴趣的其他人有用。
当研究Xcode如何构建iOS项目时,我们发现了这个非常棒的帖子,其中详细说明了该过程。
简而言之,构建过程包括以下步骤:
- 将桥接头文件(由作者维护)和Swift源文件传递给swift工具,生成两种文件:
a).o,用于生成最终可执行二进制文件的机器码文件.
b) 包含所有Swift代码中定义的类和接口的*-Swift.h文件。这些文件可以被显式导入到要使用Swift代码定义的功能的Objective-C文件中。
- 将所有Objective-C源文件和*-Swift.h文件传递给clang工具,它会为每个Objective-C文件生成.o文件。
- 将所有.o文件传递给ld命令,链接所有机器代码文件并生成最终的可执行文件。
Buck使用与上述大致相同的过程来构建iOS项目。然而,有一个非常重要的区别,使得支持我们这样的混合语言项目更具挑战性。
Xcode独立构建每个模块,并生成动态链接的框架。对于特定模块M,可执行二进制文件和相关资源/资产最后会保存在最终应用文件夹中的App.app/Framework/M.framework里面。
Buck将这些模块视为静态库,将它们全部连接在一起并生成单个可执行二进制文件。这种方法可以有效地减少二进制文件的大小,因为:
a)如果多个模块使用相同的资源/资产,则不需要将相同的文件复制到每个*.framework文件夹。
b)它可以剥离更多未使用的符号,因为所有库都静态地链接在一起。
这非常适合仅有Objective-C或Swift的项目。不幸的是,这种优化在混合语言项目中会出现问题。
-import-underlying-module构建标志会导致在同一模块内Objective-C文件会被隐式导入Swift。不幸的是,这个标志在Buck里不起作用。
当Xcode生成框架时,它会生成module.modulemap和.hmap头映射文件以指示头位置。稍后,swift工具将使用这些文件以导入Objective-C头。但是,由于Buck不生成独立的框架,所以它不会生成这些文件。因此,-import-underlying-module标志在swift工具中不起作用。
这意味着必须将桥接头文件显式传递给swift工具。然而,这样做会导致更多的问题。
考虑这个例子:A.h包含行#import“B.h”,但B.h却放在folderB/下。这在Xcode下能够完美工作,因为有.hmap。但在Buck里,这不行,因为它找不到B.h。
在这个PR中,我们更新了Buck,以允许它生成头映射供Swift工具使用,以便工具能找到头文件并导入它们。
当swift工具生成*-Swift.h文件时,它会显式导入用于Objective-C定义的桥接头。这将导致Buck构建失败。
根据Apple的代码,当使用-import-underlying-module标志时,生成的*-Swift.h文件会将项目头导入。例如:
#import <Project/Project.h>
当提供桥接头(如在Buck中一样)时,生成的*-Swift.h文件将直接导入桥接头文件:
#import "ios/Project/Project-Bridging-Header.h"
你可以看到,导入的路径是相对的。当另一个文件导入此*-Swift.h文件时,它无法找到桥接头。
在这个提交中,我们更新了Buck,将-iquote buckRootPath传递为编译器参数。它能具体地告诉swift工具去查找buckRootPath内的桥接头文件。
将头文件导入到Objective-C有#import和@import两种方法。@import在Buck中不起作用,因为Buck不生成module.modulemap。
这需要将@import M替换为#import <M/M.h>和/或 #import <M/M-Swift.h>。 这对于我们的源代码来说比较简单。然而,这对于生成的代码更加棘手。例如*-Swift.h文件始终使用@import。
为了解决这个问题,我们使用了一个公认的hack(修改)解决方案,引入这个脚本来即时执行替换。在这个提交中,我们向Buck的apple_library构建规则添加了一个新的objc_header_transform_script参数,这个参数使得我们可以在所有*-Swift.h文件上调用替换脚本。这一变更扫除了我们使用Buck的最后一个大障碍。
为了说明我们所做的工作,我们创建了这个示例项目,你可以随时复制它,并自己测试!
如前所述,当构建混合语言库时,需要将桥接头文件传递到apple_library构建规则中。
apple_library(name = 'ImportObjC',visibility = ['PUBLIC'],bridging_header = 'bridging-header.h',exported_headers = glob(['**/*.h',]),srcs = glob(['**/*.m','**/*.swift',]),)
我们将每个pod视为单个库,并对每个库使用不同的构建规则。
在大多数情况下,pod包含其源代码,所以可以使用apple_library来构建它。
apple_library(name = 'PromiseKit',visibility = ['PUBLIC'],bridging_header = 'BuckSupportFiles/PromiseKit-Bridging-Header.h',exported_headers = glob(['PromiseKit/**/*.h',]),srcs = glob(['PromiseKit/**/*.m','PromiseKit/**/*.swift',]),)
当pod仅提供一个编译好的二进制文件(例如libSample.a)时,我们则使用prebuilt_cxx_library构建规则。
prebuilt_cxx_library(name = 'BTDeviceCollectorLibrary',lib_name = 'DeviceCollectorLibrary',lib_dir = 'Braintree/BraintreeDataCollector/Kount/',)
当pod提供框架文件时,我们则使用prebuilt_apple_framework构建规则。此构建规则的更多示例可以在这里找到。
prebuilt_apple_framework(name = 'BuckTest',framework = 'BuckTest.framework',preferred_linkage = 'shared',visibility = ['PUBLIC'],)
我们还面临着一些挑战,包括:
1. 使buck project能够生成Xcode项目文件。我们计划的工作流程会涉及到在本地开发过程中使用Xcode,并在CI中使用Buck。
2. 升级Buck、将库构建为模块,以便可以使用-import-underlying-module和删除hack。
3. 优化用于iOS的Buck缓存。我们在Buck缓存机制中找到了一些可改进的地方,并将继续在这方面投入。
4. 进一步分析从Xcode切换到Buck后所获得的收益。
如果你有任何问题/反馈,请随时与我们联系。如果你想帮助我们解决这些挑战,请加入我们!
查看英文原文:https://medium.com/airbnb-engineering/building-mixed-language-ios-project-with-buck-8a903b0e3e56