本文译自「Exploring PausableComposition internals in Jetpack Compose」,原文链接blog.shreyaspatil.dev/exploring-p...,由Shreyas Patil发布于2025年7月14日。

嗨,Composers 们👋,在最近的 Compose 1.9.X 版本中,Compose-runtime 引入了一个名为 PausableComposition 的新内部 API,据称它可以解决性能问题。它听起来像魔法,但在底层,这一切都归功于一些非常巧妙的工程设计。在深入研究 Compose 运行时以更好地理解这一点时,我偶然发现了一个强大的内部工具,它使这一切成为可能。
这篇文章将深入分析这一机制:PausableComposition。这是 Compose 的内部 API,开发者无需了解它。但了解它的底层工作原理总是有益的。对于想要深入了解 Compose 如何实现其惊人性能的 Jetpack Compose 开发者来说,这篇探索文章将为你提供更清晰的视角。我们将深入运行时源代码,了解它的工作原理、它对性能如此重要的原因,以及如何协调所有组件以使我们的 UI 感觉如此流畅。让我们开始吧!
缘起
为了实现流畅的 60 帧/秒 (fps),我们的应用需要在 16.7 毫秒内绘制每一帧。当用户滚动浏览 LazyColumn 时,必须在这个微小的窗口内创建、测量和绘制新的项目。
如果一个项目很复杂,包含嵌套布局、图片和大量逻辑,那么组合它所需的工作很容易超过 16 毫秒。当这种情况发生时,主线程会被阻塞,帧会丢失,用户会在滚动过程中看到"卡顿"或卡顿。😩
这正是 PausableComposition 的初衷。
"做什么":更智能的 Compose 方式
想象一下,你是一位厨师,正在为一场活动准备一顿大餐。👨🍳 与其在第一位客人到来时慌乱地从头开始烹饪所有食材,不如提前几个小时做好准备工作。切菜、调酱、烤甜点。等到上桌时,最后的烹饪和组装速度会快得令人难以置信。
PausableComposition 将这种"准备工作"的理念带到了 Compose 中。它允许运行时:
- 增量式 Compose:将大型 UI 元素的合成分解成更小、更易于管理的部分。
- 异步准备:在 UI 真正需要显示在屏幕上之前进行合成工作,通常利用帧间的空闲时间。
这种可组合项的预热意味着,当某个项目最终滚动到视图中时,大部分繁重的工作已经完成,使其几乎可以立即显示。
为了直观地理解这一概念,请观看以下动画:

滚动发生时,假设项目 A、B、C、D 和 E 已在屏幕上可见,下一个项目是 F。如果项目 F 的布局或结构复杂,需要更多时间进行布局计算或其他预处理才能在 UI 上渲染,则此预处理将在帧时间轴内分块进行(例如 16 毫秒)。因此,如果它需要 2 帧,则 F 所需的预处理会在 2 帧的空闲时间内完成,不会造成任何帧卡顿。最后,当需要显示时,它会被绘制到 UI 上。项目 G 和 H 也采用相同的流程。
工作原理:核心组件
通过查看运行时源代码,我们可以看到它是如何通过一些关键接口和类来处理的。虽然你不会直接使用这些 API,但理解它们可以揭示 LazyColumn 的性能提升。🕵️♂️
生命周期:PausableComposition 及其控制器
旅程从 PausableComposition 接口开始,该接口扩展了 ReusableComposition 并添加了暂停功能。
关于 ReusableComposition 的简要说明:
在讨论暂停之前,我们先来了解一下什么是 ReusableComposition?它是一种特殊的组合,专为需要高效回收 UI 内容的高性能场景而设计。想象一下 LazyColumn 中的项目。ReusableComposition 不会销毁滚动到屏幕外的项目的整个组合,而是允许运行时停用它。这会保留底层 UI 节点,但会清除已记住的状态。然后,这个停用的组合可以快速地用新内容"重新填充",从而节省了从头创建节点的成本。PausableComposition 直接构建于这个强大的回收基础之上。
PausableComposition 的外观如下:
Kotlin
// https://cs.android.com/androidx/platform/frameworks/support/+/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a:compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt;l=66
sealed interface PausableComposition : ReusableComposition {
fun setPausableContent(content: @Composable () -> Unit): PausedComposition
fun setPausableContentWithReuse(content: @Composable () -> Unit): PausedComposition
}
(注意:该接口是密封的,因为它仅在 Compose 运行时内部具有一组封闭且有限的实现。这为编译器提供了更多信息来进行优化。)
调用 setPausableContent 不会立即组合界面。相反,它会返回一个 PausedComposition 对象,该对象充当逐步过程的控制器。
Kotlin
// https://cs.android.com/androidx/platform/frameworks/support/+/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a:compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt;l=112
sealed interface PausedComposition {
val isComplete: Boolean
fun resume(shouldPause: ShouldPauseCallback): Boolean
fun apply()
fun cancel()
}
这个生命周期最好以状态机的形式来表示:

