HarmonyOS LTPO 帧率实战:别把刷新率锁死 120Hz,expected 按内容填
2026/9/13 23:44:57 网站建设 项目流程

本文聚焦 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/maxV哥的建议
高动态应用启停、窗口转场、拖拽窗口90~120Hzexpected 120、min 90 兜底,纯全屏非持续动效才用
稳定滚动视频弹幕、小说翻页、消息滑动60Hzexpected 60、min 0,闲时让系统自由降频
微动效转盘、加载转圈、滚动条消失15~30Hzexpected 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 的低功耗设计 · 功耗测试工具):

  1. 看实时刷新率:设置里搜"开发者" → 打开"显示刷新频率"开关,肉眼看屏幕帧率有没有随内容降下来;
  2. 量整机功耗: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)?
  • displaySyncaboutToDisappearstop()并置空了吗?避免回调泄漏。
  • 切档用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 换来的只有一块发烫的机身和一条更陡的功耗曲线。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询