[关闭]
@zb5228126 2017-08-14T07:34:03.000000Z 字数 3591 阅读 3276

用Buck构建混合语言iOS项目

作者Brian Zhang,译者:张斌


用Buck构建混合语言iOS项目

Airbnb认为开发人员的体验是良好工程设计的关键,特别是移动开发者Infra团队,该团队的目标是优化移动应用程序的构建时间。
6月份,我们通过Buck(buckbuild.com)成功构建了iOS应用。这对我们来说是一个巨大的里程碑:当开始工作的时候,Buck并不支持混合语言的iOS项目,而我们的iOS代码库由Swift和Objective-C均匀混合组成。但由于成功使用Buck,我们的CI构建速度提高了50%,而应用程序的大小也缩小了30%
达到这一里程碑是一个复杂的过程。在本文中,我们将分享所面临的挑战的技术细节,以及我们如何在iOS代码库中使用Buck。希望这对对类似工作感兴趣的其他人有用。

如何构建iOS应用程序

当研究Xcode如何构建iOS项目时,我们发现了这个非常棒的帖子,其中详细说明了该过程。
简而言之,构建过程包括以下步骤:
- 将桥接头文件(由作者维护)和Swift源文件传递给swift工具,生成两种文件:
a).o,用于生成最终可执行二进制文件的机器码文件.
b) 包含所有Swift代码中定义的类和接口的*-Swift.h文件。这些文件可以被显式导入到要使用Swift代码定义的功能的Objective-C文件中。
- 将所有Objective-C源文件和*-Swift.h文件传递给clang工具,它会为每个Objective-C文件生成.o文件。
- 将所有.o文件传递给ld命令,链接所有机器代码文件并生成最终的可执行文件。

Buck与Xcode之间的差异

Buck使用与上述大致相同的过程来构建iOS项目。然而,有一个非常重要的区别,使得支持我们这样的混合语言项目更具挑战性。
Xcode独立构建每个模块,并生成动态链接的框架。对于特定模块M,可执行二进制文件和相关资源/资产最后会保存在最终应用文件夹中的App.app/Framework/M.framework里面。
Buck将这些模块视为静态库,将它们全部连接在一起并生成单个可执行二进制文件。这种方法可以有效地减少二进制文件的大小,因为:
a)如果多个模块使用相同的资源/资产,则不需要将相同的文件复制到每个*.framework文件夹。
b)它可以剥离更多未使用的符号,因为所有库都静态地链接在一起。

这非常适合仅有Objective-C或Swift的项目。不幸的是,这种优化在混合语言项目中会出现问题。

-import-underlying-module标志不起作用

-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.h内部找到桥接头

swift工具生成*-Swift.h文件时,它会显式导入用于Objective-C定义的桥接头。这将导致Buck构建失败。
根据Apple的代码,当使用-import-underlying-module标志时,生成的*-Swift.h文件会将项目头导入。例如:

  1. #import <Project/Project.h>

当提供桥接头(如在Buck中一样)时,生成的*-Swift.h文件将直接导入桥接头文件:

  1. #import "ios/Project/Project-Bridging-Header.h"

你可以看到,导入的路径是相对的。当另一个文件导入此*-Swift.h文件时,它无法找到桥接头。
在这个提交中,我们更新了Buck,将-iquote buckRootPath传递为编译器参数。它能具体地告诉swift工具去查找buckRootPath内的桥接头文件。

@import不起作用

将头文件导入到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的最后一个大障碍。

BUCK示例

为了说明我们所做的工作,我们创建了这个示例项目,你可以随时复制它,并自己测试!

建立混合语言库

如前所述,当构建混合语言库时,需要将桥接头文件传递到apple_library构建规则中。

  1. apple_library(
  2. name = 'ImportObjC',
  3. visibility = ['PUBLIC'],
  4. bridging_header = 'bridging-header.h',
  5. exported_headers = glob([
  6. '**/*.h',
  7. ]),
  8. srcs = glob([
  9. '**/*.m',
  10. '**/*.swift',
  11. ]),
  12. )

构建CocoaPods

我们将每个pod视为单个库,并对每个库使用不同的构建规则。
在大多数情况下,pod包含其源代码,所以可以使用apple_library来构建它。

  1. apple_library(
  2. name = 'PromiseKit',
  3. visibility = ['PUBLIC'],
  4. bridging_header = 'BuckSupportFiles/PromiseKit-Bridging-Header.h',
  5. exported_headers = glob([
  6. 'PromiseKit/**/*.h',
  7. ]),
  8. srcs = glob([
  9. 'PromiseKit/**/*.m',
  10. 'PromiseKit/**/*.swift',
  11. ]),
  12. )

当pod仅提供一个编译好的二进制文件(例如libSample.a)时,我们则使用prebuilt_cxx_library构建规则。

  1. prebuilt_cxx_library(
  2. name = 'BTDeviceCollectorLibrary',
  3. lib_name = 'DeviceCollectorLibrary',
  4. lib_dir = 'Braintree/BraintreeDataCollector/Kount/',
  5. )

当pod提供框架文件时,我们则使用prebuilt_apple_framework构建规则。此构建规则的更多示例可以在这里找到。

  1. prebuilt_apple_framework(
  2. name = 'BuckTest',
  3. framework = 'BuckTest.framework',
  4. preferred_linkage = 'shared',
  5. visibility = ['PUBLIC'],
  6. )

接下来要做什么?

我们还面临着一些挑战,包括:
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

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注