大型 iOS 应用生产力设计:构建系统、模块化架构与 Dev App 实践

技术创新与团队流程改进,如何让 iOS 开发者在庞大代码库中依然保持高效。本文以某海外短租平台的大型 iOS 应用为例,介绍其如何通过现代构建系统、模块类型设计和开发应用(Dev App),优化 Xcode 构建体验,提升 iOS 开发效率和代码库可维护性。

大型 iOS 应用生产力设计:构建系统、模块化架构与 Dev App 实践

每一位 iOS 工程师大概都记得,第一次看到自己写的应用在 iOS 设备上运行时的那种兴奋。iPhone 以人为本的交互界面,让程序真正“活”了起来。当你选择 iOS 开发作为职业,随着应用触达越来越多用户,这种成就感也会不断增强。

影响更多用户,通常意味着要持续添加新的 iOS 功能、流程和能力。但随着应用用户规模增长,新增功能也会带来更多复杂性和系统负载,进而拖慢产品迭代速度,并让细粒度重构变得更加困难。

作为某海外短租平台的 iOS 团队,我们一路经历了这样的过程。每周发布的代码,能够帮助旅行者获得难忘的假期体验,也能为房东创造新的收入来源,这让我们倍感振奋。对设计细节的关注已经融入我们的团队基因,我们也为能够精心打磨每一个用户触点而自豪。与此同时,我们同样没有避开大规模开发带来的挑战。

本文将讨论我们在支撑不断增长的业务需求时遇到的困难,并介绍该平台 iOS 团队如何通过技术、所有权和流程方面的投入,实现两件事的平衡:既能参与一个具有全球影响力的代码库,又不会被庞大的系统复杂度压垮。

大型 iOS 应用开发面临的挑战

2010 年 6 月 16 日,一位实习生向我们的 iOS 应用提交了第一行代码。自那以后,这个 Xcode 项目逐渐发展为一个拥有 150 万行第一方代码的大型代码库。如今,约有 75 位 iOS 工程师负责这款应用的持续开发。我们每周都会发布支持 62 种语言的应用,为几乎遍布全球各个国家和地区的旅行者与房东社区提供服务。

在这样的规模下,代码组织成为一项重要挑战。我们欢迎并鼓励实验和新想法;而一旦某个解决方案经过充分探索并被验证有效,我们又会非常重视产品体验的一致性。

直到不久前,我们的大部分代码仍以模块形式组织在 lib/<modules> 这样一个扁平目录中。由于缺少清晰的层级结构和分类方式,工程师很难发现通用功能是否已经存在。我们逐渐发现,在应用代码中,同一件事往往有很多种实现方式。这不仅让交付给用户的二进制文件变得臃肿,也使这些分散实现的质量往往低于那些经过集中投入和长期打磨的方案。

Xcode 是 iOS 工程师最熟悉的工具,但我们发现,它并不能很好地适应如此规模和复杂度的代码库。Xcode 项目文件不仅难以在 pull request 中审查,随着工程团队规模扩大、开发节奏加快,项目文件中的合并冲突和竞态问题也越来越频繁。甚至连打开 Xcode 本身,在大型代码库中也变成了一件令人头疼的事。一年多前,我们曾经测试过:加载包含所有源代码的工作区时,Xcode 需要一到两分钟才能进入可交互状态。

大多数 iOS 工程师都对漫长的编译时间和缓慢的迭代周期并不陌生。一位极有创造力的工程师甚至发现,拔掉外接显示器后,笔记本电脑编译代码的速度会更快。许多工程师也养成了将 USB-C 充电线插在 MacBook Pro 右侧的习惯,以尽量避免因为过热降频而降低生产力。

当我们的代码库还不到 50 万行第一方代码时,一些问题可以通过更强大的硬件来缓解。但随着规模继续扩大,我们发现这种方案存在明显上限。当一天中大量时间都耗在等待编译完成上时,开发者很难真正保持投入感和创造力。