- resume(shouldPause: ShouldPauseCallback):这是引擎。预取系统(此处指 LazyColumn 上下文)会反复调用 resume() 来执行大量的组合工作。神奇之处在于 shouldPause 回调。Compose 运行时会在组合过程中频繁调用此 lambda。如果它返回 true(例如,由于帧截止时间已近),则组合过程将停止,并将主线程交还给更重要的工作,例如绘制当前帧。
- apply():一旦 resume() 返回 true,即表示操作完成,就会调用 apply()。这会获取所有计算出的界面更改,并将其提交到实际的界面树中。
- cancel():如果用户滚动离开,并且不再需要预先组合的项目,则会调用 cancellation() 来丢弃工作并释放资源。
内部结构概览:PausedCompositionImpl
上述状态机由内部的 PausedCompositionImpl 类管理。该类保存状态并连接所有部分。
Kotlin
// https://cs.android.com/androidx/platform/frameworks/support/+/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a:compose/runtime/runtime/src/commonMain/kotlin/androidx/compose/runtime/PausableComposition.kt;l=202
internal class PausedCompositionImpl(...) : PausedComposition {
private var state = PausedCompositionState.InitialPending
internal val pausableApplier = RecordingApplier(applier.current)
// ...
override fun resume(shouldPause: ShouldPauseCallback): Boolean {
when (state) {
PausedCompositionState.InitialPending -> {
// This is the first time resume() is called.
// It starts the initial composition of the content.
invalidScopes =
context.composeInitialPaused(composition, shouldPause, content)
state = PausedCompositionState.RecomposePending
if (invalidScopes.isEmpty()) markComplete()
}
PausedCompositionState.RecomposePending -> {
// This is for subsequent calls to resume().
state = PausedCompositionState.Recomposing
// It tells the Composer to continue where it left off,
// processing any pending invalidations.
invalidScopes =
context.recomposePaused(composition, shouldPause, invalidScopes)
state = PausedCompositionState.RecomposePending
if (invalidScopes.isEmpty()) markComplete()
}
// ... other states like Recomposing, Applied, Cancelled are handled here ...
}
return isComplete
}
override fun apply() {
// ... other state checks ...
if (state == PausedCompositionState.ApplyPending) {
applyChanges() // The call site
state = PausedCompositionState.Applied
}
// ...
}
private fun applyChanges() {
// ...
pausableApplier.playTo(applier, rememberManager)
rememberManager.dispatchRememberObservers()
rememberManager.dispatchSideEffects()
// ...
}
}
调用 resume() 时,它会检查其内部状态并采取相应的措施:
- InitialPending:首次调用时,它会通过调用 context.composeInitialPaused 启动合成过程。这会告知核心 ComposerImpl 开始执行 @Composable 内容,并执行 shouldPause 回调。
- RecomposePending:后续调用时,它会通过调用 context.recomposePaused 继续工作。此方法用于处理合成中任何因状态变化而失效的部分,或继续之前暂停的工作。
- Applier:在此过程中,ComposerImpl 将所有 UI 更改操作转发给 pausableApplier(即 RecordingApplier),该操作会进行缓冲,而不是立即应用。
- 此过程持续进行,直到工作完成或 shouldPause 回调返回 true。
RecordingApplier:推迟最后的润色
一个关键的性能技巧是 RecordingApplier。调用 resume() 时,Composer 不会直接更改实时 UI 树。如果分小步执行,可能会很慢,并导致 UI 更新不完整,显得怪异。
PausableComposition 使用的是 RecordingApplier。这个特殊的 Applier 会将其应该执行的所有 UI 操作(例如"创建 Text 节点"、"设置其文本属性"或"添加子图像")记录到一个内部列表中。
只有调用 PausedComposition.apply() 时,RecordingApplier 才会将其记录的操作列表"回放"到实际的 Applier 上,从而高效地单步更新 UI 树。PausedComposition 的公共 apply() 方法是一个简单的状态机守卫。真正的工作发生在内部的 applyChanges() 方法中(如上面的代码片段所示)。
当调用 applyChanges 时,它会按顺序执行三项关键操作:
- 它会告诉 RecordingApplier 将其所有缓冲的命令播放到实际的 applier 上。这才是 UI 真正出现在屏幕上的关键。
- 它会为所有已创建的 RememberObservers(例如 DisposableEffect)调度所有 onRemembered 生命周期回调。
- 最后,它会运行在合成过程中排队的所有 SideEffect。
这种有序的批处理过程确保 UI 高效更新,并且所有生命周期事件都在正确的时间发生。

