HarmonyOS 7 新特性8:闪控窗深潜——三种刷新策略、主窗帧率不动与CPU代价转移真机实测
HarmonyOS 7 新特性8:闪控窗深潜——三种刷新策略、主窗帧率不动与CPU代价转移真机实测
文章目录
1、引言
篇 1 里我把闪控窗做成了一张「轻量状态卡」——倒计时、进度、引擎态,悬浮在桌面上跟着专注走。写完回头看,「轻量」这两个字我一次都没量过。它到底多重?每秒重绘一次的那个小窗口,在渲染系统里花掉了什么?
我甚至提前写好了一个引子,打算在这篇开头用:「闪控窗一开,主窗口的录制预览掉了三帧。我没加任何界面,只是让那个小窗口每帧都刷新数字。」常驻窗口嘛,一直在画,画就得占合成预算,预算和主窗共享,所以主窗会卡——推理链看起来无懈可击。
真机数据出来时,这个引子被我整段删了。三档刷新策略下,主窗的卡顿计数增量全部是零。 一次都没有——而且后来我补了一组对照实验(让主窗持续滚动、悬浮窗每帧重绘),主窗照样一个卡顿都没有。这个「零」到底是系统做了隔离,还是负载太轻谁都掉不了,我给不出确定答案(第 7 节会老实交代);但有一件事是确定的:代价没有消失,它换了个地方。同一台 Mate 60 Pro,进程 CPU 占用从事件驱动的约 0% 抬到每帧刷新的约 33%,和实测出来的刷新率 1、11、60fps 对应。帧率的账主窗不背,整台设备的 CPU 背。
所以这篇的问题从「怎么节流防掉帧」变成了三个更实际的问题:
- 怎么量:常驻窗口的绘制开销,用什么工具量才有说服力?(顺手辟个谣:网上流传的 hiAppEvent FRAME_DROP 掉帧埋点,在 API 26 的 SDK 里根本不存在)
- 量出来是什么:每帧刷新、定时刷新、事件驱动三档,帧率、CPU、内存各是多少?信息密度(放一个数字还是放五个控件)影响大吗?
- 工程上怎么办:既然主窗不掉帧,是不是就可以随便每帧刷?当然不是——CPU 是电,是热,是和其他应用抢的资源。节流策略怎么按数据选?
环境声明:以下全部实测数据来自单台 Mate 60 Pro(ALN-AL80,麒麟 9000S),系统 HarmonyOS 7.0.0.107(SP8C00E105R4P3) / API 26 Release(正式版),IDE 为 DevEco Studio 26.0.0(内置 SDK 26.0.0.105 Release)。
可核查性:实验工程 LightLab(com.qingkouwei.lightlab),刷新层代码在 features/sparkperf/,证据(hilog 原文 + 三策略原始采样 JSON + 截图)随工程归档。DisplaySync API 见 @ohos.graphics.displaySync.d.ts(@since 11),hitchs/fpsCount 为 RenderService 的 hidumper 子命令(下文给原文输出)。
2、效果展示与项目结构
2.1 运行效果:一张可热切换的刷新实验台
诊断入口沿用篇 7 的「我的」Tab,新增绘制开销台:三档刷新策略(每帧 frame / 定时 100ms / 事件 event)× 三档信息密度(纯数字 / +进度条 / 全要素),加一行帧探测摘要和一行采样标准提示(pid、两个窗口名、清零命令直接印在面板上,实测时照抄):

截图里能看到实验态一切正常:策略=frame、密度=3,诊断报「来源=displaySync 帧数=9480 样本=240 中位=16.7ms」——16.7ms 就是 60fps 的帧间隔,说明 DisplaySync 的每帧回调在悬浮窗这个独立 UIContext 里真的能收到帧信号(这是预检门槛 P1 要回答的问题,后文细说)。
悬浮卡在 frame 模式下分别渲染密度 1(只剩一个大数字)和密度 3(全要素)的样子——E5 实验的两个端点:


