android 动画掉帧

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,动画在第一帧就到达终态。所以你看到的是"只有一帧",不是帧率变低。
  • 因此"动画卡/一帧一帧"要分两种情况:
    1. 没有动画:scale=0,动画被跳过;
    2. 真掉帧 :scale=1 但每帧耗时超过预算(用 dumpsys gfxinfo 的 percentile 判断)。

4.2 刷新率是怎么被选的

Android 的 DisplayModeDirector 会综合三路输入来选当前 display mode:

  1. 系统设置 :min_refresh_rate(下限,0.0 表示不限制)、peak_refresh_rate(上限,null 表示用机型默认)。
  2. App 请求 :Window.setFrameRate() / Surface.setFrameRate(),会体现在 dumpsys display 的 frameRateOverride {uid=... frameRateHz=...}。
  3. 显示模式能力:这台 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. 注意事项

  1. scale=0 不是性能优化。它只是把动画时长变成 0,跳过插值帧;不会提升真实 FPS,反而会让动画观感变成"瞬间跳变/只有一帧"。
  2. 三个倍率是 Settings.Global,属于全局设置,会被自动化测试脚本、e2e 框架、手工调试改掉,排查前先读一遍。
  3. 修改后开发者选项界面可能不实时刷新,退出重进即可。
  4. peak_refresh_rate=null 在本机等于 60Hz;想稳定 120Hz 需要显式设 120,并确认前台 app 没有 frameRateOverride。
  5. 页面级的 60fps 是 app 侧请求(frameRateOverride)。如果希望该页面跑 120Hz,需要改 app 代码/配置,系统设置无法覆盖。
  6. 判断"掉帧"优先看 gfxinfo 的 90th/99th percentile,不要只凭肉眼;也要区分进页面瞬间的动画峰值和稳定态。
相关推荐
在多学一点1 小时前
MYSQL第二次作业
android·数据库·mysql
骑着蜗牛撵大象3272 小时前
MySQL 8.0 迁移后 SQL 变慢:执行计划里 3 类被忽略的成本差异
android·数据库·sql·mysql·优化器·执行计划
我叫张土豆2 小时前
Android 14+ 模拟器抓包实录:把 Burp CA 装成系统 CA,让 targetSdk≥34 的 App 乖乖解密
android
booboosui6 小时前
Android 悬浮窗藏不住自己:一个标志位与 140 毫秒
android
传奇开心果6 小时前
【Flutter踩坑排雷】第5课:扩展方法冲突坑|同名扩展引发编译歧义,故障规避手段
android·flutter
.痞老板.8 小时前
mysql初阶--数据类型
android·数据库·mysql
StarsGreen8 小时前
Android 启动流程详解
android·linux·单片机·嵌入式硬件
booboosui1 天前
Android 上来电怎么判:7 个条件加 5 个中文词
android
MardaWang1 天前
App 内嵌 H5 的信任边界设计(ArkWeb/HarmonyOS 实战)
android