第 22 章

22.13自适应刷新率与帧率策略实战

适用 Android 15 (API 35) - Android 17 (API 37) · 内容验证 2026-08-24

应用能表达内容期望帧率,但最终刷新节奏仍由窗口、Surface、系统策略和显示能力共同决定。接入时要让请求贴近实际更新源,并通过 FrameTimeline 与显示模式验证,而不是只检查 API 调用成功。

从三个帧率开始判断

自适应刷新率(Adaptive Refresh Rate,ARR)调整的是显示节奏,不会缩短应用生成一帧时的 CPU、GPU 或解码耗时。排查前要分清三个量:

  • 内容帧率:视频、游戏或动画每秒产生多少个不同画面;
  • 应用渲染速率:应用收到帧回调并提交新图形缓冲区的节奏;
  • 显示刷新率:显示设备每秒更新画面的次数。

fps 表示每秒生成的画面数,Hz 表示显示设备每秒刷新的次数。60 fps 的应用可以运行在 120 Hz 显示设备上,每个应用帧保持两个刷新周期,画面仍然均匀。应用平均达到 60 fps,也可能因为提交间隔忽快忽慢而出现卡顿。帧率平均值、呈现间隔和输入延迟回答的是不同问题。

本文的平台实现基线是 Android 17、API 37 和 android-17.0.0_r1,内核源码基线是 android17-6.18-2026-06_r6。ARR 从 Android 15 进入平台;应用要使用完整能力,系统需为 Android 15 QPR1 或更高版本,设备还要实现相应的 Composer3 硬件抽象层(Hardware Abstraction Layer,HAL)能力。系统版本符合要求,并不表示每块显示设备都支持 ARR。

在 ARR 配置中,显示设备的垂直同步(VSync)或面板 TE(Tearing Effect,面板扫描时序信号)可以与内容刷新节奏分开。面板能在同一个显示模式内,按 TE 周期的离散倍数选择呈现时机。这样可以减少只为改变刷新率而切换显示模式的次数,也能缩短静态内容和低频动画占用高刷新率的时间。平台选择链、ARR/MRR 分支和 attached Choreographer 反馈统一见 2.2 帧率、刷新率与显示模式选择;本文只负责应用接入、跨设备实验和回退策略。

应用仍有两项责任:控制生成一帧的成本和节奏,并向系统准确描述内容偏好。ARR 不会修复主线程超时、RenderThread 堵塞、GPU 迟完成、视频时间戳错误或 BufferQueue 中待显示缓冲区堆积。

Android 15—17 API 边界

工程代码应同时检查 API 可用性和目标显示设备的能力。Android 15 可以提交 ViewWindowSurface 的帧率请求,公开的 ARR 能力查询则从 Android 16 才可用。

能力 API 版本 用途与限制
Surface 内容帧率 Surface.setFrameRate() API 30;三参数版本 API 31 提交 Surface 级偏好;不会替画面生产方限速
清除 Surface 请求 Surface.clearFrameRate() API 34 内容结束或目标改变时撤销旧请求
View 帧率请求 View.setRequestedFrameRate(float) API 35 可传具体值或 View 帧率类别;View 需要重绘时才参与
滚动速度提示 View.setFrameContentVelocity(float) API 35 平滑滚动或惯性滚动期间,每个绘制帧上报像素/秒
Window 省电平衡 Window.setFrameRatePowerSavingsBalanced(boolean) API 35 控制窗口是否采用 ARR 省电平衡;默认开启
Window 触摸升帧 Window.setFrameRateBoostOnTouchEnabled(boolean) API 35 控制该窗口的触摸升帧;默认开启
ARR 能力查询 Display.hasArrSupport() API 36 查询当前显示设备是否公开支持 ARR
类别建议值 Display.getSuggestedFrameRate(int) API 36 只接受 NORMALHIGH 两个显示帧率类别
UI Surface 下限语义 Surface.FRAME_RATE_COMPATIBILITY_AT_LEAST API 36 用于 UI、动画、滚动和惯性滚动;视频、游戏有各自语义
支持的渲染速率 Display.getSupportedRefreshRates() API 21,API 36 改变返回语义 API 36 及以上返回显示设备支持的渲染速率;旧版本只覆盖默认显示模式的刷新率
速度—帧率映射 Display.getFrameRateVelocityMapping() API 37 返回当前显示设备的滚动速度映射;换屏或显示设备变化后要重新读取