2.2 工程结构:刷新层与状态层解耦
篇 7 把引擎抽成了单例(features/sparkwindow/),这次的刷新实验一行都不动它——状态机、错误码、串行化逻辑全部保持已过审状态,新增的东西全在 features/sparkperf/:
features/sparkperf/
├── SparkRefreshDriver.ets # 三策略驱动器:frame / timer / event,热切换
└── SparkFrameProbe.ets # 帧间隔自测:中位/最大/长间隔,来源分账
features/sparkwindow/
└── FloatCardView.ets # 悬浮卡改造:接策略 + 密度三档分支(E5 自变量)
解耦不只是洁癖:悬浮卡是 setUIContext 挂进独立窗口的内容页,它的刷新行为与主窗组件树是两个 UIContext 的事。刷新层(什么时候重绘)和状态层(重绘时读到什么值)各自有明确的变更理由,改动互不牵连——E1-E9 的每个实验变量都能单独拨动,这本身就是「刷新频率与数据源解耦」这条避坑的工程实现。
3、Kit 能力与核心 API 深度解析
3.1 先辟谣:hiAppEvent 没有 FRAME_DROP
写测量代码前我先查了 @ohos.hiviewdfx.hiAppEvent.d.ts(API 26),把事件常量逐个列出来核对:OS / USER_LOGIN / USER_LOGOUT / DISTRIBUTED_SERVICE_START / APP_CRASH / APP_FREEZE / APP_LAUNCH / SCROLL_JANK / CPU_USAGE_HIGH / BATTERY_USAGE / RESOURCE_OVERLIMIT / ADDRESS_SANITIZER / MAIN_THREAD_JANK / APP_KILLED / APP_HICOLLIE / AUDIO_JANK_FRAME / SCROLL_ARKWEB_FLING_JANK——17 个,没有 FRAME_DROP。
网上不少鸿蒙性能文章(包括一些二手教程)写的「用 hiAppEvent.write 上报 FRAME_DROP 事件统计掉帧」,在这个 SDK 上连常量都不存在。它可能是更早版本的遗留、可能是把别的系统的事件名张冠李戴,也可能干脆是编的——我没法考证来源,只能确认当下写不了。掉帧测量得换路。
3.2 帧率与卡顿的权威测量:RenderService 的两个子命令
真机上验证可用的路径是 hidumper 打 RenderService:
# 按窗口名取卡顿分档计数(累计值)
$ hdc shell hidumper -s RenderService -a "hitchs lightlab0"
window [lightlab0] Id[6846177869826]:
more than 66 ms 0
more than 33 ms 0
more than 16.67 ms 0
# 全局刷新率分布计数(LTPO 屏 60/120Hz 各档)
$ hdc shell hidumper -s RenderService -a "fpsCount"
Refresh Rate:60, Count:22285;
Refresh Rate:120, Count:40606;
有三点要说。第一,hitchs 是按窗口记账的——主窗 lightlab0 和悬浮窗 float_view_window(窗口名从 hidumper -s WindowManagerService -a '-a' 拿)可以分别查,这正是核心数据(3.4 节)能被量出来的原因。第二,它是累计计数,取增量要用「前后两次读数相减」。RenderService 确实提供清零命令(帮助列表原文:fpsClear |[surface name]/composer fpsClear, clear the fps info、clearFpsCount |clear the refresh rate counts info),但清零的具体时序行为我没实测,所以本篇一律用差值法,不依赖清零。第三,三档阈值 16.67/33/66ms 对应「掉 1 帧 / 掉 2 帧 / 掉 4 帧」,比笼统的「帧率」更有诊断价值:掉帧的严重程度分布直接可见。
CPU 和内存用 top -n 1 -b(toybox 版支持批处理模式,%CPU 在 PID 后第 8 列,别数错位)和 hidumper --mem <pid>(Pss 分栏)。
3.3 DisplaySync:应用侧的帧驱动,不是掉帧事件
@ohos.graphics.displaySync(@since 11,从 @kit.ArkGraphics2D 导出)给了应用一个「每帧回调」:
// ArkTS:DisplaySync 每帧回调(节选,与工程一致,此处省略代次闸门判断)
const sync: displaySync.DisplaySync = displaySync.create();
const range: ExpectedFrameRateRange = { min: 30, max: 120, expected: 120 };
sync.setExpectedFrameRateRange(range); // ExpectedFrameRateRange 是全局 ambient 类型
this.frameCb = (info: displaySync.IntervalInfo): void => {
// info.timestamp / info.targetTimestamp,单位纳秒
this.probe.onFrame(info.timestamp); // 记帧间隔
this.sink(); // 驱动一次重绘
};
sync.on('frame', this.frameCb);
sync.start();
它的定位得先说清楚:它是主动帧驱动,不是被动掉帧通知。我用它做两件事——制造「每帧无条件重绘」的压力档(frame 策略),以及顺手把帧间隔记下来(E6)。另外 off('frame') 不传回调引用会退订该类型全部订阅,和篇 7 讲的 emitter.off 是同一个坑的又一次出现,回调必须存字段。
还有一个对实验设计很要紧的问题:DisplaySync 在悬浮窗这个独立 UIContext 里收得到帧吗?工程里我做了自动降级预案(起帧后两级 3 秒窗口都收不到回调,就切 8ms 高频定时逼近每帧,并把「降级」记进说法)。真机答案是:
15:27:39.492 refresh driver start mode=frame via DisplaySync
15:27:42.496 DisplaySync alive at stage1, keep frame mode
一级窗口内就有真帧回调,不降级。悬浮窗内容页拿帧信号没问题——P1 门槛通过,这条也顺带证明 @StorageProp + @Watch 在独立 UIContext 下能触发(切策略时悬浮窗侧日志 applyMode triggered in float window 每次都出现)。
3.4 主窗帧率不动,代价转移到 CPU——以及一个我必须承认的归因边界
有了 3.2 的尺子,三策略各跑一个采样窗口(真实窗长约 63s,含读数往返开销,见第 7 节边界说明),读主窗 lightlab0 的 hitchs 增量:
| 策略 | fpsCount 60Hz 桶增量 | 折算刷新率 | 主窗 hitchs 增量(>16.67/33/66ms) | 进程 CPU 中位 |
|---|---|---|---|---|
| frame(DisplaySync 每帧) | +3780 | ≈60 fps(满帧) | 0 / 0 / 0 | 33.3%(29.6–37.0) |
| timer(100ms 定时) | +672 | ≈10.7 fps | 0 / 0 / 0 | 17.2%(14.8–18.5) |
| event(秒级数据驱动) | +67 | ≈1.1 fps | 0 / 0 / 0 | 0%(0–7.4) |
三行数据三个结论,其中第一个我要先泼自己一盆冷水:
- 主窗一帧没掉——但「系统做了隔离」这个归因,我没办法验证。 悬浮窗从每秒 1 次重绘拉到每秒 60 次满帧,主窗卡顿计数纹丝不动。我最初的写法是「RenderService 把悬浮窗当独立合成层,重绘不进主窗帧预算」,回头看时被抓了个正着:hitchs 统计的是该窗口自己超阈值的帧,一个不绘制的主窗本来就记不到卡顿——我的三组实验里主窗全程静止,零结果可能只是负载不足,压根没测到「共享预算竞争」这回事。于是我补了一组对照:让主窗持续往复滑动(60 秒约 70 次),分「有 frame 悬浮窗」和「无悬浮窗」两组各跑一遍——两组 hitchs 都是 0。也就是说在这个负载强度下,主窗自己滚动都不掉帧,「零」是平凡结果。把话说准一点:「常驻窗拖垮主窗帧率」的叙事无法复现;「系统做了合成隔离」这一机制本实验既不能证实也不能证伪(缺一个「无悬浮窗时主窗本该掉帧」的阳性对照)。 能确定的只有 CPU 那条线。
- 代价在 CPU,且边际递减。刷新率 1→11→60fps,CPU 0%→17%→33%。注意形状:从 1fps 到 11fps 就付出 17 个点,从 11 到 60 只再加 16 个点——大头在第一档,越往上边际越便宜(两段斜率差约 5 倍,是凹形不是线性,我第一版写成「近似线性」,被自己的表格打脸)。帧率的账没人背?不,整台设备背。CPU 是电、是热、是和其他应用抢的调度片——这些都不体现在主窗帧率上,但用户拿手机的手会知道。
- fpsCount 增量是刷新率的独立佐证。+3780/63s≈60fps、+672/63s≈10.7fps、+67/63s≈1.1fps,和策略设计值(60/10/1)对得上,说明三档切换真实生效,不是面板自说自话。
悬浮卡常驻的内存底价也量了:STARTED 态 Pss 64071 kB,结束专注销毁后 63377 kB,增量约 694 kB(单次对照,受 GC 时机抖动,当量级参考)。销毁后窗口树里 float_view_window 计数归零,teardown 链路干净。
4、逻辑流梳理
4.1 三策略刷新链路
event 档不装任何驱动器——数据源自己就是心跳(秒变 → AppStorage → @StorageProp → 局部刷新),这是篇 1 的现状基线。frame/timer 档的 bump 是无条件的:数字没变也强制走一遍 build,刻意制造「无谓重绘」,这样三档才构成干净的对照实验。
4.2 epoch 代次闸门:热切换的安全边界
策略是运行时随便切的,切早了会出事:DisplaySync 的回调从入队到执行有系统调度的时间窗,off 之后仍可能有最后一两个已入队的回调落地;定时器的 clearInterval 同理。这些「旧时代的遗孤」如果继续执行,就会 bump 新策略的 @State、污染新窗口的诊断统计。我的做法是给每次 start/stop 递增一个代次号,所有异步回调进来先对号:
// ArkTS:代次闸门(节选)。旧代次回调一律丢弃,不写诊断也不 bump
private epoch: number = 0;
private startDisplaySync(): void {
const gen: number = this.epoch;
const sync: displaySync.DisplaySync = displaySync.create();
this.sync = sync; // create 成功即登记,异常路径才拆得掉
this.frameCb = (info: displaySync.IntervalInfo): void => {
if (gen !== this.epoch) {
return; // 已过时代,静默丢弃
}
this.probe.onFrame(info.timestamp);
this.sink();
};
sync.on('frame', this.frameCb);
sync.start();
}
stop(): void {
this.epoch++; // 闸门落下,此后一切旧回调 no-op
// …clearTimeout/clearInterval/teardownSync 各自容错
}
真机上快速连切 frame→timer→frame→event 四次,日志每次恰好一条 refresh driver start、时间戳严格递增、最终收敛 event,诊断无残留增长——闸门生效。这段代码本身就是代码审查的产物:审查时两条独立意见都指出「已排队回调可跨切换继续写状态」,我最初想用「stop 时把 sink 置 null」补救,权衡后选了代次方案——置 null 只挡 sink,挡不住诊断写入,而且组件重挂载时还得重建闭包,代次闸门一并解决。
5、项目实战
5.1 诊断的统计标准:两个只有真机能暴露的坑
帧诊断(SparkFrameProbe)维护一个 240 样本的环形缓冲,出中位/最大/长间隔三个指标。听起来简单,代码审查加真机实测各逮出一个说法错误。
坑一(四条审查意见全命中):resetWindow() 最初只清环形缓冲,没清累计计数 count。而 P1 降级判定恰恰依赖 totalCount() === 0——只要之前跑过任何策略,切到 frame 后计数永远非零,降级机制永久静默失效。不修的话,DisplaySync 万一在悬浮窗里收不到帧,实验会无声跑零帧、不降级、不报警,整组数据作废。修复就是把 count 和 source 一起归零:
// ArkTS:全量清窗。count/source 不清,P1 判定就废(审查修订,4/4 一致确认)
resetWindow(): void {
this.gaps = [];
this.count = 0;
this.source = 'none';
this.lastNs = 0;
this.lastMs = 0;
}
坑二(真机暴露):修完上线,面板打出「中位=17ms 最大=16.667ms」——中位数比最大值还大,自相矛盾。根因是中位数做了 Math.round 而最大值返回原始浮点,16.667 四舍五入成 17,两个指标的舍入标准打架。静态审查抓不到这个(代码逻辑都对,只是格式化不一致),是真机数据一跑就现形。修复统一交给 summary 做 toFixed(1):
// ArkTS:中位数不再 round,与最大值同说法,summary 统一格式化
medianGap(): number {
if (this.gaps.length < 2) {
return -1;
}
const s: number[] = this.gaps.slice();
s.sort((a: number, b: number): number => a - b);
const mid: number = Math.floor(s.length / 2);
return s.length % 2 === 0 ? (s[mid - 1] + s[mid]) / 2 : s[mid];
}
同类问题还有一个:长间隔计数最初是单调累加、环形淘汰样本时不回退,导致「样本=240(当前窗口)」和「长间隔=全生命周期累计」并列展示。改成对当前环形窗口动态重算,三个指标才真正同窗口。
5.2 E5 信息密度实验:又一个反直觉
固定 frame 策略,切三档密度各跑一个采样窗口(名义 20s,真实约 23s):
| 密度 | 内容 | CPU 中位 | fpsCount 60Hz 桶增量 |
|---|---|---|---|
| 1 | 纯倒计时数字 | 33.3% | +1408 |
| 2 | 数字 + 进度条 | 33.3% | +1413 |
| 3 | 全要素(篇 1 现状) | 33.3% | +1416 |
三档几乎无差异。 瓶颈是刷新频率本身,不是子树复杂度——DisplaySync 每帧回调固定来,ArkUI 对没有变化的子节点做 diff 复用,多几个 Text/Progress 在「每帧都 bump」面前不构成额外压力。这里得把归因标明白(这是基于当前数据的推断,不是实测结论):内容组织的代价应该只在「事件驱动 + 大子树频繁变化」时显现;frame 模式下这个变量被刷新频率压平了,所以测不出差异——测不出差异本身就是有效结论,它告诉你优化该往哪使劲:砍刷新率,别抠控件数量。
5.3 E6 帧间隔与 LTPO:两个说法别混着说
诊断侧的帧间隔数据:frame 策略稳定期中位 16.667ms(60fps),某几秒跳到 8.3ms(120fps)——DisplaySync 跟随了 LTPO 屏的升降档。但这里得把观察和归因分开说:E6(应用侧 DisplaySync 回调间隔)和 E9(RenderService 侧 fpsCount 合成计数)是两个说法,不能直接画等号。8.3ms 那几秒屏幕升到了 120Hz 档,而 frame 那个采样窗口的 fpsCount 增量全部落在 60Hz 档(120Hz 计数 +0)——不同采样窗口里屏幕档位不同,数据不能跨窗口比。单设备 + 采样窗口代表性,这就是这篇的边界之一。
5.4 E7/E8 尺寸与模板响应:顺手采的两个快数据
- E7:
setWindowSize传超限值 2268×1702(limits.max 1134×851 的 2 倍)→ Promise ≈4ms resolve,未报错,getWindowProperties核对实际值夹回 1134×851。附带一个新边界:夹回后的尺寸等于当前尺寸,所以onRectChange没有来——这个回调只在实际几何变化时触发,不能拿它当「调用成功」的信号,核对必须主动查属性。 - E8:
switchTemplate圆角矩形→水平条 → Promise 到回调 ≈2ms,onLimitsChange(max=1134×104)与onRectChange(SIZE_CHANGE)几乎同拍到达。水平条的 max 高只有 104px(圆角矩形 851),两种模板的布局差异巨大,切换前把内容按目标模板重排不是可选项。
5.5 三方案选型对比(用实测数据填)
| 维度 | event 事件驱动 | timer 定时 | frame 每帧 |
|---|---|---|---|
| 实测刷新率 | ≈1.1 fps | ≈10.7 fps | ≈60 fps(满帧) |
| 主窗 hitchs 代价 | 0 | 0 | 0(含主窗滚动对照) |
| 进程 CPU | ≈0% | ≈17% | ≈33% |
| 数据实时性 | 秒级(够用,倒计时本来就一秒一跳) | 100ms | 帧级 |
| 实现复杂度 | 最低(@StorageProp 天然驱动) | 低(一个 setInterval) | 中(DisplaySync 生命周期 + 代次闸门) |
结论一句话就能说完:倒计时这种秒级数据源,event 档就是正确答案——实时性匹配、CPU 几乎为零、代码最少。timer 和 frame 存在的意义是当对照,量出「多花的每一档刷新率值不值」。如果内容真需要高频动效(进度环补间之类),优先让 ArkUI 动画系统自己驱动属性动画,而不是把应用侧 bump 拉满——动画属性走渲染线程,比每帧回 JS 里改 @State 便宜得多。
6、避坑指南
6.1 hiAppEvent FRAME_DROP 不存在,掉帧测量别照抄网文
- 现象:按网文写
hiAppEvent的 FRAME_DROP 事件订阅,常量根本导入不了。 - 根因:API 26 的 hiAppEvent 事件常量共 17 个,无此事件;网文来源无法考证,可能是旧版遗留或张冠李戴。
- 方案对比:hiAppEvent(写不了)vs RenderService hitchs/fpsCount(本篇,按窗口分档计数)vs 应用侧 DisplaySync 自测帧间隔(互补)。
- 最佳实践:掉帧/帧率测量走
hidumper -s RenderService -a 'hitchs <窗口名>',按窗口取、三档阈值;hitchs 是累计值,一律用前后两次读数相减——RenderService 虽有 fpsClear/clearFpsCount 清零命令,但其清零时序行为未实测,不依赖它。 - 验证:E1/E9 全部数据出自这条路径,原始输出见
docs/evidence/dp8-raw-*.json。
6.2 主窗 hitchs 恒 0 不等于「系统做了隔离」——零结果要先做阳性对照
- 现象:悬浮窗每帧重绘 60 次每秒,主窗 hitchs 增量为 0,第一反应是「系统做了合成隔离,代价不进主窗账」。
- 根因(审查抓出):hitchs 统计的是该窗口自己超阈值的帧;一个不绘制的主窗本来就记不到卡顿——三组实验里主窗全程静止,零结果可能只是负载不足,压根没测到「共享预算竞争」。
- 方案对比:把零结果直接归因隔离(本篇第一版的错)vs 补「主窗持续滚动」的对照组再下结论(本篇修正版)。
- 最佳实践:零结果(null result)只能说明「在这个负载下没差异」,要归因机制必须先做阳性对照——先证明「无干预时本应掉帧」,否则零结果只是实验不够狠;评估常驻窗口开销,hitchs 之外必须配
top采 CPU、hidumper --mem采内存——代价不会消失,只会转移。 - 验证:补测两组(有 frame 悬浮窗 + 主窗滚动 76 次 / 无悬浮窗 + 主窗滚动 67 次),hitchs 均 0/0/0——「零」确认是平凡结果,隔离归因降级为「无法证实也无法证伪」,CPU 0→17→33% 才是确定证据。
6.3 统计标准混入:清窗不清计数,降级判定永久失效
- 现象:P1 自动降级(DisplaySync 静默 3s 即切高频定时)在实验室永远不触发。
- 根因:
resetWindow()只清了环形缓冲,没清累计计数count;降级判定totalCount() === 0读到的是跨策略累计值,热切换后必然大于零。 - 方案对比:判定改用独立会话计数(多一个状态要维护)vs 清窗全量归零(本篇)。
- 最佳实践:凡是「窗口 A 的统计要喂给窗口 B 的判定」的结构,清窗必须清全——计数、来源、时间戳基线一个不落;统计函数与判定条件共用的字段,写代码时先画清生命周期。
- 验证:修复后真机快速连切四次,诊断计数随切换归零重计,无跨窗口污染。
6.4 中位数 round 与最大值不 round,面板自相矛盾
- 现象:诊断摘要「中位=17ms 最大=16.667ms」,中位大于最大,数学上不可能。
- 根因:medianGap() 出口做了
Math.round,maxGap() 返回原始浮点,16.667→17 后两指标舍入标准不一致。 - 方案对比:所有出口统一保留原始值、只在展示层格式化(本篇)vs 每个 getter 各自 round(漏一个就再犯)。
- 最佳实践:一组要并列展示的统计量,舍入策略必须统一且放在最后一层;单元测试测不出这种错(每个函数单看都对),真机数据跑出来才现形——这也是我坚持每篇必须上真机采证的原因。
- 验证:修复后复测「中位=8.3ms 最大=8.3ms」,矛盾消失。
6.5 DisplaySync 的 off 与孤儿会话:create 成功就要登记
- 现象:策略热切换或组件销毁后,偶尔还能看到旧回调在 bump;极端情况下(on/start 抛错的异常路径)帧会话彻底失去引用。
- 根因:两个结构缺陷——
this.sync = sync写在start()成功之后,中间抛错则局部对象无人持有、永远拆不掉;stop()里 off 和 stop 共用一个 try,off 抛错会跳过 stop 还把引用置 null。 - 方案对比:try/finally 包一切(粒度粗)vs create 即登记 + teardownSync() 两步独立容错(本篇,off 失败也保证 stop 执行)。
- 最佳实践:系统资源的句柄,拿到手的瞬间就登记进可清理的位置,别等「初始化全部成功」——异常路径才是泄漏的温床;另外
off('frame')不带回调引用会退订全部,与 emitter 同坑。 - 验证:epoch 闸门 + 登记式清理落地后,连切/关窗压测无残留 tick。
6.6 onRectChange 不是「调用成功」的信号
- 现象:
setWindowSize传超限值,Promise resolve 了,等onRectChange核对夹回结果——左等右等回调不来。 - 根因:E7 实测——超限值 2268×1702 被静默夹回 1134×851,而当前尺寸恰好就是 1134×851,几何没变,系统不发变更回调。onRectChange 的语义是「变了才说」,不是「办完了说」。
- 方案对比:等回调核对(会死等)vs 主动
getWindowProperties查询(本篇)。 - 最佳实践:所有「静默调整」类接口(尺寸夹回、篇 7 的超限探测),核对结果一律用属性查询接口,回调只当增量通知用。
- 验证:E7 原始日志(
dp8-hilog-e7e8.txt):resolve 后无 RECT_CHANGE,getWindowProperties 返回夹回值。
7、总结
回到开头的三个问题。怎么量:hiAppEvent 没有 FRAME_DROP,掉帧走 RenderService hitchs(按窗口、三档阈值、取差值),刷新率走 fpsCount,CPU/内存走 top 和 hidumper --mem。量出来是什么:主窗帧率在这个测试强度下纹丝不动(三策略 hitchs 恒 0,补了主窗滚动对照还是 0),代价转移到 CPU(0/17/33%,边际递减),内存底价约 694kB,信息密度在高频刷新下不构成差异。工程上怎么办:秒级数据源就用事件驱动(event 档),把每帧刷新的冲动留给真正需要帧级动效、且优先交给 ArkUI 属性动画的场景。
这篇给我自己最大的收获有两层。第一层是预设结论被打掉的过程:引子写好了、故事线想好了、连「掉三帧」的数字都准备引用了——数据出来是零。如果当时选择「再测一轮肯定能测出来」,那就是在给自己编素材。第二层更疼:零结果本身也差点被我写成结论。我最初把「主窗不掉帧」直接归因成「系统做了合成隔离」,回头看时一针见血——不绘制的主窗本来就记不到卡顿,我的零结果可能只是负载不足。补了对照实验(主窗持续滚动,有无悬浮窗各跑一遍)之后发现:这个强度下主窗自己滚动都不掉帧,「零」是平凡结果。测不出差异,比测出差异值钱——但前提是你知道自己测不出的是什么:是「没有代价」,还是「实验不够狠」。这两者的分界线,就是阳性对照。
下一篇(深潜篇 9)把闪控窗三部曲收尾:冲突治理与降级——闪控球/画中画的互斥矩阵、1300034 冲突实录、三级降级路径,以及 FLOAT_VIEW 与 SYSTEM_FLOAT_WINDOW 两种悬浮权限的边界(后者误声明会直接导致安装失败,是个高频误区)。
更多推荐


所有评论(0)