这些挑战都是自然形成的。新员工的反馈,也为我们确定基础设施建设优先级提供了宝贵输入。那些来自较小项目团队的 iOS 工程师,还没有适应大型代码库带来的缓慢体验和各种变通做法,因此他们更容易指出问题本身。我们意识到,必须做出改变,才能继续交付世界级的 iOS 应用。

iOS 开发效率优化方案

多年来,我们研究并落地了多种方案来解决上述问题。本文将重点介绍帮助我们实现大规模高效协作的三个关键因素。我们相信,这些高层次理念也适用于许多正在快速成长的中小型 iOS 团队。

对于类似规模的移动研发团队来说,生产力提升并不只依赖构建系统和模块化架构,也依赖需求、排期、开发、测试、发布和复盘之间是否形成顺畅闭环。类似 PingCode 这样的智能化研发管理工具,可以帮助团队打通目标、需求、开发、测试、发布和 Wiki 知识沉淀,让研发数据在不同工具和流程之间持续流转;而类似 Worktile 这样的通用项目协作系统,则适合承载任务、项目、文档、日历、甘特图、工时和审批等协作场景,帮助跨职能团队保持信息透明和执行可追踪。

采用现代构建系统优化 Xcode 构建体验

Xcode 仍然是我们 iOS 工程师首选的 IDE。与此同时,我们也注意到,其他构建系统中的一些能力可以显著提升 iOS 开发效率。其中有几项能力尤其重要:构建产物的网络缓存、构建图查询接口,以及能够将自定义步骤无缝添加为依赖项的机制。我们认为,这些都是现代构建系统的基本能力。

某海外大型互联网公司开源的 Buck 构建系统满足了这些需求。我们从 2016 年底开始认真讨论 Buck,并在 2017 年开始深入探索。到 2019 年,我们全面迁移到了 Buck 的声明式构建系统。我们从 Buck 中获得了巨大收益,同时也发现其公开文档仍有不少改进空间。因此,我们将自己的 Buck 配置整理并分享到了一个公开代码仓库中。

作为这次转型的一部分,我们移除了手动维护的 Xcode 项目,转而使用声明式的 BUCK 文件。这些文件与每个模块的代码并列存放。BUCK 文件使用 Starlark 语言定义,而 Starlark 也与另一个流行的现代构建系统 Bazel 兼容。下面是一个现有模块的目录结构示例:~/apps/ios/features/WifiSpeedFeature> tree -L 1 . ├── BUCK ├── Sources ├── Tests └── _infra 3 个目录,1 个文件

我们的基础设施团队采取的策略是:在充分尊重工程师现有工作环境的前提下,从底层大幅改善开发体验。因此,iOS 工程师仍然在 Xcode 工作区中开发,只不过这些工作区是基于 Buck 中定义的构建图生成的。

最初,只有通过命令行构建的 iOS 应用能够使用 Buck 的 HTTP 缓存。这本身已经是一项巨大改进,因为它使我们能够在每次提交 pull request 后验证应用商店构建是否成功,同时不会拖慢工程师的工作节奏。然而,本地 Xcode 构建仍然无法从缓存中获取构建产物,因为生成的 Xcode 工作区仍然使用标准的 Xcode 构建系统。

随后,我们继续利用应用底层的现代构建系统来缩短工程师的迭代周期。我们实现了一种 Xcode 工作区生成方式:工程师仍然使用 Xcode,但应用构建过程由 Buck 在背后完成。iOS 工程师日常依赖的标准 Xcode 工具,例如断点、控制台和错误信息,都可以正常使用。

基于 Buck 的 Xcode 工作区提升了本地构建速度,因为它能够利用 Buck 缓存。为了进一步缩短 Xcode 启动时间,我们还实现了只包含代码库局部内容的 Xcode 工作区生成功能。完整应用仍然使用 Buck 构建,但 Xcode 需要加载和索引的内容大幅减少,因此交互速度明显提升。

设计模块类型,改善 iOS 代码库组织

为了解决代码缺少层级结构、通用能力难以发现的问题,我们为第一方代码设计了一套组织结构。模块被归入具有明确语义的分组中,我们称之为“模块类型”。