getSuggestedFrameRate() 返回设备为指定类别配置的建议值。它不是当前刷新率,系统也不保证采用这个值。getSupportedRefreshRates() 从 API 21 就已存在,只在 API 36 更新了返回语义,不能把它写成 Android 16 新增的方法。

下面的封装只读取能力,不替业务选择固定的 60 Hz 或 120 Hz。

kotlin
sealed interface ArrCapability {
    data object QueryUnavailable : ArrCapability
    data object Unsupported : ArrCapability

    data class Supported(
        val normalFrameRate: Float,
        val highFrameRate: Float,
        val supportedRenderRates: List<Float>,
    ) : ArrCapability
}

fun Display.readArrCapability(): ArrCapability {
    if (Build.VERSION.SDK_INT < 36) {
        return ArrCapability.QueryUnavailable
    }
    if (!hasArrSupport()) {
        return ArrCapability.Unsupported
    }
    return ArrCapability.Supported(
        normalFrameRate =
            getSuggestedFrameRate(Display.FRAME_RATE_CATEGORY_NORMAL),
        highFrameRate =
            getSuggestedFrameRate(Display.FRAME_RATE_CATEGORY_HIGH),
        supportedRenderRates = supportedRefreshRates.sorted(),
    )
}

QueryUnavailable 表示 API 35 没有公开查询入口,不表示设备一定缺少 ARR。应用仍可提交公开的 ViewSurface 帧率请求,由系统按设备能力处理。折叠屏内外屏、外接屏和窗口迁移都可能改变关联的 Display 对象;缓存结果时应带上 displayId,并在收到显示设备变化回调后重新读取。

Android 17 的 getFrameRateVelocityMapping() 主要服务滚动组件。setFrameContentVelocity() 接收像素/秒,返回映射中的 FrameRateVelocityPoint.dpPerSecond 则使用密度无关像素/秒。普通自定义 View 只需按接口要求上报像素速度,由框架结合当前显示设备选择偏好;应用不应复制一套固定速度阈值,也不能直接比较这两种单位的数值。

View 帧率请求要贴近会更新的内容

官方文档把这套汇总机制称为“投票”(voting):每个可见且需要重绘的 View 提交帧率偏好,系统再汇总成宿主窗口的提示。“投票”只是请求合并的比喻,不表示每个 View 有固定票数,也不表示结果一定采用最高请求。可用类别包括:

  • REQUESTED_FRAME_RATE_CATEGORY_DEFAULT:恢复框架默认行为;
  • REQUESTED_FRAME_RATE_CATEGORY_NO_PREFERENCE:该 View 明确不影响结果;
  • REQUESTED_FRAME_RATE_CATEGORY_LOW:适合低速、小面积且高平滑度收益有限的动画;
  • REQUESTED_FRAME_RATE_CATEGORY_NORMAL:适合不依赖高刷的动画,设备通常把它映射到 60 Hz 附近;
  • REQUESTED_FRAME_RATE_CATEGORY_HIGH:适合高平滑度收益明确的动画,功耗也可能上升。

同一帧中,View 层通常采用较高的有效请求。进入 SurfaceFlinger 后,系统还会综合其他应用的图层(layer,即 SurfaceFlinger 的合成单元)、系统界面、触摸状态、空闲状态、功耗策略、可用显示模式和能否无缝切换。这里不存在“某个 View 请求 120,屏幕就锁定 120 Hz”的保证。

ViewGroup.setRequestedFrameRate() 在 API 35 不会自动传给子 View。API 36 增加的接口是 ViewGroup.propagateRequestedFrameRate(frameRate, forceOverride):调用方要明确提供帧率值;forceOverride=false 保留子 View 已明确设置的值,true 则允许覆盖;恢复时传入 DEFAULT 类别。给页面根节点长期请求 HIGH 会扩大高刷新率范围,进度条、视频卡片、列表与静态正文也更难按各自节奏工作。

场景策略可以按下面的边界制定:

场景 合适的表达 验收重点
小型进度条、低频波形 局部 View 请求 NORMAL 或经设备验证的具体值 动画节奏稳定,高刷新率驻留时间下降
点击反馈、短位移动画 保留触摸升帧;卡顿时对局部 View 评估 HIGH ACTION_DOWN 到首个可见反馈
静态阅读、表单 不触发无意义的无效化与重绘,不主动请求高值 静止后不再持续生成应用帧
RecyclerView 惯性滚动 RecyclerView 1.4.0 及以上;配套 AndroidX Core 1.15.0 及以上 松手后随速度降低渲染速率
NestedScrollView AndroidX Core 1.15.0 及以上 速度下降段的节拍与滚动位移
自定义滚动容器 惯性滚动或平滑滚动的每个绘制帧上报速度 速度值、帧回调与滚动位移同步
视频卡片 视频 Surface 表达源帧率,宿主控件单独提交请求 视频帧节奏与宿主界面分开观察

下面的例子从 OverScroller 读取当前惯性滚动速度,并在每个绘制帧更新提示。

kotlin
class ArrAwareScrollView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null,
) : View(context, attrs) {
    private val scroller = OverScroller(context)

    override fun computeScroll() {
        if (!scroller.computeScrollOffset()) {
            return
        }

        if (Build.VERSION.SDK_INT >= 35) {
            frameContentVelocity = scroller.currVelocity
        }
        scrollTo(scroller.currX, scroller.currY)
        postInvalidateOnAnimation()
    }
}

currVelocity 已是非负的像素/秒。setFrameContentVelocity() 的值只对下一个绘制帧有效,所以一次手势只写一次无法覆盖完整的惯性滚动。触摸按住期间仍由触摸升帧保持较高节奏;速度映射用于松手后的减速阶段。

ARR 只改变目标节拍。若滚动仍出现“应用错过帧截止时间”(app deadline missed),应继续检查布局、数据绑定、图片处理、预取、RenderThread 和 GPU。相关 RecyclerView 方法见 22.2 RecyclerView 与 Compose LazyList 性能

Window 开关属于回退手段

触摸升帧(touch boost)默认开启。收到 ACTION_DOWN 后以及手指抬起的一段时间内,系统可以提高渲染速率,用于按压态、拖动起步和窗口内的交互反馈。官方文档明确不建议关闭它。

窗口级 API 可以直接调用,无需手动修改 WindowManager.LayoutParams 再写回 attributes。下面的函数适合实验开关和少量经验证的窗口策略。

kotlin
fun Window.configureFrameRatePolicy(
    touchBoostEnabled: Boolean,
    powerSavingsBalanced: Boolean,
) {
    if (Build.VERSION.SDK_INT < 35) {
        return
    }
    setFrameRateBoostOnTouchEnabled(touchBoostEnabled)
    setFrameRatePowerSavingsBalanced(powerSavingsBalanced)
}

powerSavingsBalanced=false 会关闭该窗口的 ARR 省电平衡,通常会增加耗电;它不能指定一个固定刷新率。touchBoostEnabled=false 也不适合作为全应用默认值。两项都应有独立的远端开关,避免一次回退影响另一项策略。

关闭触摸升帧的实验至少测量:

  • ACTION_DOWN 到按压态首帧呈现的延迟;
  • 松手后 200 ms 内的惯性滚动位移、错过截止时间次数和帧间隔;
  • 连续拖动时输入事件、View 树遍历(traversal)、SurfaceFrameDisplayFrame 的间隔;
  • 普通、低电量、热限制和多窗口条件下的差异。

SurfaceView、TextureView 与普通 View 的请求路径

这三类对象的显示拓扑不同,不能都视为“独立内容层”:

承载方式 Android 17 路径 请求与观测对象
普通 View View/HWUI 绘入宿主应用窗口的图形缓冲区(buffer) View 请求汇总到宿主图层
TextureView 外部画面生产方写入 SurfaceTexture;HWUI 读取纹理并采样进宿主缓冲区 生产方设置的帧率可以传回 TextureView 偏好,SurfaceFlinger 通常仍只看到宿主图层
SurfaceView 画面生产方写入独立的 BufferQueue;BLAST 子层作为独立缓冲区图层进入 SurfaceFlinger 内容 Surface 与宿主窗口可拥有不同帧节奏、缓冲区和同步栅栏(fence)

