跨平台开发成本太高?Dropbox 最终宁愿将代码编写两次

直到最近,Dropbox 都在使用一个通过 C++ 在 iOS 和 Android 之间共享代码的移动技术策略。这个策略背后的想法很简单:用 C++ 编写一次代码,而不是用 Java 和 Objective C 编写两次代码。现在,Dropbox 已经完全放弃了这个策略,转而使用每个平台的原生语言。这个决定是由于与代码共享相关的隐藏成本太高。

正文

直到最近,Dropbox 都在使用一个通过 C++ 在 iOS 和 Android 之间共享代码的移动技术策略。这个策略背后的想法很简单:用 C++ 编写一次代码,而不是用 Java 和 Objective C 编写两次代码。早在 2013 年,我们就采用了这个策略。当时,我们的移动工程团队相对还比较小,但需要支持快速增长的移动路线图。我们需要找到一种方法,使这个小团队可以快速交付大量的 Android 和 iOS 代码。

现在,我们已经完全放弃了这个策略,转而使用每个平台的原生语言(主要是 Swift 和 Kotlin,这两种语言在我们刚开始制定移动策略时还不存在)。这个决定是由于代码共享相关的隐藏成本太高。下面是我们作为一个公司在有效共享代码的成本方面学到的一些东西。它们都源于同一个基本问题:

以非标准的方式编写代码,使我们承担了一些开销,而如果我们采用广泛使用的平台默认选项,我们就不必担心这些开销。这种开销最终比只编写两次代码要昂贵得多。

在详细介绍我们遇到的所有不同类型的开销之前,我想澄清一下,我们实际上从来没有达到用 C++ 开发大部分代码库的地步。采用 C++ 的开销实际上阻止了我们完全朝着这个方向前进。

同样值得注意的是,像谷歌和 Facebook 这样的大公司多年来一直在开发可扩展的代码共享解决方案。到目前为止,这些解决方案只获得了有限的采用。虽然你可以利用第三方代码共享解决方案(如 React Native 或 Flutter)来避免下面描述的一些开销,但有一些仍会存在(至少在其中一项技术获得支持并成熟之前)。例如, Airbnb 放弃 React Native 的原因与本文中描述的许多相同。

我们可以把我们面临的不同类型的开销分为四大类。

自定义框架和库的开销

关于使用 C++,最容易预见的开销是需要构建框架和库。这大致可以分为两大类:

  • 框架,让我们可以与主机环境交互,构建一个功能齐全的移动应用程序。例如:
    • Djinni :一个可以生成跨语言的类型声明和接口绑定的工具;
    • 用于在后台和主线程中运行任务的框架(在平台原生语言中,这是一个很简单的任务)。
  • 这些库将取代我们本可以在平台原生语言中使用的默认语言 / 开源标准。例如:
    • 用于 JSON(反)序列化的 json11
    • C++ 非可空指针 nn

如果我们继续使用平台原生语言,那么这些代码都是不必要的;如果我们使用平台原生语言,那么我们对开源项目的贡献可能会使更多的开发人员受益。我们有可能在利用开源 C++ 库方面做得更好,但是 C++ 开发社区中的开源文化并不像在移动开发社区中那样强大(特别是在几乎不存在的 C++ 移动社区中)。

