本文聚焦 HarmonyOS 上基于 LTPO 屏幕的自适应刷新率与可变帧率能力。文中代码为便于说明自行编写,API 名称、枚举取值与版本号等事实性信息均标注官方出处;涉及真机功耗/帧率表现的部分已明确标注,未编造任何实测数据。
引子:一行设置,省的是电不是帧
先抛个场景。你做了个首页小转盘加载动画,丝般顺滑——但测试同事说"这页面手机发烫"。你查了一圈,最后发现罪魁是这么一行:
sync.setExpectedFrameRateRange({expected:120,min:120,max:120});你以为"高帧率 = 体验好"。在 LTPO 屏上,这句等于告诉系统:"不管现在放什么,都给V哥 120 帧跑着。"系统是个老实孩子,你这么吩咐它就照办,于是手机一直在满帧空转,电哗哗掉,后盖温温热。改完这行之后,转盘照样转,功耗曲线却肉眼可见地往下走了。
动画又丝滑又省电,其实只差一行帧率设置——把帧率交给内容决定。
一、帧率不是越高越好:LTPO 把"刷新率"交还给了内容
LTPO(Low Temperature Polycrystalline Oxide,低温多晶氧化物)是 OLED 屏背板的一种驱动技术。官方一句话点明它的价值:
LTPO 屏支持 1~120Hz 的自适应刷新率,使应用在需要高刷新率的场景下提升流畅性,而在视频、静止等场景中使用低刷新率降低显示功耗。(基于 LTPO 的低功耗设计,更新于 2026-03-12)
翻成白话:看静止图文时,屏幕可以掉到 1Hz;快速滑动时拉到 120Hz。帧率跟着内容走,功耗和流畅就兼顾了。把 1Hz 和 120Hz 放在一起想就很直观——同一个屏幕,待机时几乎不刷,动起来才全速,这就是"自适应"的本意。
到了 HarmonyOS 7,系统在功耗层面进一步把 LTPO 可变帧率做成了一项系统级能力(虎嗅《HarmonyOS 7 正式发布》提到"在功耗上则引入 LTPO 可变帧率技术")。注意这里的重点:对开发者而言,接入这套能力用的还是既有的可变帧率接口,这些接口自API 12(5.0)就开放了。换句话说——本文讲的帧率控制能力不是 7.0 才新增的 API,而是 5.0 起就在的"可变帧率能力";7.0 把系统侧的 LTPO 适配做厚了,让"用对这套接口"更值钱。这也是为什么本文如实标注每个接口的起始版本,而不把它包装成 7.0 专属新特性。
二、三条入口:动画帧率 / UI 绘制帧率 / 自绘制帧率
官方把可变帧率能力归成三类适用场景(可变帧率简介):
- 动画绘制帧率:属性动画 / 显式动画,用
expectedFrameRateRange参数; - UI 绘制帧率:用
displaySync申请一个独立绘制帧率; - 自绘制内容帧率:XComponent / NativeVsync 在 Native 侧申请独立帧率(游戏等)。
三条线对应三种内容形态,别混用:普通 ArkUI 组件上的动画走第一条;你想自己控制某块 UI 的刷新节奏走第二条;游戏或 Canvas 自绘制走第三条。V哥把四类内容映射到帧率档位,做成一张自用表:
| 内容类型 | 官方场景举例 | 推荐 expected/min/max | V哥的建议 |
|---|---|---|---|
| 高动态 | 应用启停、窗口转场、拖拽窗口 | 90~120Hz | expected 120、min 90 兜底,纯全屏非持续动效才用 |
| 稳定滚动 | 视频弹幕、小说翻页、消息滑动 | 60Hz | expected 60、min 0,闲时让系统自由降频 |
| 微动效 | 转盘、加载转圈、滚动条消失 | 15~30Hz | expected 30、max 60,小区域别浪费算力 |
| 跟随源 | 插画动效、表情包、导航主界面 | 跟随内容源 | expected 0,交给系统/内容源决定 |
这张表的"V哥的建议"列是V哥踩坑后的取舍:很多团队一上来全用 120,结果微动效也满帧跑。分档的核心就一句——帧率该由内容决定,交给系统调度。
三、手把手:displaySync.create() + on(‘frame’) 自绘制帧率
displaySync来自@kit.ArkGraphics2D,是"自绘制内容"那条线的主力。流程就三步:建实例 → 设期望帧率范围 → 注册frame回调 →start()启动。
V哥封装了一个按内容类型分档的调度器,避免到处散落魔法数字,也把"切档复用实例"和"销毁回收"这两件容易忘的事一起管起来:
// LtpoFrameScheduler.ets —— 按内容类型分档的帧率调度封装import{displaySync}from'@kit.ArkGraphics2D';// 内容类型:高动态 / 稳定滚动 / 微动效 / 跟随源exportenumContentTier{HIGH='high',STEADY='steady',MICRO='micro',SOURCE='source'}// 每类内容的帧率三件套,语义来自 ExpectedFrameRateRangeconstRANGE:Record<ContentTier,displaySync.ExpectedFrameRateRange>={[ContentTier.HIGH]:{expected:120,min:90,max:120},// 全屏非持续动效[ContentTier.STEADY]:{expected:60,min:0,max:120},// 闲时自由降频[ContentTier.MICRO]:{expected:30,min:0,max:60},// 小区域微动效[ContentTier.SOURCE]:{expected:0,min:0,max:30},// 0 = 跟随应用帧率};exportclassLtpoFrameScheduler{privatesync:displaySync.DisplaySync|undefined=undefined;// 按内容类型开一档帧率,注册每帧回调start(tier:ContentTier,onFrame:(info:displaySync.IntervalInfo)=>void):void{if(this.sync===undefined){this.sync=displaySync.create();}// 把 expected/min/max 交出去,真实帧率由系统再决策this.sync.setExpectedFrameRateRange(RANGE[tier]);this.sync.on('frame',onFrame);this.sync.start();}// 切档:同一个实例改范围即可,不必重建switchTo(tier:ContentTier):void{this.sync?.setExpectedFrameRateRange(RANGE[tier]);}// 页面销毁时必须停掉,否则帧回调一直挂在 UI 主线程上stop():void{if(this.sync){this.sync.stop();this.sync=undefined;}}}displaySync.create()、on('frame')、setExpectedFrameRateRange均自API 12起提供(DisplaySync 文档)。on('frame')的回调跑在 UI 主线程,里面别塞耗时操作,否则会拖慢渲染。
用起来就是一行分档:
// 一个加载转圈,用 MICRO 档,30Hz 足够顺眼又不烧电constscheduler=newLtpoFrameScheduler();scheduler.start(ContentTier.MICRO,(info:displaySync.IntervalInfo)=>{this.angle=(this.angle+1)%360;// 每帧转一度});// 页面 aboutToDisappear 时:// scheduler.stop();四、ExpectedFrameRateRange 的 min / expected / max 怎么填
三个字段来自官方ExpectedFrameRateRange类型定义:
- expected(最优期望帧率):系统优先按它跑。这是整个结构里最关键的一个字段,填错了最影响观感。
- min(最小帧率):系统尽量不低于它;设 0 表示允许系统降到最低以省电。
- max(最大帧率):系统不会超过它;上限受屏幕硬件限制(60Hz 屏 max 封顶 60)。
官方对取值给过一句话提醒:
开发者设置的期望帧率值可能无法完全实现,因为会受到系统能力和屏幕刷新率的限制。(可变帧率简介)
所以填法的心法:expected 写"这个内容就该有的帧率",min/max 写"系统可以活动的余地"。具体落到四类内容:高动态 expected 120、min 90,留个下限防掉帧;稳定滚动 expected 60、min 0,停了就让系统闲时降频;微动效 expected 30、max 60,小区域没必要冲高;跟随源 expected 0,把决定权交还系统。
五、反模式:把 expected 锁死 120Hz
这是全文最核心的一句:别把 expected、min、max 全写成 120。
官方最佳实践用一整段"不建议锁定最高帧率运行"来警告这件事,V哥摘关键三点:
不建议将 ExpectedFrameRateRange 中的 expected、min、max 都设置为 120,这会干扰系统可变帧率机制,增加负载,影响整机性能和功耗。(基于 LTPO 的低功耗设计)
为什么坏?三点串起来:① expected 是系统优先满足的值,锁 120 系统就真按 120 跑;② 持续 120 帧功耗显著上升,长时间会过热;③ 系统被你钉死在 120,它自己的可变帧率能力等于失效了。
V哥的口诀送给你们:“expected 锁满格,手机烫成铁;留个活动位,系统才体贴。”锁死 120 不是"体验拉满",是把系统的聪明劲儿按住了。
六、用 Profiler 做功耗对比:只讲方法,不编数字
写到这,你肯定想问"到底能省多少电"。V哥必须说清楚:V哥没在真机跑过具体数值,本文不编掉帧率、不编功耗百分比。V哥能给你的,是官方给出的对比方法,你照着在真机验。
官方给了两步(基于 LTPO 的低功耗设计 · 功耗测试工具):
- 看实时刷新率:设置里搜"开发者" → 打开"显示刷新频率"开关,肉眼看屏幕帧率有没有随内容降下来;
- 量整机功耗:DevEco 的Profiler→ Realtime Monitor,选设备/app/进程,黄线斜率正表示耗电。官方建议让应用跑 30 秒、每 3 秒记一次,取第 6~21 秒的平稳段平均。
方法到位,结论留给你的真机。锁 120 和分档这两版,用上面两步一比,差异自己就出来了——这一步别省,用数据说话比拍脑袋强。
七、期望帧率 ≠ 实际帧率
最后泼盆冷水。你设了 expected 30,不代表屏幕一定 30。真实帧率受两件事卡着:
- 系统功耗/性能约束:系统觉得现在该降频省电,就会在 min~max 里挑个更合适的值;
- 屏幕硬件上限:60Hz 屏,你 max 写 120 也只到 60。
所以把帧率当"建议"而不是"命令"。设计交互时,别假设回调一定按你设的节奏来——动画的视觉连续性要靠插值保证,而不是靠死守某个帧率。比如转盘转一圈用角度插值,30Hz 和 60Hz 看着都顺,但功耗差一倍。
上线自检清单
- LTPO 是什么、为什么 1-120Hz 自适应能省电,团队对齐了吗?
- 有没有把
expected/min/max全写成 120 的反模式?有就改掉。 - 动画、UI 绘制、自绘制三条线,各自的帧率入口选对了吗(animateTo 参数 / displaySync / XComponent)?
- 内容分档表做了吗?高动态/稳定滚动/微动效/跟随源各填了合适的 expected 吗?
min有没有给系统留降频余地(闲时内容敢不敢设 0)?displaySync在aboutToDisappear里stop()并置空了吗?避免回调泄漏。- 切档用
switchTo复用同一实例,还是每次都create新的? - 真机上用"显示刷新频率"开关验证过帧率随内容变化了吗?
- 用 Profiler Realtime Monitor 对比过锁 120 与分档的功耗差异吗?
- 动画视觉连续性靠插值保证,没假设回调一定按期望帧率来吗?
参考与出处
本文涉及的事实性信息(API 名称、枚举取值、版本号、官方约束)来自以下官方文档;文中的结构、代码示例、决策流程与自检清单为本人整理编写:
- 基于 LTPO 的低功耗设计(最佳实践)(更新于 2026-03-12)
- 可变帧率简介(开发指南)(更新于 2026-03-09)
- 请求动画绘制帧率
- 请求 UI 绘制帧率
- 请求自绘制内容绘制帧率(XComponent)
- DisplaySync(@kit.ArkGraphics2D 可变帧率)
- Animator.setExpectedFrameRateRange(API 12)
- 显式动画 animateTo 的 expectedFrameRateRange 参数
- ArkUI_ExpectedFrameRateRange(NDK,起始版本 12)
最后一句:LTPO 真正的难点不在expectedFrameRateRange那三个数——三个数填完不到十行。难的是承认"帧率该由内容决定":高动态给 120、稳定滚动 60、微动效 30,敢留 min=0 让系统自由降频,页面关掉前记得stop()。这四件想清楚,丝滑和省电系统自己会平衡;想不清楚,锁死 120 换来的只有一块发烫的机身和一条更陡的功耗曲线。