本博文由 Google 与 Meta 团队合作撰写
Instagram Direct 是 Instagram 的核心平台之一,每天处理数十亿条用户消息。经过多年的迭代,该团队从旧版 Android View 系统中榨取了所有可能的微优化。然而,维护和扩展经过深度优化的旧版途径会产生巨大的技术债务和工程开销,尤其是在小组越来越多地采用声明式界面和 AI 编码辅助的情况下。
Instagram Direct 采用 Jetpack Compose,不仅仅是实现界面现代化。该团队构建了一个 AI 原生界面代码库,其规模比原始实现缩小了 50% ,同时将 AI 智能体执行时间缩短了 35% ,将工程师与智能体之间的交流次数减少了 32% ,并将 token 成本降低了 33% 。该团队与 Google 密切合作,在采用 Jetpack Compose 的同时,保持了较高的性能标准。通过性能优化,Meta 和 Google 不仅改进了 Instagram 的 Compose,还改进了更广泛的 Android 开发者生态系统。
大规模实现代码库现代化改造
AI 已迅速成为业内工程师的日常助手,将其应用于 Instagram 这样的大规模代码库,已经带来了切实的效率提升。Instagram Direct 团队设定了更宏大的目标。该团队没有简单地将 AI 工具应用于现有代码,而是重新设计了代码库及其架构,使其原生支持 AI,从而使 AI 的影响远远超出了单纯改造所能达到的效果。
Instagram Direct 团队选择 Jetpack Compose 作为构建 AI 原生界面架构的关键组件。其声明式特性可确保代码简洁、可预测,并且在结构上更易于 AI 模型进行推理,同时副作用更少、隐式状态更少、组件边界更清晰。
迁移到 Jetpack Compose 需要周密的规划。每天都有数亿人在 Instagram 上发送消息,因此迁移必须循序渐进、平稳进行,在团队重新构建底层基础架构的同时,不能对用户体验造成任何干扰。为了说明这一挑战的规模,我们来看一组数据:单个界面组件可以呈现超过 160 种不同的状态排列组合,而仅一个对话界面就需要处理超过 200 种不同的消息类型。
将这种规模的代码库迁移到 Compose 时,很容易选择走捷径,将 Compose 界面组件嵌入到现有的视图层次结构中。作为逐步迁移过程中的一个增量步骤,这完全可行。不过,从长远来看,在基于 View 的代码库中集成 Compose 会带来挑战。AI 工具通常会选择阻力最小的路径。如果您混合使用声明式和命令式界面代码,AI 可能会错误地将它们混合在一起,从而引入细微的 bug、技术债务和性能回归。
构建 AI 原生界面架构
在 Instagram 这样的规模下,一定程度的架构抽象是不可避免的,这也是应用在不断发展壮大的同时保持可维护性的关键。考虑一种常见模式,其中每个 RecyclerView 商品种类都建模为自定义 RecyclerViewItem 基类的后代,该基类会公开常见的生命周期钩子,例如 onBind。
示例 1
class ChatItem( val features: FeatureFlagProvider ) : RecyclerViewItem<ComposeViewHolder, ChatUiState> { // Imperative context: // AI could often take the path of least resistance and generate a mutable // state here, dispatched outside the ChatUiState. This class survives // re-bindings and is shared across multiple items, ultimately leading to // unexpected, hard-to-reproduce bugs. var isPinned: Boolean = false override fun onBind(holder: ComposeViewHolder, uiState: ChatUiState) { // Imperative context val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature") // Declarative context holder.composeView.setContent { // Blending imperative and declarative contexts if (isPinnedChatsEnabled) { Button(onClick = { isPinned = !isPinned }) { Text(if (isPinned) "Unpin" else "Pin") } } ... } } }
在上面的代码段中,出现了两个问题。首先,在命令式代码中读取 isPinnedChatsEnabled 标志,然后在 Compose lambda 中捕获该标志,这是跨范式的细微耦合。其次,isPinned 作为可变字段存在于项本身(而不是 ChatUiState)中,因此它会在 RecyclerView 重新绑定和跨行回收后继续存在,从而导致泄露并产生难以重现的 bug。
即使通过为该项提供专用 @Composable 函数来清理代码,同样的问题仍然存在。
示例 2
class ChatItem( val features: FeatureFlagProvider ) : ComposeRecyclerViewItem<ChatUiState> { // Imperative context val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature") var isPinned: Boolean = false // Declarative context @Composable override fun Content(uiState: ChatUiState) { // Blending imperative and declarative contexts if (isPinnedChatsEnabled) { Button(onClick = { isPinned = !isPinned }) { Text(if (isPinned) "Unpin" else "Pin") } } ... } }
这个例子虽然简单,但却揭示了一个更广泛的问题:AI 受到的约束越少,其生成的代码质量就会随着时间的推移而下降。护栏和技能会有所帮助,但仅靠这些还不够,因为当 AI 遇到阻碍时,它通常会绕过这些限制来解除阻碍。
为了使代码库便于 AI 使用,需要遵循以下两条实用规则:
- 尽量减少对自定义上下文的依赖。AI 智能体需要掌握的特定于代码库的定制知识越多,其输出质量就越低。代码库越接近已知最佳实践,AI 生成的结果就越好。
- AI 优先的代码库必须强制执行自己的边界。用 AI 技能来弥补设计缺陷并非长久之计,因为加载到上下文中的每项技能都会消耗 token,并可能降低智能体的性能。相反,架构本身应该承担这一重任。AI 智能体自然会选择阻力最小的路径,因此设计应确保该路径能生成正确、高质量的代码,同时让糟糕的设计决策难以表达且成本高昂。
列表项仍可由其自己的抽象表示,但在这种情况下,所有 Compose 代码都位于构造函数中,因此它无法访问类成员或状态,并且其唯一的实参来源是构造函数。这使其等同于一个普通的 @Composable 函数,同时符合现有架构。
示例 3
class ChatItem( val features: FeatureFlagProvider, val onPin: (Boolean) -> Unit, ) : ComposeItem<ChatUiState>( // Compose UI content = { uiState: ChatUiState -> val isPinnedChatsEnabled = features.isEnabled("pinned_chats_feature") if (isPinnedChatsEnabled) { Button(onClick = { onPin(!uiState.isPinned) }) { Text(if (uiState.isPinned) "Unpin" else "Pin") } } ... }, )
迁移如此庞大的代码库是一项艰巨的任务。长期以来,构成 Direct UI 主体的数百个界面组件不得不与旧版组件共存,并且两者需要并行维护。AI 工作流加快了编写大量代码的过程,从而使这种并行迁移成为可能。正是这种方法,让 Direct 团队在创纪录的时间内完成了迁移,同时没有干扰团队的其他成员,他们继续发布功能,每天改善数百万人的体验。
多位工程师针对在迁移过程中构建的可重用技能和惯例的共享知识库,运行了自己的 AI 智能体。这让整个团队的工作流程和最佳实践保持同步,而不是让每位工程师重新探索。在每个平台上,该团队分以下几个阶段执行了迁移:
- 使用 AI 编写所有 Compose 代码。
- 他们不断打磨,处理极端情况并弥合性能差距,直到该界面在公开测试中向真实用户推出。
将每个屏幕的工作分为两个阶段,让一位工程师能够快速完成整个界面的开发,提前解决架构和棘手的极端情况。有了这些基础工作,其他人就可以专注于让界面做好可用于生产用途的准备,而无需停下来自己做出这些技术决策,从而保持整体迁移速度。
迁移结果验证了该方法的有效性。对于已迁移的 Instagram Direct 界面,Jetpack Compose 帮助团队将界面代码总量减少了 50%。AI 需要生成的代码越少,输出质量越高,每项任务的 token 成本越低。
一项针对 Instagram Direct 的 Android 代码库进行的内部数据分析,对比了 AI 智能体在使用 Compose UI 和 Android View 处理相同任务时的会话情况。效率提升在两个维度上表现得尤为明显:
- 按落地代码的字符数计算:Compose 所需的工程师与智能体交互次数减少了 32% ,智能体执行时间减少了 35% (即从智能体开始处理工程师请求到返回响应所经过的时间)。
- 每个代理会话:与 View 相比,Compose 的总体 token 费用降低了 33% 。
我们同时报告了输出效率和典型会话数,因为它们都是有用的独立结果。工程师与智能体之间的交互次数和执行时间数据比较的是单位落地输出的资源使用情况,而 token 数据比较的是典型智能体对话的总成本。
数据还揭示了两个框架在处理复杂或脆弱代码时存在一致的差异。Meta 通过代码更改风险评分来跟踪这一指标,该评分用于评估整体代码质量以及更改导致生产环境突发事件的可能性。该分析通过综合考量 token 消耗、智能体执行时间以及工程师与智能体的互动情况,来衡量智能体的资源效率。随着文件累积的风险评分越来越高,AI 智能体会话的资源效率自然会降低。
当文件的累积风险评分翻倍时,使用 Android 视图实现的界面会将代理资源效率降低 30%(按着陆字符计算)。在相同情况下, Jetpack Compose 界面仅减少 9%
通过 Google 与 Meta 的合作,Instagram Direct 团队为 Compose 的采用带来了全新的视角:从代码库的 AI 就绪程度(而不仅仅是界面重写)角度来审视。这项工作表明,Compose 非常适合作为构建 AI 优先代码库和架构的基础,尤其是在 Instagram 这样的大型应用中。
性能优化
Instagram Direct 是该应用中最重要的一个平台,用户希望它始终能够快速响应。采用 Jetpack Compose 实际上意味着需要对界面进行大量重写,而首要目标是保持高质量体验,不出现任何性能退化。
经过多年的迭代,Instagram 上基于 View 的旧版实现已经达到了极高的性能标准,而团队在迁移到全新的界面框架时,也需要达到同样的标准。
Instagram 会衡量数百甚至数千个效果指标。对于 Compose 的采用,以下三点最为重要:
- 可交互时间 - 从打开界面到能够使用界面所经历的时间。
- 完全加载所用时间 - 从打开屏幕到所有内容(例如图片)完全加载所需的时间。
- 滚动性能 - 屏幕滚动的流畅程度,不出现丢帧。
这些指标会在生产环境中运行时进行跟踪,以便运行 A/B 测试,将迁移后的 Compose 界面与旧版界面进行比较,并评估此项工作对性能的影响。
此类迁移的常见方法是先从小处着手,迁移少量界面组件,收集数据并研究其行为。虽然这些早期结果很有帮助,但它们只能反映部分情况,并会针对 Compose 采用情况提供假负例,原因如下:
- 不具代表性 - 一个迁移后的界面组件可以提供有关其在特定屏幕上的整体性能的实用数据。不过,不同组件的行为方式各不相同,原因也无法一概而论,因此您无法总是据此进行推断。
- 互操作成本 - 大型 View 代码库中的一小部分 Compose 代码会产生两个系统之间不可预测的桥接成本。这种开销会使测量结果失真,因此早期的少量迁移结果并不能反映完全迁移后的实际情况。
结果是,小规模迁移虽然有用,但并不总能反映 Compose 的全部影响。 在不中断桥接的情况下,端到端迁移的界面越多,性能方面的表现就越清晰、越好。
Instagram Direct 中的核心界面围绕各种商品种类构建,最初使用 RecyclerView 实现。该架构依赖于自定义抽象来实现可伸缩性,但仍受限于基于 View 的系统的生命周期。
该团队的主要任务是在现有的基于 RecyclerView 的架构中,逐步将数百个单独的列表项迁移到 Compose,并在 A/B 测试下以小型独立群组的形式在生产环境中发布,同时确保用户的消息传递体验不会发生明显变化。
这种设置的最大缺点是,即使将每个列表项完全迁移到 Compose,也会通过核心 RecyclerView 架构严重依赖旧版 View 系统。自然而然地,团队决定投资将基于 RecyclerView 的核心架构替换为 Compose 原生替代方案 LazyColumn。
这意味着,Compose 界面组件应与其所在的框架分离开来,同时仍能与 RecyclerView 和 LazyColumn 兼容。同样重要的是,能够在运行时通过功能标志在两者之间切换,以进行 A/B 测试。
虽然新的 Compose 项原生兼容 LazyColumn,并且可以插入到不间断的组合树中,但我们还创建了一个互操作 API,以便将它们也插入到 RecyclerView 中。这样一来,他们就可以在 A/B 测试中同时推出 LazyColumn 设置和 RecyclerView,从而重复使用相同的 Compose 项并提升性能,而不会干扰团队其他成员构建和优化功能。
Instagram 的规模、复杂性以及对哪怕是最小的回归问题的敏感性,都对 Jetpack Compose 提出了独特的挑战。解决这些问题需要反复实践。Google 和 Meta 的工程师密切合作,分析了各项指标,以确定并设计新的 Compose 功能,从而达到或超越基于 View 的基准。通过此次合作,Jetpack Compose 取得了以下显著进展:支持使用 LazyLayoutCacheWindows 暂停组合,以及支持可见性跟踪。
使用 LazyLayoutCacheWindows 的可暂停组合
可暂停的组合(在 Compose 1.10 中默认启用)允许跨帧以增量方式组合开销较大的延迟列表项,以防止卡顿。与 LazyLayoutCacheWindow(在 Compose 1.9 中添加)搭配使用时,滚动流畅度会显著提升。在 Meta 最近的内部测试中,与原生 Compose 相比,将可暂停的组合与一个视口 LazyLayoutCacheWindow 结合使用,可将每分钟的大幅掉帧 (LFDs/m) 减少约 13%。与同一基准相比,仅使用 Cache Window 就能将延迟降低约 8%。LFDs/m 是 Meta 用于跟踪滚动时明显卡顿的内部指标。
在应用中使用 LazyLayoutCacheWindow 可在视口周围基于像素的带状区域内准备和保留屏幕外项,从而实现快速滑动。如需在应用中利用 LazyLayoutCacheWindows,您可以使用最新的 Compose 1.13.0-alpha03,并按照以下示例所示进行设置:
val cacheWindow = LazyLayoutCacheWindow(ahead = 150.dp, behind = 100.dp) // OR val cacheWindow = LazyLayoutCacheWindow(aheadFraction = 0.5f, behindFraction = 0.3f) LazyColumn(state = state, cacheWindow = cacheWindow) { ... }
您可以通过两种方式配置缓存窗口。两者描述的是同一件事:要保持组合的屏幕外内容量,但单位不同。
- Dp:固定的绝对长度。无论设备如何,
ahead = 150.dp都会保留 150dp 的内容,这些内容在可见边缘之外。 - 浮点数:视口的分数。
aheadFraction = 0.5f会提前合成半个屏幕的内容,因此绝对数量会随着屏幕高度而变化,从而支持各种外形规格:在平板电脑或展开的可折叠设备上合成的内容较多,在紧凑型手机上合成的内容较少。
Instagram 团队专门针对 Direct 的内容结构和内容大小,对缓存窗口的浮点分数进行了微调。由于理想值因具体界面参数而异,因此需要进行一些实验才能找到合适的平衡点。
使用 onVisibilityChanged 记录展示
onVisibilityChanged(在 Compose 1.9.0 中添加)API 是 Google 与 Meta 技术合作的另一项重要成果。它为大规模 Jetpack Compose 界面提供了一种一致的方式,让其知道可组合项何时在屏幕上实际可见,从而取代了过去使用的自定义手动实现。仅在 Instagram Direct 中,这些可见性信号就用于数百个文件,以支持产品质量指标,这些指标取决于界面元素是否实际向用户显示。
启动性能
在 Instagram Direct 中采用 Jetpack Compose 后,该应用的其他界面也获得了意想不到的性能提升。Jetpack Compose 运行时会产生预热成本,但只需支付一次。由于消息功能是一个高流量界面,通常在用户会话早期就会被访问,因此 Instagram 中依赖 Compose 的其他界面也获得了显著的性能提升。
Instagram Direct 内部 Compose 界面的启动性能通过使用基准配置文件进行了优化,这些配置文件会在安装时预编译热代码路径,以便 Compose 从首次启动开始就能快速渲染。
Instagram Direct 迁移到 Jetpack Compose 的经验教训
- Jetpack Compose 可立即带来投资回报率:您无需使用高级 AI 工作流即可从 Compose 中受益。代码量减少约 50% 意味着需要维护的代码更少,出现 bug 的可能性也更低。
- 设计 AI 原生架构带来了显著成效,包括 AI 智能体执行时间缩短 35%、工程师与智能体之间的交互减少 32%,以及 token 成本降低 33%。
- 虽然有许多互操作 API,并且支持将 View 和 Compose 结合使用,但请尽量迁移较大的界面,而不是单个小型组件。这样可将界面保持在单个不间断的组合层次结构中,并解锁所有最佳的 Compose 原生性能优化。
- 将可暂停的组合与 LazyLayoutCacheWindow 搭配使用:将这两者搭配使用比单独使用缓存窗口效果更好。如果只有缓存窗口,较重的项仍可能会尝试在单次传递中进行组合,这可能会超出帧预算。
- 为 Compose 本身做出贡献!Meta 与 Jetpack Compose 团队合作,在 Compose 中实现他们的反馈和想法。使用开源工具包意味着,当集中修复 bug 和提升性能时,我们所有人都会受益。因此,请分享您的反馈!
采用 Jetpack Compose 后,Instagram 不仅在 AI 辅助开发方面取得了显著进展,还简化了日常界面工程。声明式方法可减少样板代码,使状态更易于理解,并提高开发者的整体工作效率。Instagram 工程团队期待将 Compose 引入应用中的更多界面,并期待 Google 与 Meta 继续合作,为 Instagram 和 Jetpack Compose 用户带来更多改进。
如果您尚未尝试过 Compose,现在有了 AI 辅助功能,迁移到 Jetpack Compose 比以往更加轻松。
致谢。感谢 Meta 的 Michal Zielinski 和 Matthew Du,以及 Google 的 Andrei Shikov 和 George Mount,他们通过 Meta 和 Google 之间的合作,为 Compose 带来了性能提升!同时,还要感谢 Meta 的 Gary Ye 帮助将 Compose 引入 Instagram Direct,以及 Meta 的 Gopal Juneja 通过数据科学为这项工作提供支持!
-
案例研究Grab 是东南亚领先的超级应用,通过单一应用为八个国家/地区的数百万用户提供配送、出行和金融服务。
Ajesh Pai, Nguyen Van Minh, Tracy Agyemang • 阅读用时:5 分钟 -
案例研究WhatsApp 是全球最大的即时通讯平台,为全球数十亿用户提供服务。它是不同地区用户的默认通讯工具,通过私密、可靠且安全的即时通讯服务连接用户。
Niharika Arora, Tracy Agyemang, Mayank Jain • 阅读用时:8 分钟 -
案例研究Tinder 的使命是让每一代单身人士都能轻松有趣地结识新朋友,从而促进和激发真实的联系。
Ajesh Pai, Ulises Uriel Verduzco Díaz , Tracy Agyemang • 阅读用时:4 分钟
每周通过电子邮件接收最新的 Android 开发洞见。