我们为模块类型编写了详尽文档。由于模块类型已经成为 iOS 开发者日常工作方式的重要组成部分,这些文档托管在内部开发者门户中,并通过源代码控制系统进行管理。每种模块类型都会用几段简短文字说明其用途,以及它应当承载哪类代码。

在设计这套架构时,我们同时考虑了应用编程和构建系统的最佳实践。每种模块类型都有一套严格的可见性规则。这些规则定义了不同类型模块之间允许存在的依赖关系。单个模块可以进一步收紧自己的可见性。一些大型团队会采用这种方式:既享受模块化带来的好处,又避免模块产生意外的外部依赖。但单个模块不能突破所属模块类型设定的可见性上限。

我们来看一个例子。

功能模块是我们的核心模块类型之一。在该平台,功能模块指面向用户的一个具体目的地。在 iOS 代码中,这通常意味着一个以模态方式展示,或被安装到应用内 UINavigationController 中的 UIViewController 页面。功能模块应尽可能聚焦于单个面向用户的目的地,但它可以包含多个用于实现该目的地的 UIViewController

功能模块对其他功能模块不可见。也就是说,一个功能模块不能依赖另一个功能模块。但它们可以通过同级的“功能接口”模块类型共享轻量级类型。

每个功能模块都有一个对应的功能接口模块,而接口模块拥有更广泛的可见性。一个功能模块可以依赖任意数量的功能接口模块,但始终会依赖自身对应的接口模块。这个接口在作用上有点类似 Clang 程序中的头文件。

功能模块类型的可见性规则确保所有功能模块彼此独立。接口模块类型则允许功能之间共享简单类型,例如协议、枚举和值类型,从而实现功能间强类型路由等能力。

除了功能模块类型,服务模块类型也很重要。服务模块包含非 UI 对象,用于管理多个功能之间共享的状态。任何服务模块都可以选择拥有一个对应的接口模块。

目前,我们共有 12 种 iOS 模块类型。

语义清晰的模块类型,就像庞大代码库中的目录系统。工程师可以根据模块类型,快速建立对某个模块较为准确的心智模型。目前,我们 90% 的第一方代码已经从原来的 lib/ 文件夹迁移到了新的模块类型结构中。

创建 Dev App,缩短大型 iOS 应用迭代周期

我们在构建系统和 iOS 应用架构上的投入,催生了第三项创新:开发应用(Dev App)。开发应用是按需创建的临时 Xcode 工作区,用于承载单个模块及其依赖项。

开发应用最初来自我们的安卓生态。安卓和 iOS 开发应用之所以都受到欢迎并取得成功,源于一个简单原则:如果把 IDE 的工作范围缩小到只需要编辑当前相关代码,就可以显著缩短开发周期。当 Xcode 工作区中的代码量减少时,Xcode 就能更快完成索引和编译。

模块类型的引入,打破了功能单元之间代价高昂的依赖关系。现在,模块之间的依赖关系非常少。例如,构建任意一个功能模块及其所有依赖项的成本,都远低于构建完整应用。由于功能模块之间不能相互依赖,我们从根本上避免了那种会间接拖入整个应用的大型功能模块。

iOS 工程师可以通过强大且易用的命令行界面创建开发应用。生成开发应用的命令遵循 Unix 最佳实践,并充分考虑了不熟悉终端的工程师的使用体验。该工具底层使用 Buck 的查询接口来收集完整源文件列表。

Dev App 命令行工具会生成一个容器化 iOS 应用来承载对应功能,并打开一个自动生成的 Xcode 工作区。开发者可以在非生产代码中定义该功能的各种变体。这些变体让工程师能够一键进入任意可能的 UI 状态。Dev App 容器应用还为一些常见工作流提供便利,例如将 OAuth 令牌附加到 HTTP 请求中。

