HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

Facebook iOS 版:探索移动应用 10 年演进之路

作者:Dustin Shahidehpour

  • 2023-03-15
    北京
  • 本文字数:5752 字

    阅读完需:约 19 分钟

Facebook iOS版:探索移动应用10年演进之路

iOS 版 Facebook(FBiOS)是 Meta 公司所掌握的最古老的移动端代码库。自2012年应用重写以来,这版应用经过数千名工程师的改动和调整、被交付给全球数十亿用户,并可支持数百名工程师同时对其进行迭代。

 

经过多年演进,FBiOS 的代码库已经跟典型的 iOS 代码库大有区别:

 

  • 充斥着 C++、Objective-C(++)与 Swift 代码。

  • 包含几十个动态加载库(dylibs),以及很多无法一次性加载到 Xcode 中的类。

  • Apple SDK 的直接使用率几乎为零——所有内容都被打包或替换成了内部抽象的形式。

  • 在 Fcaebook 的自定义构建系统 Buck 的推动下,这款应用大量使用代码生成技术。

  • 如果构建系统不配备巨大的缓存,那么应用构建大概要花掉工程师们整整一天的时间。

 

千万不要误会,FBiOS 的整个架构设计过程从来就没有故意朝着这个混乱的方向前进。这款应用的代码库其实是过去 10 年间持续演进的体现,反映的是越来越多工程师参与到开发中来、对应用稳定性的强调以及改善用户体验等核心技术决策。

 

如今,为了庆祝这套代码库的 10 岁生日,让我们一同回顾整个演变历程背后的重要决定和历史背景。


2014 年:建立自己的移动框架

 

在 Meta 对 Facebook 应用进行原生重写的两年之后,News Feed 代码库开始出现可靠性问题。当时,News Feed 的数据模型由苹果的默认数据模型管理框架 Core Data 负责支持。Core Data 中的对象是可变的,这并不适合 News Feed 的多线程架构。更糟糕的是,News Feed 还在使用双向数据流,这正源自苹果 Cocoa 应用中的默认设计模式 Model View Controller。

 

最终,这样的设计加剧了非确定性代码的产生,工程师们发现这些代码既难以调试、又很难进行 bug 重现。面对这样一种不可持续的架构,是时候做点什么了。

 

在考虑新设计时,一位工程师研究了 Facebook 的(开源)UI 框架 React。React 的声明式设计能够将 Feed 上棘手的命令式代码抽象出来,同时利用单向数据流降低代码的归因难度,因此在 JavaScript 社区中越来越流行。这些功能特性似乎很适合处理 News Feed 面对的挑战,只剩下一个问题……

 

Apple SDK 中不提供声明式 UI。

 

那时候距离Swift的发布还有几个月,而 SwiftUI(苹果的声明式 UI 框架)更是要到 2019 年才亮相。如果 News Feed 想要获得声明式 UI,就必须得由开发团队自己动手打造。

 

认真权衡了一番后,开发团队决定自己动手。

 

经过几个月的构建和迁移之后,News Feed 终于运行在了新的声明式 UI 与数据模型上,这让 FBiOS 的性能提高了 50%。几个月后,他们将这套受 React 启发的移动 UI 框架推向开源,并正式命名为 ComponentKit。

 

时至今日,COmponentKit 仍然是 Facebook 中原生 UI 的最佳构建选项。它通过视图重用池、视图扁平化和背景布局计算等功能,为应用带来了可观的性能改进。它甚至启发了 SwiftUI 乃至 Android 阵营的对手Litho

 

总而言之,选择用自定义基础设施替换 UI 和数据层代表着一种权衡。为了实现可靠、可维护且令人满意的用户体验,开发团队决定放下自己在 Apple API 上积累的专业知识,从零开始探索内部基础设施。

 

FBiOS 在最终用户体验与开发者体验/速度间的取舍,到这里才刚刚拉开帷幕。时间来到 2015 年,该应用的成功迅速引起了功能爆炸,一系列独特挑战也接踵而来。

 

2015 年:架构转折点

 