在 Android 17 的 AOSP 实现中,只有 Flags.toolkitSetFrameRateReadOnly() 特性开关启用时,TextureView 才会在 SurfaceTexture 上安装帧率监听器。画面生产方对其 Surface 调用 setFrameRate() 后,监听器会更新 TextureView 请求的帧率;内容仍要经过 TextureLayer.updateSurfaceTexture() 和宿主 HWUI。这个回传过程不会把 TextureView 变成独立的 SurfaceFlinger 视频图层。

Android 17 的 SurfaceView 会创建容器层、BLAST 内容子层,以及按需创建的背景色层。视频或相机画面生产方可以按自己的节奏更新 BLAST 子层,宿主 View 树只负责普通界面、位置、裁剪、可见性与相对层级。使用 Perfetto 分析时,要分别观察宿主窗口和 SurfaceView 内容层。

固定帧率视频适合在承载视频的 Surface 上提交 FIXED_SOURCE。下面的调用表达 24 fps 固定源,并把显示模式变化限制为无缝切换。

kotlin
fun Surface.setVideoFrameRate(frameRate: Float) {
    if (Build.VERSION.SDK_INT < 30) {
        return
    }
    if (Build.VERSION.SDK_INT >= 31) {
        setFrameRate(
            frameRate,
            Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
            Surface.CHANGE_FRAME_RATE_ONLY_IF_SEAMLESS,
        )
    } else {
        setFrameRate(
            frameRate,
            Surface.FRAME_RATE_COMPATIBILITY_FIXED_SOURCE,
        )
    }
}

只有在长时间播放且产品接受一次可见切换时,才评估 CHANGE_FRAME_RATE_ALWAYS。播放结束、Surface 复用或内容帧率改变时要更新请求;API 34 及以上可用 clearFrameRate() 清除旧值。调用成功只表示提示已提交,不表示显示设备已选择 24 Hz 或 120 Hz。

API 36 及以上的普通界面、动画和滚动 Surface 可以使用 FRAME_RATE_COMPATIBILITY_AT_LEAST;游戏继续使用 DEFAULT,并由引擎控制产帧节奏;视频继续使用 FIXED_SOURCE。相机预览要按捕获帧率、宿主界面和产品延迟目标单独评估,不能因为输出到 Surface 就套用视频语义。

Android 17 怎样处理这些帧率请求

View 层先汇总当前绘制帧的类别或具体值。Surface 级请求和帧提交历史进入图层状态。Android 17 的 SurfaceFlinger 再按以下路径处理:

  1. LayerHistory::summarize() 汇总可见图层的需求,包含请求类型、期望值、图层面积占显示面积的权重、是否获得焦点,以及目标显示设备;
  2. Scheduler::chooseRefreshRateForContent() 收集内容需求和全局信号;
  3. RefreshRateSelector::getRankedFrameRates() 过滤候选并评分;
  4. 显示策略、无缝切换条件、触摸状态、空闲状态、功耗状态和其他图层共同约束结果;
  5. 选出的渲染速率或显示模式改变应用与 SurfaceFlinger 的调度节拍,并向硬件合成器(Hardware Composer,HWC)提交显示配置或 ARR 呈现间隔。

Android 17 的 RefreshRateSelector 用一组内部类型区分请求语义:ExplicitDefault 表示默认值,ExplicitExactOrMultiple 表示精确值或其整数倍,ExplicitExact 表示精确值,ExplicitGte 表示不低于给定值,ExplicitCategory 表示类别请求,Heuristic 表示系统根据历史推断。FRAME_RATE_COMPATIBILITY_AT_LEAST 对应最低值请求,与固定源的精确值或整数倍诉求采用不同评分。触摸状态和空闲状态也有提前结束选择及延后升帧分支,不能概括成“触摸必定拉满,固定若干秒后降频”。

Composer3 的设备能力位于另一层。DisplayConfiguration.vrrConfig 非空时,该显示配置才属于 ARR;DisplayConfiguration.vsyncPeriod 表示 VSync/TE 周期,VrrConfig.minFrameIntervalNs 约束两帧之间允许的最短间隔。设备若提供 DisplayConfiguration.vrrConfig.notifyExpectedPresentConfig,Android 框架可以通过 IComposerClient.notifyExpectedPresent() 提前告知预计呈现时间和后续的 frameIntervalNs 帧间隔。厂商仍可按面板与功耗策略决定具体行为。

