在Pinterest上开发快速可靠的iOS版本(第一部分)

Rahul Malik | iOS平台技术主管

在Pinterest,我们专注于帮助人们发现启发性的想法,从晚餐食谱到尝试,从家庭和时尚产品到购买,再到旅行的地方。 构建最佳的移动产品是其中的关键部分,所有Pinner的80%通过移动应用访问Pinterest。 特别是在iOS团队中,我们一直在不断努力,以尽可能高效和迅速地改善这种体验,而为我们的团队提供最佳的开发和测试环境是其中的关键一步。

最近,我们研究了简化该过程的方法,并着手提高基于本地和持续集成环境的iOS构建的速度和可靠性。 此外,我们开始将应用程序模块化为独立的框架,并需要一个系统来支持该迁移。 我们回顾了多种工具,包括Xcode,Cocoapods,Buck和Bazel。 我们希望为未来引入更稳定的基础,这对于我们快速迭代并向Pinners发布新功能的能力至关重要。

通过比较Xcode,Cocoapods,Buck和Bazel,我们发现Bazel最适合我们的目标,即为性能提高一个数量级,消除构建环境中的可变性并逐步采用该基础。 因此,我们现在使用Bazel交付我们所有的iOS版本,这些版本已经取得了成功,其中包括:

当地发展

  • 更快的构建:将干净的构建时间从4m 38s减少到3m 38s,提高了21%。
  • 本地磁盘缓存允许即时重建您之前构建的任何内容(其他分支,提交等)。
  • CI和本地环境之间的环境相同,因此生成问题很容易重现。
  • 自动化程度更高:代码生成等任务已包含在构建图中。

持续集成

  • 每个构建都是增量构建:由于Bazel具有可复制性,因此一年多来我们一直没有在CI上执行任何全新构建。
  • 一次构建,可在任何地方重复使用:引入远程构建缓存后,由于我们不需要重新构建任何机器上已构建的任何内容,因此构建时间减少了不到一分钟且低至30秒
  • 缩短着陆时间:将构建时间从10m 24s减少到7m 34s,提高了27%。
  • 缩短更改Beta测试人员的时间:Beta的构建时间从1400万32秒减少到700万52秒,提高了45%。
  • 更快的测试执行:如果修改后的代码不影响测试,则测试运行是即时的。
  • 更高的构建成功率:使用Bazel运行构建任务时,构建成功率从大约80%提高到97%-100%。

迈向快速可靠构建的未来

由于我们使用的是编译语言(Objective-C / C ++),因此构建速度一直是开发人员的瓶颈。 但是构建速度很难量化。 它包括在不同环境中的构建,例如持续集成或本地开发。 我们还处理各种工作流程方案,例如全新构建,增量构建,分支切换,变基,还原更改等。 您无法改善无法衡量的内容,因此,提高构建速度需要跟踪各种方案,以使我们能够准确地确定回归并集中精力进行性能工作。

我们可以通过减少工作量或提高工作效率来加快构建速度。 这可能涉及使用不同的工具,改善并行化或更新项目的体系结构以需要更少的源文件。 围绕维护模块化体系结构以及清除未引用的代码或与已完成的实验相关的无效代码,采取有力的实践将有助于维护/提高构建速度。 我们使用各种内部工具和脚本来识别无效代码。 对于实验,我们利用自动化功能添加了clang批注以弃用与实验相关的方法和常量,从而允许编译器警告开发人员实验已结束且应删除代码。 开发人员通过定期运行工具来检查未引用的代码,即定期运行工具来检查标头包含我们构建的图形,并递归查找零引用的文件。

我们的构建过程必须既快速又可靠。 如果构建是可复制的,则它们是可靠的。 可重现的构建不仅对于重现错误很重要,而且对于确保我们提供经过开发和测试的应用程序的确切版本也很重要。 只有构建环境(输入和输出)一致,我们才能实现这一目标。

环境的变化会极大地影响最终产品并引入可变性。 一致的环境可确保应用程序的行为相同,而不管它是在开发人员的计算机上构建还是通过持续集成构建而成,并且消除了花时间弄清楚构建为何在一个环境中成功而在其他环境中失败的时间。