2015 年,Meta 开始全力强调其“移动优先”的口号,FBiOS 代码库的日均贡献量也开始急剧提升。随着越来越多产品被集成到该应用当中,程序的启动速度也越来越慢,甚至连普通用户都注意到了这一点。到 2015 年底,应用的启动时长已将近 30 秒,稍不留神就有可能被手机操作系统强制关闭。

 

经过调查,开发团队发现了一系列会导致启动性能下降的因素。为了简洁起见,这里我们只提对于应用架构存在长期影响的原因:

 

  • 由于应用体量随着新的产品和功能的增加而膨胀,应用的“premain”时间正在无限制延长。

  • 应用的“模块”系统允许各个产品不受约束地访问所有应用资源,于是引发了公共资源争用的闹剧。每个产品在启动时都会利用自己的“hook”执行高计算量操作,这就造成指向各产品的导航操作经常出现卡顿。

 

而要想对优化和改进启动过程,相应的变更将从根本上改变产品工程师在 FBiOS 上的代码编写方式。

 

2016 年:动态库与模块化

 

根据苹果关于改进启动时间的维基页面,在调用应用的“main”函数之前必须完成一系列操作。通常来说,应用本体的代码越多,启动所需的时间就越长。

 

虽然“pre-main”只占 30 秒启动时长当中的一小部分,但却特别值得关注。因为随着 FBiOS 新功能的不断引入,这部分负载会无休无止地膨胀、延长。

 

为了帮助缓解应用启动时间无限延长的问题,Facebook 工程师开始将大量产品代码转移到动态库(dylib)当中,也就是延迟加载容器。通过这种方式,这部分代码就不需要在应用的 main()函数前进行预加载了。

 

最初,FBiOS dylib 的结构如下所示: 



这里我们创建两个产品动态库(FBCamera 和 NotOnStartup),而第三个动态库(FBShared)是用于在各动态库和主应用的二进制文件间共享代码的。

 

动态库的效果很好,FBiOS 也终于遏制住了应用启动时间无限延长的问题。随着时光流逝,大部分代码最终都跑到了动态库里头,这样应用的启动性能就一直很快、不会受到应用中产品添加/删除的影响。

 

事实上,动态库的加入了彻底改变了 Meta 产品工程师编写代码的方式。在此之后,NSClassFromString() 这类运行时 API 所需要的类存在于未经加载的动态库内,所以可能引发运行时故障。另外,因为 FBiOS 中大量原有核心抽象均建立在遍历内存中所有类的基础之上,因此 FBiOS 被迫重新审视这套核心系统的有效性。

 

除了运行时故障之外,动态库还引发了一类新的链接器错误。如果 Facebook(启动集)中的代码引用了动态库中的代码,则链接器会向工程师弹出以下错误:


Undefined symbols for architecture arm64:  "_OBJC_CLASS_$_SomeClass", referenced from:      objc-class-ref in libFBSomeLibrary-9032370.a(FBSomeFile.mm.o)
复制代码

 

为了解决这个问题,工程师需要用特殊的函数将代码打包起来,由该函数在必要时加载动态库:

于是在一夜之间,原本的:

 

int main() {  DoSomething(context);}
复制代码

 

就变成了:

 

int main() {  FBCallFunctionInDylib(    NotOnStatupFramework,    DoSomething,    context  );}
复制代码

 

这么改当然不是不行,但却带来了新的麻烦:

 

  • 特定于应用的动态库枚举将被硬编码至各种调用站点上。Meta 的所有应用都必须共享同一个动态库枚举,读取方则须负责确定运行代码的应用是否使用了该动态库。

  • 如果使用了错误的动态库枚举,代码将失败,但这种失败仅发生在运行时上。由于应用内的代码和功能已经极为庞大,这个故障信号延后问题给开发工作制造了不少麻烦。

 

最重要的是,我们在启动期间防止过量引入调用的唯一系统,是以运行时为基础来实现的。应用的很多版本都曾因临时加入回归而被迫推迟发布。

 

