作者丨Facebook
译者丨王强
策划丨小智
与之前的 iOS 版本相比,新版 Messenger 的启动速度提升到了两倍,体积缩减到了四分之一。我们将 Messenger 的核心代码减少了 84%,从 170 万行减少到 360,000 行。
为了实现这样的结果,我们尽可能使用原生 OS、使用 SQLite 支持的动态模板重用 UI、使用 SQLite 作为通用系统,还构建了一个服务器 broker 充当 Messenger 和其服务器功能之间的通用网关。
Messenger 于 2011 年首次作为独立应用发布。当时,我们的目标是尽可能为用户构建功能丰富的体验。从那时起到现在,我们增加了支付、相机效果、故事、GIF 甚至是视频聊天功能。但是,由于每月有超过 10 亿人使用 Messenger,表面上看起来很简单的全功能消息应用,幕后却变得颇为复杂。帮助我们构建、测试和管理所有这些功能所需的后端使这款应用变得更加复杂了。这款应用的二进制文件在鼎盛时期的体积超过了 130MB。这么大的体积和庞大的代码量拖慢了应用的启动速度,尤其是在较旧的设备上更是雪上加霜;它还有多达 9 个标签,让用户在导航时容易不知所措。
在 2018 年,我们在发布 Messenger 第 4 版时重新设计并简化了它的界面,但我们还想做更多改进。我们开始思考,如果我们今天从头开始打造一款消息应用会做哪些事情。自我们十年前开始开发 Messenger 以来,情况发生了怎样的变化?事实证明变化是很多的。实际上,移动应用的编写方式已从根本上改变了。因此在去年的 F8 大会上,我们宣布了让 Messenger 的 iOS 应用变得更快、更小、更简单的愿景。我们称其为 LightSpeed 项目。
为了构建 Messenger 的新版本,我们需要从头开始重新构建架构并重写整个代码库。这次重写使我们得以利用自 2011 年初版应用推出以来,整个移动应用领域中出现的那些重大进步。此外,我们还可以利用公司在过去几年中开发的最新技术。从今天开始,我们很高兴在接下来的几周内在全球范围内向 iOS 推送新版 Messenger。与之前的 iOS 版本相比,新版 Messenger 的启动速度提升到了两倍 *,体积仅为前者的四分之一。通过这一全新迭代,我们在 Messenger 上重新构想了构建应用的方式,并从头开始应用了全新的客户端核心和服务器框架。这项工作帮助我们改进了自己的最前沿技术,并且新的代码库可以在未来十年内实现可持续性和可扩展性,为跨应用的私有消息传递和互操作性奠定了基础。
根据使用生产数据的内部测试得出:
https://www.facebook.com/Engineering/videos/500081744266613/
我们首先假设 Messenger 必须是一个简单、轻量级的实用程序。有些应用是身临其境的(视频流、游戏);人们会在它们身上花费数小时时间。这些应用占用大量存储空间和电池时间等,因此需要作出权衡。但是消息只是一小段文本,发送时间不到一秒钟。从根本上讲,消息传递应用应该是手机上最小、重量最轻的应用之一。秉承这一原则,我们开始寻找使 iOS 应用显著缩小的正确方法。
无论设备类型或网络条件如何,小型应用的下载、安装、更新和启动速度都会更快。小型应用也更易于管理、更新、测试和优化。当我们开始考虑这个新版本时,Messenger 的核心代码库已增长到 170 万行以上。仅仅改动几部分代码是不够的。
获得更小应用的最简单方法是剥夺我们多年来添加的许多功能,但是对我们来说,保留所有最常用的功能(例如群组视频通话)是非常重要的。因此我们退后一步,研究了如何应用过去十年中所学到的知识,以及我们对当今应用中用户需求的理解来行事。在研究了各个选项之后,我们决定越过表层的界面,深入研究应用本身的基础架构。
完全重写代码库是一项极为罕见的工作。在大多数情况下,重写应用所需的大量工作产生的实际效率改进收益是很小的(如果有的话)。但是这一次,早期的原型探索表明我们可以实现巨大的收益,这促使我们去尝试做一些类似规模的应用很少做过的事情。这不是一件小事。LightSpeed 项目启动时只有少数几名工程师参与,但到最后需要 100 多名工程师才能完成项目,交付最终产品。
最后,我们将 Messenger 核心代码减少了 84%,从 170 万行减少到 360,000 行。我们重建了功能以适应简化的架构和设计,从而实现了这一目标。我们保留了大多数功能,并且随着时间的推移将继续引入更多功能。更少的代码量让应用变得更轻巧、更快,并且简化的代码库意味着工程师可以更快地创新。
我们的主要目标之一是最大程度地降低代码复杂度并消除冗余。我们知道统一的架构将允许全局优化(而不是让每项功能都专注于局部优化),并允许以灵活的方式重用代码。为了构建这种统一的架构,我们建立了四项原则:利用 OS、重用 UI、利用 SQLite 数据库,以及推送到服务器。
移动操作系统正在飞快进化,日新月异。在用户需求和竞争压力的推动下,新功能和创新层出不穷。开发人员在构建新功能时,往往会选择在操作系统之上构建抽象,以填补功能差距、增加工程灵活性或创建跨平台用户体验。但是现有的操作系统通常可以满足许多需求。诸如渲染、代码转换、线程和日志记录之类的操作都可以由操作系统来处理。即使可能有在局部指标上速度更快的自制解决方案,我们也会使用操作系统针对全局指标进行优化。
尽管 UI 框架功能强大,且可以提升开发人员的生产力,但它们需要不断的升级和维护,以适应不断变化的移动 OS 格局。我们没有重新设计轮子,而是使用了设备原生 OS 上可用的 UI 框架来支持更广泛的应用功能需求。在避免了缓存 / 加载大型定制框架的需求后,我们不仅减小了应用体积,还降低了复杂性。原生框架不必转译为子框架。我们还使用了许多 OS 库,包括 JSON 处理库,而不是在代码库中构建和存储我们自己的库。
总体而言,我们的方法是很简单的。如果操作系统做得很好,我们就使用它。我们充分利用了操作系统的全部功能,而无需等待哪个框架公开这些功能。如果操作系统没有做到什么事情,我们将找到或编写最小的库代码来满足特定需求,仅此而已。我们还采用了依赖平台的 UI 和相关工具。对于任何跨平台逻辑,我们都使用原生 C 代码内置的操作扩展,其具有高度可移植性,效率出众,速度飞快。我们将这种扩展用于所有全局次优的类操作系统功能,或操作系统未涵盖的那些功能。例如,所有特定于 Facebook 的联网功能都在扩展程序中用 C 编写。
在 Messenger 中,我们一些相同的 UI 体验有着多个版本。比如说在项目开始时,我们有 40 多个不同的联系人列表页面。每个页面的设计都有细微的差异,具体取决于电话渲染等因素——每个页面都必须增强以支持横向模式、黑暗模式和可访问性等特性,这使我们需要支持的数量翻了一番。这意味着我们需要很多视图,这些视图在 Messenger 之类的应用中占了很大的比例。为了简化和消除冗余,我们限制了设计架构,强制对不同的视图重用同一结构。这样一来我们就只需要几类基本视图即可,并且这些视图可以由不同的 SQLite 表驱动。
在今天的 Messenger 中,联系人列表是单个动态模板。我们可以更改屏幕外观,而无需其他任何代码。每次有人加载页面时(要向群组发送消息,阅读新消息等),应用都必须与数据库对话以加载适当的名称、照片等。现在应用不需要存储 40 种页面设计了,取而代之的是数据库包含了根据要加载的各种子功能来显示不同构件的指令。单个联系人列表页面可以扩展以支持大量功能,例如联系人管理、组创建、用户搜索、消息安全性、故事安全性、共享、故事共享等等。在 iOS 世界中,这是一个单视图控制器,具有适当的灵活性来支持所有这些需求。在我们所有的设计中使用这个更优雅的解决方案后,我们就能删除掉大量代码。
大多数移动应用将 SQLite 用作存储数据库。但是随着功能的有机增长,每种功能最终都有自己独特的存储、访问数据和实现相关业务逻辑的方式。为了构建一个通用系统,我们从桌面世界中汲取了一个理念。我们不是去管理几十个独立的功能,并让每个功能提取信息并在应用上构建自己的缓存,而是利用 SQLite 数据库作为一个通用系统来支持所有功能。
从历史上看,协调各种功能之间的数据共享需要自行开发复杂的内存中数据缓存和事务子系统。在数据库和 UI 之间传递这种逻辑会拖慢应用的速度。我们决定放弃这种途径,而只使用 SQLite,并让它处理并发、缓存和事务。现在,我们不会再让一个系统来更新 " 哪些朋友现在处于活动状态 " 的信息,让另一个系统来更新联系人列表中个人资料图片的更改,再让另一个系统来检索你收到的消息了,如今来自数据库的数据请求都是自包含的。所有的缓存、过滤、事务和查询都在 SQLite 中完成。UI 只会反映数据库中的表。
这样就可以让逻辑保持简单和高效,并限制了其对应用其他部分的影响。但是我们走得更远。我们为所有功能开发了一个集成的架构。我们为 SQLite 扩展了存储过程的功能,使 Messenger 功能开发人员可以编写可移植的、面向数据库的业务逻辑,最后,我们构建了一个平台(MSYS)来编排对数据库的所有访问,包括队列更改、延期或可重复执行的任务,并支持数据同步。
MSYS 是一个用 C 编写的跨平台库,可操作我们需要的所有原语。将所有代码整合到一个库中让管理一切事务变得更容易了。它更集中,更专注。我们尝试以单一的方式来做各种事情——向服务器发送消息的方式只有一种,发送媒体的方式只有一种,记录的方式只有一种,等等。使用 MSYS 后,我们就有了全局视图。现在我们可以确定负载的优先级。假设 " 加载消息列表 " 的任务比更新 " 几天前是否有人在线程中读取消息 " 的任务具有更高的优先级;我们可以将高优先级任务上移到队列中。一个通用系统可以简化我们对应用的支持工作。有了 MSYS,我们可以更轻松地一站式追踪所有这些功能的性能表现、发现性能退化并修复错误。此外,我们在自动化测试上投入资源,使系统的这一重要部分变得异常稳健,结果让 MSYS 逻辑的代码行覆盖率达到了(在行业中很少见)的 100%。
对于不属于上述任何类别的内容来说,我们会将它们推到服务器上。我们必须建立新的服务器基础架构,以支持客户端上 MSYS 的单个集成数据和同步层的存在。原始 Messenger 的客户端 - 服务器交互的工作方式与传统应用是一样的:对于每个功能,客户端都有明确的协议和连接格式,以便客户端同步数据并向服务器更新任何更改。然后,该应用必须实现该协议,并协调正确的数据库更新以驱动 UI。这意味着对于应用中的每个功能,都有很多(到头来是不必要的)自定义的平台特定业务逻辑。
客户端和服务器之间的协调逻辑非常复杂,并且容易出错,而且随着功能数量的增加会更容易出错。例如,接收文本消息这个操作涉及到消息列表的更新、相关线程片段的更新、最后修改时间 / 线程的更新、删除可能已插入的任何乐观版本的消息(例如从通知中删除)、删除正在处理消息乐观版本的任务、解密,以及其他众多任务。这些类型的客户端 - 服务器交互涵盖了应用中的所有功能。结果,应用需要重复处理相似的问题,并且就所有这些事件和交互的组合方式而言,整个应用运行时的行为是不确定的。随着时间的流逝,我们的应用已经变成了繁忙的高速公路,两个方向都堵上了长长的车队。
在今天的 Messenger 中,我们有一个通用的灵活同步系统,该系统允许服务器定义和实现业务及同步逻辑,并确保客户端和服务器之间的所有交互都是统一的。与客户端上的 MSYS 相似,我们构建了一个服务器代理来支持所有这些情况,而实际的服务器后端基础架构负责支持这些功能。服务器 broker 充当 Messenger 和所有服务器功能之间的通用网关,而在过去,所有客户端功能都使用各种各样的方法直接与服务器功能通信。
https://www.facebook.com/Engineering/videos/3509976602377949/
如今的 Messenger 是非常轻巧的——代码库已从 170 万行减少到 360,000 行。应用的二进制大小现在是原来的四分之一。但是在将新的代码库投入生产之前,我们必须确保它不会随着新添加的修补程序、更新和功能而再次膨胀起来。为此,我们为每个功能设置了预算,并责成我们的工程师遵循上述架构原则来坚持遵守这些预算约束。我们还构建了一个系统,使我们能够了解每个功能带来了多少二进制大小权重。我们要求工程师负责遵守预算约束,作为功能接受标准的一部分。按时完成功能很重要,但达到质量目标(包括但不限于二进制大小的预算约束)更为重要。
构建今天的 Messenger 经历了一段漫长的旅程,并且公司中的许多工程师都参与了它的开发工作。但对于使用这款应用的用户来说,它的外观或感觉不会有太大不同。它的启动速度会更快,但仍将提供人们期望的,与旧版本相同的出色消息体验。但这仅仅是一个开始。
我们在重建 Messenger 方面所做的工作,将使我们在迈向未来的过程中能够继续创新和扩展消息体验。除了构建可在未来十年或更长时间内可持续发展的应用之外,这项工作还为我们整个应用系列中的跨应用消息传递奠定了基础。它还为我们以隐私为中心的消息传递体验打下了根基。
我们要感谢为 LightSpeed 项目做出贡献的所有人们。
参考链接:
https://engineering.fb.com/data-infrastructure/messenger/
InfoQ 读者交流群上线啦!各位小伙伴可以扫描下方二维码,添加 InfoQ 小助手,回复关键字“进群”申请入群。大家可以和 InfoQ 读者一起畅所欲言,和编辑们零距离接触,超值的技术礼包等你领取,还有超值活动等你参加,快来加入我们吧!
点个在看少个 bug👇