1. 现象
- 系统/应用动画看起来"一帧一帧的",感觉 FPS 很低。
- 进入带图表/动画的详情页后更明显。
2. 观察(实测数据)
2.1 动画倍速:三项全为 0
bash
adb shell settings get global window_animation_scale # 0
adb shell settings get global transition_animation_scale # 0
adb shell settings get global animator_duration_scale # 0
0 的含义是 动画时长为 0,不是"性能模式"。表现为所有动画在首帧直接到终态,肉眼看就是"只有一帧"。
2.2 刷新率:设备默认 60Hz
bash
adb shell settings get system peak_refresh_rate # null
adb shell settings get system min_refresh_rate # 0.0
adb shell dumpsys display | grep mDisplayModeSpecs
# mDisplayModeSpecs={baseModeId=1 ... primary=physical: (0.0 60.0) ...}
Pixel 8 硬件支持 120Hz,但这台机器 peak_refresh_rate=null 时实际落在 60Hz (baseModeId=1)。
2.3 前台应用自己请求 60fps
bash
adb shell dumpsys display | grep frameRateOverride
# frameRateOverride {uid=10522 frameRateHz=60.0}
前台应用会通过 Window.setFrameRate() / Surface.setFrameRate() 请求帧率。即使系统峰值开到 120Hz,只要该应用在前台,屏幕就会被 app 的帧率请求压回 60Hz;回到桌面/设置页则恢复 120Hz。
2.4 gfxinfo:区分"没动画"和"真掉帧"
bash
adb shell dumpsys gfxinfo <package>
| 采样窗口 | 帧数 | Janky | 50th | 90th | 99th |
|---|---|---|---|---|---|
| 进页面/动画阶段 | 701 | 79(11.27%) | 9ms | 18ms | 117ms |
| 稳定 15s | 907 | 1(0.11%) | 9ms | 11ms | 14ms |
| 连续滑动 6 次 | 398 | 0(0.00%) | 8ms | 10ms | 14ms |
60Hz 的每帧预算是 16.7ms。稳定阶段 99th=14ms 说明页面本身是流畅的;进页面阶段的 99th=117ms 才是真正的掉帧(首帧/动画集中触发)。
3. 尝试设置 + 效果
| 设置项 | 修改前 | 尝试值 | 实测效果 |
|---|---|---|---|
window_animation_scale |
0 | 1 | 窗口/Activity 切换动画恢复播放;设为 0 时直接跳到终点 |
transition_animation_scale |
0 | 1 | Activity 转场动画恢复;设为 0 时无转场 |
animator_duration_scale |
0 | 1 | ValueAnimator 类动画恢复时长;设为 0 时首帧即终态("只有一帧") |
peak_refresh_rate |
null(60Hz) |
120 | 桌面/设置页从 60Hz 变 120Hz;但前台有帧率请求的应用仍是 60Hz |
peak_refresh_rate |
120 | 删除(null) |
回到 baseModeId=1,即 60Hz,与原状态一致 |
min_refresh_rate |
0.0 | 未改动 | 全程保持 0.0 |
关键结论:
- 三个倍率设回
1后,系统动画会正常播放;但它们不会提高帧率。 - 把
peak_refresh_rate设到 120 只对"没有 app 帧率覆盖"的页面有效(桌面/设置页);有帧率请求的页面仍然 60Hz。 - 页面级的 60fps 是 app 层行为,不是系统开关。
4. 原理
4.1 三个动画倍速分别管什么
| 设置 | 作用范围 |
|---|---|
window_animation_scale |
窗口/Activity 打开、关闭、切换时的窗口动画时长倍率 |
transition_animation_scale |
Activity 之间、任务切换的转场动画时长倍率 |
animator_duration_scale |
ValueAnimator / ObjectAnimator / ViewPropertyAnimator 等属性动画的时长倍率(含绝大多数自定义动画、RecyclerView 的部分动画、图表动画) |
读取链路(简化):
Settings.Global.*_animation_scale
│
├─ WindowManagerService.getCurrentAnimatorScale()
│ └─ ValueAnimator.setDurationScale(scale)
│
└─ 各动画在 start 时用 duration × durationScale
scale = 1:按代码里写的 duration 播放,有完整的插值帧。scale = 0:duration × 0 = 0,动画在第一帧就到达终态。所以你看到的是"只有一帧",不是帧率变低。- 因此"动画卡/一帧一帧"要分两种情况:
- 没有动画:scale=0,动画被跳过;
- 真掉帧 :scale=1 但每帧耗时超过预算(用
dumpsys gfxinfo的 percentile 判断)。
4.2 刷新率是怎么被选的
Android 的 DisplayModeDirector 会综合三路输入来选当前 display mode:
- 系统设置 :
min_refresh_rate(下限,0.0表示不限制)、peak_refresh_rate(上限,null表示用机型默认)。 - App 请求 :
Window.setFrameRate()/Surface.setFrameRate(),会体现在dumpsys display的frameRateOverride {uid=... frameRateHz=...}。 - 显示模式能力:这台 Pixel 8 可选 60 / 120Hz。
本机的实际行为:
peak_refresh_rate=null→ 机型默认落到 60Hz;- 设置
peak_refresh_rate=120→ 桌面/设置页 120Hz; - 但前台应用请求
60fps→frameRateOverride生效 → 屏幕仍回落到 60Hz。
所以"开了 120Hz 还是觉得卡"时,要先看 frameRateOverride 是不是被前台 app 压住了。
4.3 三层因素是独立的
动画观感
├─ 动画时长倍率(scale):决定"有没有动画 / 动画多长"
├─ 帧率上限(refresh rate):决定每秒最多画几帧(60/120)
└─ 单帧耗时(app 渲染):决定能不能在预算内画完(16.7ms@60 / 8.3ms@120)
- 只调 scale:解决"动画被跳过/瞬间完成",不解决掉帧。
- 只调刷新率:提高上限,但 app 请求 60fps 或单帧超时仍会卡。
- 真正掉帧要看 gfxinfo 的 90th/99th percentile:超过 16.7ms(60Hz)或 8.3ms(120Hz)就是掉帧。
5. 常用命令
bash
D=39271FDJH00152
# 读取
adb -s $D shell settings get global window_animation_scale
adb -s $D shell settings get global transition_animation_scale
adb -s $D shell settings get global animator_duration_scale
# 恢复动画(1 = 正常)
adb -s $D shell settings put global window_animation_scale 1
adb -s $D shell settings put global transition_animation_scale 1
adb -s $D shell settings put global animator_duration_scale 1
# 关闭动画(0 = 跳过)
adb -s $D shell settings put global window_animation_scale 0
adb -s $D shell settings put global transition_animation_scale 0
adb -s $D shell settings put global animator_duration_scale 0
# 刷新率
adb -s $D shell settings get system peak_refresh_rate
adb -s $D shell settings put system peak_refresh_rate 120 # 开到 120
adb -s $D shell settings delete system peak_refresh_rate # 恢复机型默认(本机=60Hz)
adb -s $D shell settings get system min_refresh_rate
# 查看当前帧率上限 / app 覆盖
adb -s $D shell dumpsys display | grep -E "mDisplayModeSpecs=|frameRateOverride|mActiveSfDisplayMode"
# 查看真实掉帧
adb -s $D shell dumpsys gfxinfo <package> reset
adb -s $D shell dumpsys gfxinfo <package> | grep -E "Total frames|Janky|percentile"
6. 注意事项
scale=0不是性能优化。它只是把动画时长变成 0,跳过插值帧;不会提升真实 FPS,反而会让动画观感变成"瞬间跳变/只有一帧"。- 三个倍率是
Settings.Global,属于全局设置,会被自动化测试脚本、e2e 框架、手工调试改掉,排查前先读一遍。 - 修改后开发者选项界面可能不实时刷新,退出重进即可。
peak_refresh_rate=null在本机等于 60Hz;想稳定 120Hz 需要显式设 120,并确认前台 app 没有frameRateOverride。- 页面级的 60fps 是 app 侧请求(
frameRateOverride)。如果希望该页面跑 120Hz,需要改 app 代码/配置,系统设置无法覆盖。 - 判断"掉帧"优先看
gfxinfo的 90th/99th percentile,不要只凭肉眼;也要区分进页面瞬间的动画峰值和稳定态。