注意,这些成本在 C++ 中特别高(与 Python 或 C#等其他可能的非原生语言相比),因为它缺少一个功能齐全的标准库。虽说如此,C/C++ 是唯一一种编译器同时受到谷歌和苹果支持的语言,因此,使用另一种语言将会产生一系列其他需要处理的问题。

自定义开发环境的开销

移动生态系统有许多工具可用来提高开发效率。移动 IDE 非常丰富,谷歌和苹果投入了大量的资源,力争在各自的平台上为开发人员提供最佳的开发体验。我们远离了这些平台的默认选项,也就放弃了其中的一些好处。最值得注意的是,使用平台原生语言的调试体验通常优于在平台的默认 IDE 中使用 C++ 代码的调试体验。

一个特别令人难忘的例子是一个 Bug,它在我们的后台线程框架中导致了死锁,使得应用程序随时都会崩溃。即使是在处理简单的标准栈时,也很难确定这些类型的 Bug。因为这个问题涉及到调试在 C++ 和 Java 之间来回运行的多线程代码,所以花了几周的时间才确定下来!

除了失去工具,我们还必须投入时间构建支持 C++ 代码共享的工具。最重要的是,我们需要一个定制的构建系统,它创建了包含 C++ 代码以及 Java 和 Objective-C 封装器的库,并且能够生成 Xcodebuild 和 Gradle 都能识别的目标文件。这个系统对我们的资源是一个很大的拖累,因为它需要不断地更新以支持两个构建系统中的更改。

处理不同平台差异的开销

尽管 iOS 和 Android 应用程序都是“移动应用程序”,通常都具有相同的特性和功能,但平台本身存在一些影响实现的差异。例如,应用程序在每个平台上执行后台任务的方式不同。即使在我们开始采用跨平台策略时非常相似的东西,随着时间的推移也会产生很大的差异(例如,与相机相册的交互)。

因此,你甚至不能真正地编写一次代码就在不同的平台上运行。你必须花费大量的时间将代码集成到不同的平台中,并编写特定于平台的代码(有时这些代码就位于 C++ 层本身!)。

这使得只编写一次代码理论上的好处没有达到预期的效果,从而大大降低了这种方法的好处。

培训、招聘和留住开发人员的开销

最后,但同样重要的是,培训和 / 或招聘将在我们定制程度非常高的技术栈上进行开发的开发人员的成本。当 Dropbox 开始采用这种移动策略时,我们有一个由经验丰富的 C++ 开发人员组成的核心团队。这个小组启动了 C++ 项目,并在 Dropbox 培训其他移动开发者如何为代码库做贡献。

随着时间的推移,这些开发人员转向其他团队和其他公司。留下来的工程师没有足够的经验来填补技术领导的空缺,并且越来越难招聘到具有相关 C++ 经验、对移动开发感兴趣的高级工程师来替代他们。

因此,我们最终失去了维护 C++ 代码库的关键专业知识。重新获得这种专业知识的唯一方法是大量投资于以下两个选择之一:

  1. 寻找并招聘具备这种特殊技能的求职者(我们曾尝试招聘这个职位超过一年,但没有成功);
  2. 针对内部移动(或 C++)工程师缺少的技能集对他们进行培训,当你没有所期望技能集的人员来执行培训时,这实际上是不可能做到的。即使在核心团队离开之前,移动工程师通常也对学习 C++ 不感兴趣,所以找人来接受培训也是一个大问题。

除了招聘问题,我们自己的技术栈也导致留住开发人员成为一个问题——移动开发者根本不想从事 C++ 项目开发。这导致许多有才华的移动工程师离开了这个项目,而不是费力地使用一个维护得不是很好的自定义技术栈。总的来说,移动开发社区非常有活力——新技术和新模式层出不穷,并且被迅速采用。最好的开发人员喜欢让他们的技能保持最新。

在具有标准技术栈的成熟产品环境中,跟上最新最好的技术潮流是一个挑战。为了稳定性,你牺牲了采用速度。当你把自己锁在一个自定义技术栈中,并置身于更广泛的移动生态系统之外时,这一挑战将被极大地放大。

小结

尽管编写一次代码听起来很划算,但是相关的开销使这种方法的成本超过了所能获得的好处(结果证明,这种好处比预期的要小)。最后,我们不再通过 C++(或任何其他非标准方式)共享移动代码,而是用平台的原生语言编写代码。

此外,我们希望我们的工程师有一个愉快的经历,能够为社会作出贡献。这就是为什么我们决定使我们的实践与行业标准保持一致。

查看英文原文: The (not so) hidden cost of sharing code between iOS and Android