开发应用让产品开发者能够只构建完整应用中的一小部分,就可以迭代功能 UI 和大部分业务逻辑。虽然开发应用最初是为功能模块和 UI 模块设计的,但现在我们已经支持为任意类型模块创建开发应用。实践中,我们发现许多 iOS 开发者也更喜欢在这种精简的 Xcode 环境中开发非 UI 模块。

在很多情况下,构建并运行完整应用仍然非常重要,尤其是在测试多个功能如何相互交互时。不过,如果你编写的代码足够独立,并且有充分测试覆盖,那么仅使用开发应用也可以有信心地发布变更。

改造带来的成效:构建速度、测试覆盖率与代码所有权提升

这些努力最终打造了一个生态系统,让不同团队能够在各自领域内更独立地运作。在开发应用中写代码,既能重温在更简单、更小型项目中开发的流畅体验,又能享受成熟官方工具和框架带来的全部优势。

目前,开发应用已经占本地构建次数的 50% 以上。开发应用构建时间的第 75 百分位数不到两分钟,第 50 百分位数远低于一分钟。我们也鼓励开发应用尽可能使用模拟依赖项,以减少需要构建的代码量。

我们的应用架构开启了代码 100% 所有权的新阶段。即使在团队重组过程中,我们也能保持 100% 的代码所有权。这在很大程度上得益于高度模块化的代码库,因为所有权转移和重新划分只需要极少的重构工作。如今,我们的第一方代码已经被拆分为近 1500 个模块。

我们发现,采用现代模块结构的代码测试覆盖率超过 50%,而沿用传统模块结构的代码测试覆盖率只有 23%。这部分要归功于接口模块,因为它们强烈推动开发者使用面向协议的编程技术来编写服务。当代码主要与协议交互时,为依赖项创建测试替身就会变得非常容易。

通过严格控制模块类型之间的可见性规则,我们获得了高度并行化的构建图。在一台配备 8 核处理器的 16 英寸 2019 款 MacBook Pro 上查看完整应用的构建跟踪记录时,我们发现编译阶段接近 80% 的时间都充分利用了可用 CPU 资源。

现代化构建系统也催生了健康的命令行工具生态,极大简化了常见任务。过去,创建模块需要遵循一长串繁琐且容易出错的步骤。现在,工程师可以使用交互式 Rake 命令创建模块。我们甚至利用 Buck 查询接口构建了一个命令行工具,引导工程师完成从 lib/ 模块迁移到新模块结构所需的步骤。

最后,同样重要的是,由于我们不再使用源代码控制系统管理 Xcode 项目文件,添加和删除模块依赖项变得非常简单,相关合并冲突也更容易解决。

共同推动 iOS 工程实践边界

我们希望推动整个行业的 iOS 开发能力向前发展。我们相信原生移动应用的强大力量,也希望其他公司能够借鉴这些实践,把更多精力投入到打造用户喜爱的体验上。

我们知道,我们并不是唯一一家应用从小型项目自然成长起来,并最终超出最初开发者设想的公司。我们也知道,许多同行都经历过类似转型。本文提到的每一种解决方案都与我们的具体环境和组织文化有关,但其中一些主题具有长期价值,也适用于更广泛的团队。

提升生产力的旅程尚未结束。展望未来,我们看到了利用 Swift 静态分析生成样板代码、提高功能和服务代码可移植性的巨大机会。我们也会继续优化 iOS 产品开发中的构建、测试和运行迭代循环,避免大型 iOS 应用的臃肿感扼杀独立 iOS 开发原本的乐趣。我们也期待有一天,原生平台生态能够拥有得到官方认可的现代化构建系统。

我们相信,移动计算领域仍有大量潜力尚未被充分释放。我们将继续改进工具、技术和团队流程,让大规模创新成为可能。

文章包含AI辅助创作:大型 iOS 应用生产力设计:构建系统、模块化架构与 Dev App 实践,发布者:su,转载请注明出处:https://worktile.com/kb/p/3981239

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
su的头像su

发表回复

登录后才能评论
注册PingCode 在线客服
站长微信
站长微信
电话联系

400-800-1024

工作日9:30-21:00在线

分享本页
返回顶部