尽管这些想法和探索都围绕iOS进行,但快速且可复制的构建目标是我们所有人共同的目标,并将使我们能够扩展客户端工程。

挑战性

专注于改善构建过程的决定植根于它对开发人员生产力的影响。 随着我们团队和产品的增长,投资于开发人员使用一致且快速的构建系统的能力至关重要。

  • 规模:随着我们扩展客户工程的规模,花费在支持开发人员,维护或减少构建时间以及提高可靠性规模上的时间也有所增加。 支持开发人员的工程师数量不一定与开发人员数量成比例地扩展,并且Xcode不包含在性能下降时用于分析构建的工具。
  • 模块化架构:我们已经开始从应用程序重构构成我们平台的核心框架,以改善整体架构,文档和质量。 这增加了复杂性,因为它需要一个构建系统,该构建系统可以管理需要按特定顺序配置和编译的构建目标的依赖关系图。 尽管在Xcode中并非并非不可能,但是由于缺乏表达性配置API,这种图形的配置和维护将难以长期维护。
  • 生成不稳定性:在我们的代码库之外,有许多用不同语言(Ruby,Python,Bash等)编写的工具,它们要求特定的版本和工具链必须相同才能创建一致的生成。 这些变化可能导致难以可靠再现的错误。 对于开发人员而言,在本地进行构建传递并不常见,但是在持续集成时失败,反之亦然。 仅某些计算机具有创建候选发布版本所需的要求。 本地状态可能会损坏,这需要执行干净的构建。 那浪费时间。
  • 任务自动化和代码生成:我们依靠代码生成来创建不可变模型(通过Plank)和日志记录基础结构(通过Thrift)。 Xcode支持运行脚本阶段,但不能将动态代码生成或一般任务自动化等工作流程纳入构建过程,而是需要手动集成生成的源代码,从而为开发人员提供更多工作并进行入门培训。 这还需要将生成的工件添加到版本控制中,从而增加我们的存储库大小和git clone性能。
  • 共享资源:外部存储库的集成路径尚不清楚,并且历史上一直导致定期从其他存储库复制资源。 我们已经探索了git子树或git子模块之类的选项,但这需要增加对员工培训的投资,并需要更改开发人员的工作流程。 这带来了混乱,并且又浪费了时间。 Xcode不支持声明外部构建依赖关系,因此我们将不得不依靠外部工具来提供此集成。

解决方案

我们想要的解决方案将使我们能够通过工具和自动化来克服这些挑战,而不是增加开发人员教育和流程的工作量,并减少浪费的时间。 我们主要针对以下方面进行优化:

  • 快速迭代:我们的解决方案应提供功能,以随着时间的推移极大地提高和保持构建速度和开发人员速度,这可能是通过更好的并行性和高级工具功能实现的。
  • 沙盒开发:一个一致的环境,使我们能够构建可靠的版本,并最大程度地减少变化和对开发人员生产力的影响。
  • 类似Monorepo的开发:所有源仍应保留在一个存储库中。 这可以最小化在整个应用程序中进行更改所需的工作量和上下文切换。
  • 分析,监视,分析:我们需要工具,这些工具可以使我们深入了解构建系统以发现问题。 我们的解决方案需要使我们可视化整个构建过程中执行的动作及其各自的持续时间。 假设有此功能,我们将能够经常跟踪详细的更改。
  • 增量编译:一旦我们构建了客户端一次,我们就应该能够安全地在所有工作流程中进行增量构建。 这应该包括切换分支,还原更改或工作流的其他部分。 干净的构建是迄今为止最昂贵的构建,通常在本地状态损坏或开发人员试图诊断未知的构建问题时执行。

未来可扩展

随着应用程序复杂性的增长和需求的发展,我们必须确保我们的构建系统具有足够的可扩展性,以允许进行更改。 但这不能太具体,以免阻碍我们构建过程中的进一步动态自动化。 这可能包括能够自动执行任务以集成第三方静态分析,自定义工具链以及我们在Pinterest开发的内部工具。

更改构建系统是一项重大更改,我们无法支持无法逐步引入的方法。 一个全有或全无的解决方案可能需要暂停开发或维持一个长期存在的分支,并在开发人员环境和CI系统之间原子地执行危险的迁移。

请继续关注第二部分!