使用了 PausableComposition 的 LazyList
LazyList 已经开始使用 PausableComposition API了。在 LazyList 中,PausableComposition 并非独立工作,而是协同工作的系统的一部分。
- 指挥器 (Recomposer):主 Recomposer 负责控制节奏,驱动可见 UI 的逐帧更新。
- 规划器 (LazyLayoutPrefetchState):当用户滚动时,此组件会预测哪些项目即将显示。
- 舞台管理器 (SubcomposeLayout):这个强大的 SubcomposeLayout 是 LazyList 的基础。它的 SubcomposeLayoutState 可以在需要时为各个项目创建和管理合成。最重要的是,它提供了 createPausedPrecomposition() API。
- 舞台调度器 (PrefetchScheduler):此调度器会在帧之间寻找空闲时间来执行规划器请求的预合成工作。
了解此功能的开发过程也很有趣。在 LazyLayoutPrefetchState 文件中,你可以找到控制它的功能标志:
Kotlin
// A simplified look inside LazyLayoutPrefetchState.kt: https://cs.android.com/androidx/platform/frameworks/support/+/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a:compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt;l=647
if (ComposeFoundationFlags.isPausableCompositionInPrefetchEnabled) {
// This is the future, modern path.
performPausableComposition(key, contentType, average)
} else {
// This is the older, non-pausable fallback.
performFullComposition(key, contentType)
}
isPausableCompositionInPrefetchEnabled 这个标志充当了终止开关的作用。虽然它在源代码中的默认值为 false。如果你想在惰性布局(LazyColumn、LazyRow 等)中启用可暂停组合行为,我们可以简单地按如下方式启用它:
Kotlin
class MyApplication : Application() {
fun onCreate() {
ComposeFoundationFlags.isPausableCompositionInPrefetchEnabled = true
super.onCreate()
}
}
规划器:LazyLayoutPrefetchState 详解
LazyLayoutPrefetchState 是预取操作的核心。它的作用是获取来自 LazyLayout 的预测(例如,"第 25 项即将上线"),并将其转换为实际的预组合任务。
它通过 PrefetchHandleProvider 实现此操作,该提供者会创建一个 PrefetchRequest。此请求是 PrefetchScheduler 可以执行的工作单元。在这个请求中,我们找到了暂停逻辑的核心。
当 PrefetchScheduler 执行请求时,它会进入一个循环,在 PausableComposition 上调用 resume()。传递给 resume 的 lambda 表达式决定是否暂停。
因此,如果启用了上述功能标记,它将通过 Pausable Composition API 执行请求,如下所示:
Kotlin
// https://cs.android.com/androidx/platform/frameworks/support/+/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a:compose/foundation/foundation/src/commonMain/kotlin/androidx/compose/foundation/lazy/layout/LazyLayoutPrefetchState.kt;l=754
// Simplified from HandleAndRequestImpl inside LazyLayoutPrefetchState
private fun PrefetchRequestScope.performPausableComposition(...) {
val composition = // get the composition for the item of the LazyLayout
pauseRequested = false
while (!composition.isComplete && !pauseRequested) {
composition.resume {
if (!pauseRequested) {
// 1. Update how much time is left in this frame's idle window.
updateElapsedAndAvailableTime()
// 2. Save how long this work chunk took, to improve future estimates.
averages.saveResumeTimeNanos(elapsedTimeNanos)
// 3. The Core Decision: Is there enough time left to do another
// chunk of work without risking a frame drop?
pauseRequested = !shouldExecute(
availableTimeNanos,
averages.resumeTimeNanos + averages.pauseTimeNanos,
)
}
// 4. Return the decision to the composition engine.
pauseRequested
}
}
updateElapsedAndAvailableTime()
if (pauseRequested) {
// If we decided to pause, record how long the final pause check took.
averages.savePauseTimeNanos(elapsedTimeNanos)
} else {
// If we finished without pausing, record the time for the final resume chunk.
averages.saveResumeTimeNanos(elapsedTimeNanos)
}
}
让我们分解一下这个逻辑:
- updateElapsedAndAvailableTime():在恢复 lambda 函数内部,系统会不断检查距离下一帧需要绘制还剩多少时间。
- averages.saveResumeTimeNanos(...):它会记录每个小块合成工作所需的时间。这有助于它构建一个平均值 (averages) 来预测未来工作的成本。
- !shouldExecute(...):这是核心决策。它会将 availableTimeNanos 与预算进行比较。这个预算是一个智能估算:完成另一块工作所需的平均时间加上暂停所需的平均时间。如果时间不足,pauseRequested 会变为 true。
- 最终计时:在本次循环退出后(因为工作完成或请求暂停),会调用最后一次 updateElapsedAndAvailableTime()。这会捕获最后一个操作的时间。
- 保存平均值:然后系统会保存这个最终计时。如果请求了暂停,则它会影响 pauseTimeNanos。如果循环自然完成,则它会影响 resumeTimeNanos。这确保了用于未来预测的历史数据始终准确。
这种自我调节的反馈循环允许预取器在系统空闲时保持积极主动,但在需要渲染 UI 时又能保持礼貌,尊重主线程。
最后一步:应用预组合 UI
那么,当屏幕上真正需要预组合的项目时会发生什么呢?这时 SubcomposeLayout 就占据了中心位置。在正常的测量过程中,它会为现在可见的项目调用其 subcompose 函数。在内部,这会触发最后一步。
Kotlin
// https://cs.android.com/androidx/platform/frameworks/support/+/8d08d42d60f7cc7ec0034d0b7ff6fd953516d96a:compose/ui/ui/src/commonMain/kotlin/androidx/compose/ui/layout/SubcomposeLayout.kt;l=1186
// Simplified from LayoutNodeSubcompositionsState inside SubcomposeLayout.kt
private fun NodeState.applyPausedPrecomposition(shouldComplete: Boolean) {
val pausedComposition = this.pausedComposition
if (pausedComposition != null) {
// 1. If the work must be completed now...
if (shouldComplete) {
// ...force the composition to finish by looping `resume`
// and always passing `false` to the `shouldPause` callback.
while (!pausedComposition.isComplete) {
pausedComposition.resume { false }
}
}
// 2. Apply the changes to the real UI tree.
pausedComposition.apply()
this.pausedComposition = null // Clear the handle.
}
}
当某个项目可见时,其组合不再是低优先级的后台任务,而是高优先级的同步任务。shouldComplete = true 参数确保所有剩余的组合工作立即完成,无需暂停。然后,apply() 被调用,完整的 UI 会立即显示在屏幕上。
它们如何协同工作:

结论
深入研究 Compose 运行时后,PausableComposition 的设计堪称性能工程的杰作。
- 它并非魔法,而是延迟:其核心理念是在紧急任务之前完成。通过在空闲时间合成项目,快速滚动时主线程所需的工作量会大大减少。
- 协作式和非阻塞式:shouldPause 回调是处理多任务的绝佳方式。它可以让长时间运行的合成任务优雅地让位于更紧急的当前帧渲染任务,从而直接防止卡顿。
- 通过批处理提高效率:RecordingApplier 通过将 UI 树中的许多小的独立更改分组为单个高效的更新,避免了这些更改带来的开销。
虽然 PausableComposition 是一个你可能永远不会直接使用的内部功能,但了解它的存在和运作方式,可以让你真正体会到 Jetpack Compose 如此高性能的明智决策。下次你轻松流畅地滚动浏览复杂的 LazyColumn 时,你就会体会到这巧妙且精心编排的"舞蹈"是如何在表面之下进行的。✅ 这种架构不仅解决了当前的性能挑战,还为 Compose 未来更先进的渲染策略铺平了道路。
希望你已经了解了这个新 API 在 Jetpack Compose 中的工作原理。
太棒了!希望你从中获得了一些宝贵的见解。如果你喜欢这篇文章,请分享 😉,因为......
"分享即关爱"
谢谢!😄
让我们一起回顾 X的最新动态,或者访问我的网站了解更多关于我的信息 😎。
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!