通用 Android 内核 android17-6.18-2026-06_r6drivers/gpu/drm/drm_vblank.c 提供 DRM(Direct Rendering Manager,Linux 显示子系统)的垂直消隐(VBlank)计数、事件和时间戳机制。Android 设备不保证采用相同的 DRM 显示路径,面板 TE、最低刷新率、硬件显示平面、带宽与厂商 HWC 策略都不能只从通用内核推出。定位到 HWC 后,还要结合目标设备的 Composer、显示驱动和厂商跟踪数据。

Perfetto 验收:把请求、生产和呈现放在一起

ARR 验收至少同时覆盖四条证据:

证据层 观察内容 能回答的问题
应用输入与工作 输入事件、Choreographer、界面线程、RenderThread、GPU 交互是否变慢,应用是否在新的帧截止时间前完成
应用 SurfaceFrame 预期/实际时间线、on_time_finishjank_type 宿主应用帧是否迟交或迟完成
SurfaceFlinger DisplayFrame 预期/实际时间线、呈现类型、SurfaceFlinger 卡顿标记 合成与显示提交是否按目标时间完成
刷新率与图层 当前显示模式、渲染速率、图层请求、选择过程的区间事件、实际呈现节奏 何时改变节拍,哪个显示对象参与决策

应用 SurfaceFrame 与 SurfaceFlinger DisplayFrame 是两种对象:前者描述应用窗口帧,后者描述合成后送往显示设备的帧。预期时间线与实际时间线的关联用于比较目标时间和完成结果,不能把两条区间事件(slice,即有开始和结束时间的跟踪记录)在界面上的“分叉”直接判成某类卡顿。SurfaceView、视频、相机和游戏的独立画面生产方,也未必拥有与标准 HWUI 应用窗口同样完整的 FrameTimeline 信息。信息缺失时,应依次查看对应图层的 queueBuffer(缓冲区入队)、BufferTX(图层事务)、acquire fence(缓冲区就绪同步)、latch(SurfaceFlinger 接收缓冲区)、HWC 合成和 present(送显)。

View 请求不等于独立图层请求。普通 ViewTextureView 的结果通常要在宿主应用窗口上验证;SurfaceView 内容则要找到它自己的 BLAST 缓冲区图层。Perfetto 的计数器(counter)是随时间记录数值的轨道;只看到刷新率计数器下降,无法证明是哪项应用策略触发,也无法证明交互没有受损。

系统刷新率设置属于测试环境变量,且键名和生效方式会受 OEM 影响。需要强制档位做实验时,应保存原值并保证脚本退出后恢复。下面的脚本要求调用者传入目标设备已支持的 peak 值。

bash
#!/usr/bin/env bash
set -euo pipefail

TEST_PEAK="${1:?pass a supported peak refresh rate, for example 120.0}"
TEST_PACKAGE="${2:-com.example.app}"
PEAK_BEFORE="$(adb shell settings get system peak_refresh_rate | tr -d '\r')"
MIN_BEFORE="$(adb shell settings get system min_refresh_rate | tr -d '\r')"

restore_setting() {
    local key="$1"
    local value="$2"
    if [[ -z "$value" || "$value" == "null" ]]; then
        adb shell settings delete system "$key" >/dev/null
    else
        adb shell settings put system "$key" "$value"
    fi
}

restore_all() {
    restore_setting peak_refresh_rate "$PEAK_BEFORE"
    restore_setting min_refresh_rate "$MIN_BEFORE"
}

trap restore_all EXIT
trap 'exit 130' INT TERM

adb shell settings put system peak_refresh_rate "$TEST_PEAK"
adb shell settings put system min_refresh_rate 0.0
adb shell am force-stop "$TEST_PACKAGE"
adb shell monkey -p "$TEST_PACKAGE" 1

read -r -p "Record the Perfetto trace, then press Enter to restore settings."

脚本只用于实验机。运行前应从设备支持的显示模式或系统设置中确认目标值,并在跟踪数据中验证设置是否生效。日常基线还要保留一组完全不改系统设置的数据,避免把调试时的强制设置当成量产策略。