总之,动态库优化确实遏制了应用启动时间的无限延长,但也标志着应用架构设计的重大转折点。FBiOS 工程师将在接下来几年内重构这款应用,打磨那些因引入动态库而造成的粗糙“接缝”。最终,我们发布了有史以来最为健壮的应用架构。

 

2017 年:重新审视 FBiOS 架构

随着动态库的引入,我们开始重新审视 FBiOS 中的几大关键组件:

 

  • “模块注册系统”不能再基于运行时。

  • 工程师需要一种新方法,以了解启动期间是否有代码路径会触发动态库加载。


为了解决这些问题,FBiOS 求助于 Meta 的开源构建系统 Buck。

 

在 Buck 当中,每个“target”(即目标,包括 app、dylib、library 等)都需要通过配置来声明,例如:

 

apple_binary(  name = "Facebook",  ...  deps = [    ":NotOnStartup#shared",    ":FBCamera#shared",  ],)apple_library(  name = "NotOnStartup",  srcs = [    "SomeFile.mm",  ],  labels = ["special_label"],  deps = [    ":PokesModule",    ...  ],)
复制代码

 

每个“target”都列出了构建它所需要的各项信息(包括依赖项、编译器标志、源代码等)。在调用“buck build”时,它会将所有这些信息构建成一份可查询的图表。

 

$ buck query “deps(:Facebook)”> :NotOnStartup> :FBCamera$ buck query “attrfilter(labels, special_label, deps(:Facebook))”> :NotOnStartup
复制代码

 

使用这个核心概念(再加上一些特殊的技术手段),FBiOS 中出现了可以在构建期间生成应用中类和函数整体视图的 buck 查询。这些信息,也成为下一代应用架构的设计基石。

 

2018 年:代码生成量激增

 

现在,FBiOS 已经能利用 Buck 查询相关依赖项中的代码信息,并创建出能够动态生成“函数/类->动态库”的映射。

 

{  "functions": {    "DoSomething": Dylib.NotOnStartup,    ...  },  "classes": {    "FBSomeClass": Dylib.SomeOtherOne  }}
复制代码

 

使用该映射作为输入,FBiOS 就能生成从 callsites 处抽象出动态库枚举的代码:

 

static std::unordered_map<const char *, Dylib> functionToDylib {{  { "DoSomething", Dylib.NotOnStartup },  { "FBSomeClass", Dylib.SomeOtherOne },  ...}};
复制代码


这种代码生成方式的优点包括:

 

  • 由于代码是根据本地输入重新生成的,所以不存在签入需求,也不存在大量合并冲突。考虑到 FBiOS 的工程体量每年都会翻一番,这将带来巨大的开发效率提升。

  • FBCallFunctionInDylib 不再需要特定于应用的动态库(所以不妨更名为「FBCallFunction」)。相反,该调用可以读取各应用在构建期间生成的静态映射。

 

事实证明,这种将 Buck 查询与代码生成相结合的办法非常成功,FBiOS 也因此将其作为新型插件系统的基石,最终取代了基于运行时的应用模块系统。

 

信号左移

 

借助 Buck 驱动型插件系统,开发团队得以将部分基础设施转为基于插件的新架构,让 FBiOS 用构建时警告取代了大部分运行时故障。

 

在构建 FBiOS 时,Buck 可以生成图表以展示应用中所有插件的位置,如下所示:

 


从积极的方面来看,这套插件系统可以显示构建时错误以提醒工程人员:

 

  • “插件 D、E 可能触发动态库加载。不允许这项操作,因为这些插件的调用者位于应用的启动路径之内。”

  • “在应用中找不到用于呈现 Profile 的插件……因此无法导航至该屏幕。”

  • “同时存在两个用于 Groups 呈现的插件(插件 A,插件 B),应删除其中一个。”

 

对于之前的应用模块系统,这些错误会被看作“懒”运行时断言。但现在,工程师们可以正建立信心,知道在 FBiOS 构建成功之后,就绝不会存在功能缺失、启动过程中加载动态库、或者模块运行时系统中包含恒量之类的问题。

 

代码生成的代价

 

虽然 FBiOS 在转向插件系统之后大大提升了应用可靠性,也能为工程师快速提供提示信号,降低了不同移动应用之间的代码共享门槛,但这一切都是有代价的:

 

  • Stack Overflow 上查不到插件错误,而且可能在调试期间造成混淆。

  • 基于代码生成和 Buck 的插件系统,与传统 iOS 开发思路相去甚远。

 

插件相当于给代码库引入了间接层。大部分应用都有一个包含所有功能的注册表文件,但此文件在 FBiOS 中由自动生成的,而且特别难以查找。

 

毫无疑问,插件的引入让 FBiOS 一步步远离了我们所熟悉的 iOS 开发,但这样的取舍似乎是值得的。我们的工程师可以调整 Meta 旗下各类应用中的代码,保证只要插件系统这边不出问题,那更新后的版本就基本能在代码路径中顺畅运行。像 News Feed 和 Groups 这样的团队还可以为插件构建扩展点,确保产品团队能在不触及核心代码的前提下将其集成到自有界面当中。

 

2020 年:Swift 与语言架构

 

虽然本文的大部分内容都在讨论 Facebook 应用规模所引起的架构变化,但 Apple SDK 的演进也确实对 FBiOS 的某些架构决策造成了影响。

 

2020 年,FBiOS 发现苹果那边仅限 Swift 使用的 API 数量越来越多,人们也更希望能在代码库中多用 Swift。到这个时候,我们必须承认 Swift 一定会在 Facebook 应用中占据一席之地——无论我们愿不愿意。

 

从历史上看,FBiOS 曾使用 C++作为构建抽象的杠杆。由于 C++的零开销优势,它显著降低了代码体量。但C++与Swift目前还无法实现互操作,所以大部分 FBiOS API(包括 ComponentKit)必须通过某种“垫片”才能在 Swift 中运行——这无疑会造成代码膨胀。

 

下图为 FBiOS 代码库中的语言架构问题:

 


 考虑到这一点,我们开始认真考虑什么时候、在哪里该使用哪种编程语言: 



最终,FBiOS 团队建议将 C++从面向产品的 API/代码中全面清理出去,以便大家可以自由使用 Swift 和苹果未来提供的 Swift API。通过插件,FBiOS 可以抽象出 C++实现,确保它们继续为应用提供支持;而对大多数工程师来说,又不必直接跟 C++打交道。

 

这样的工作流,标志着 FBiOS 工程师对于抽象构建的考量方式发生了转变。自 2014 年以来,框架构建中的最核心因素,就是对应用代码量和表现力的影响(正因为如此,ComponentKit 才选择了 Objective-C++,而非 Objective-C)。

 

Swift 的加入第一次下调了开发者效率的优先级,预计未来还会出现更多类似的情况。

 

2022 年:旅程刚刚走完 1%

 

自 2014 年以来,FBiOS 架构发生了重大变化:

 

  • 引入了无数内部抽象,包括 ComponentKit 和 GraphQL。

  • 使用动态库将“pre-main”时长保持在最低水平,并有助于加快应用启动速度。

  • 引入了插件系统(由 Buck 提供支持),将动态库从工程师那边抽象出来,降低了代码在不同应用间的共享难度。

  • 引入了什么时候在哪里该用什么语言的评判准则,并开始转变代码库以反映这些语言准则。


与此同时,苹果则在手机、操作系统和 SDK 层面做出了激动人心的改进:

 

  • 新 iPhone 的性能越来越强,加载成本比以往低得多。

  • 操作系统持续改进,dyld3 和 chain fixups 大大加快了软件代码的加载速度。

  • 推出 SwiftUI,一种用于 UI 的声明式 API,其中很多概念与 ComponentKit 相通。

  • 发布了经过改进的 SDK 和 API(例如 iOS8 中的可中断动画),可供开发者据此构建自定义框架。

 

随着 Facebook、Messenger、Instagram 和 WhatsApp 之间的可共享体验越来越多,FBiOS 也在重新审视之前做出的一系列优化,思考其中哪些可以跟苹果平台的原生功能合并起来。现在看来,共享代码的最简单方法,就是直接使用应用免费提供的功能、或者直接在构建过程中消除一切依赖项。能做到这一点,应用之间就能顺畅对接、毫无滞涩。

 