应用线上可以记录 displayId、API 版本、hasArrSupport()、应用提交的策略、窗口模式、FrameMetrics/卡顿指标和交互时延。普通应用拿不到通用、可信的面板刷新率驻留统计;请求值或 Display.getRefreshRate() 也不能代替面板功耗证据。刷新率驻留时间、CPU/GPU 工作频点和耗电应通过实验室跟踪数据、功耗仪或设备提供的可靠计数器验证。

跨设备实验与回退

实验分桶至少包含:

  • API 35:可以提交 ARR 相关请求,无法用公开 API 查询 ARR;
  • API 36—37 且 hasArrSupport=false:保留安全的帧率请求,排除在 ARR 收益统计之外;
  • API 36—37 且 hasArrSupport=true:按 displayId、设备型号、系统构建指纹(build fingerprint)、窗口模式和电源状态分组;
  • 折叠屏、外接屏、多窗口:分别记录目标显示设备,不能沿用主屏能力;
  • 设备厂商的高刷新率、游戏、低电量或热策略介入:单独标记,避免与默认策略混算。

回退开关应拆成 View 类别/具体值、滚动速度提示、触摸升帧、Window 省电平衡和 Surface 请求五类。出现机型问题时只撤销对应提示。系统默认 ARR 本身仍可继续工作。

功耗 A/B 需要固定亮度、网络、温度起点、电量区间、账号数据、页面停留、输入脚本和刷新率系统设置。每个场景至少比较:

  • 系统默认 ARR,应用不增加额外请求;
  • 系统默认 ARR,低频动态内容提交局部请求;
  • 同一策略下关闭指定窗口的触摸升帧,仅作为专项实验;
  • 关闭 Window 省电平衡,仅作为问题复现和回退对照。

通过标准要同时约束高刷新率驻留时间、慢帧/冻帧(渲染明显超时的帧)、输入到呈现延迟的分位数,以及单位场景能耗。只降低刷新率不能作为上线结论。

游戏、视频与 Compose

视频有固有的帧节奏,24/30/60 fps 内容应使用 FIXED_SOURCE,并保持播放器时间戳、解码输出和 Surface 提交节奏一致。24 fps 内容在 120 Hz 显示设备上可以形成 5:5 节奏,即每个视频画面连续显示五个刷新周期;系统也可能因为其他可见图层、显示策略或无缝切换限制选择别的结果。

游戏的帧率目标通常可以跟随设备和负载变化,使用 DEFAULT,并由引擎或 AGDK Frame Pacing(帧节奏库)控制逻辑节拍、预计呈现时间,以及在途帧深度(已经提交但尚未呈现的帧数)。Surface 帧率请求不会替游戏限制产帧速度,也不会修复缓冲区队列塞入过多帧的 queue stuffing。非 View 渲染还要结合引擎帧时间、Perfetto 和对应游戏质量指标。

Compose 从 1.9 起提供 Modifier.preferredFrameRate(Float)Modifier.preferredFrameRate(FrameRateCategory),本文核验基线为 Compose UI 1.12.0。帧率请求应靠近持续变化的可组合函数。给整屏统一设置 High 会扩大高刷新率范围;LazyList 应优先使用当前 Compose/AndroidX 自带的滚动支持,再用跟踪数据判断是否还需局部调整。ARR 也不会减少重组(recomposition)、布局(layout)或绘制(draw)本身的工作量。

全文小结

ARR 策略从内容对象出发:普通 View 和 Compose 在宿主窗口内提交请求,TextureView 的外部内容回到宿主 HWUI,SurfaceView、视频和游戏 Surface 可以形成独立图层节奏。应用提交偏好,SurfaceFlinger 结合所有可见图层与系统策略做选择,Composer3 和面板能力决定可用的离散呈现节拍。

验收时要同时检查输入响应、应用 SurfaceFrame、SurfaceFlinger DisplayFrame、图层呈现节奏、刷新率选择和功耗。系统没有原样采用请求属于正常行为;应用帧迟到、画面生产方节奏错误或设备策略异常,则要沿各自证据继续定位。

Android 17 源码与资料

正文《Android 技术内幕》作者 高建武(Gracker),原仓库 Gracker/android-internals-wiki,以 CC BY-NC-SA 4.0 授权。本站是个人非商业阅读版改编,非官方站点。正文字体 霞鹜文楷(SIL OFL 1.1,授权全文)。