期待到 2032 年时,我们能在 FBiOS 的 20 岁生日时再相聚,看看那时会有哪些值得一聊的心得体会!

 

原文链接:

https://engineering.fb.com/2023/02/06/ios/facebook-ios-app-architecture/

相关阅读:

Facebook 是如何引入并使用 Rust 的?

Facebook 怎样为数百万观众提供可靠的直播服务?

跨平台开发成为移动应用程序开发趋势

移动应用程序开发新趋势

2023-03-15 16:149122

评论 1 条评论

发布
用户头像
专门的iOS团队,不考虑iOS的业务代码能在Android上使用?不用C++如何复用模块和代码。
2023-03-16 09:50 · 广东
回复
没有更多了
发现更多内容

到底什么是云?其实云计算从业者也不懂!

Geek_f56666

云计算

实时音视频入门学习:开源工程WebRTC的技术原理和使用浅析

JackJiang

音视频 WebRTC IM 即时通讯IM

金融小程序风险如何控制,WeTest小程序质量专项方案一步到位

WeTest

时空大数据 AI 研究院在京成立,助推时空产业高质量发展

百度大脑

人工智能

百度智能云以“3D+AI”技术,助力“三亿人上冰雪”

百度开发者中心

人工智能

LabVIEW实现PCB电路板坐标定位(实战篇—2)

不脱发的程序猿

机器视觉 图像处理 LabVIEW PCB电路板坐标定位

元宇宙时代:银行如何探索数字化转型

CECBC

2022 起始篇

万万

关于A股投资--《香帅中国财富报告》摘录(3/100)

hackstoic

投资

Tengine + BabaSSL ,让国密更易用!

SOFAStack

密码学 tengine 国密 BABASSL

Cloudera的企业数据云

郭华

云计算 大数据 数据仓库 云原生

2022中国低代码十大发展趋势,市场规模预计达42.6亿

J2PaaS低代码平台

低代码 低代码开发 J2PaaS

一周信创舆情观察(1.4~1.9)

统小信uos

组件发布效率提升15倍是怎么做到的——基于 Gradle 调度机制深度研究与优化

字节跳动终端技术

字节跳动 Gradle Andriod

不是私密链接,如何继续前往?

BUG侦探

https HSTS 劫持

Flink 在字节跳动数据流的实践

字节跳动数据平台

大数据 flink 字节跳动 埋点 数据流

ORTC与SIP融合通信服务架构

安第斯智能云

音视频 RTC 流媒体

重磅功能!Apache APISIX 拥抱 WASM 生态

API7.ai 技术团队

开源 云原生 Wasm Apache APISIX

InfoQ写作平台|Python板块试运行

IT蜗壳-Tango

InfoQ-Python板块

龙蜥利器:系统运维工具 SysAK的云上应用性能诊断 | 龙蜥技术

OpenAnolis小助手

开源 运维

酒店资产管理系统解决方案

低代码小观

CRM 企业管理系统 CRM系统 企业管理工具 企业管理软件

专有云运维如何更快、更准、更稳?丨智能运维

百度大脑

人工智能

金融云原生漫谈(五)|如何打造更适合云原生的数据存储方案?

York

云原生 数据存储

一个cpp协程库的前世今生(十七)带时限的锁

SkyFire

c++ cocpp

不优雅的 React Hooks

CRMEB

Go语言gorm框架MySQL实践

FunTester

Go MySQL gorm FunTester

一步一步理解Generator函数的原理

恒生LIGHT云社区

JavaScript 前端 Generator

微软Office新增实用功能允许用户在不同设备上轻松送同步字体

淋雨

JuiceFS 即将发布 1.0 并调整开源许可

Juicedata

开源 文件系统 云存储

「干货分享」如何做好应急响应工作?常见应急响应流程

H

网络安全 应急响应

VuePress 博客优化之开启 HTTPS

冴羽

Vue 前端 博客 vuepress 博客搭建

Facebook iOS版:探索移动应用10年演进之路_移动_InfoQ精选文章