HarmonyOS 7 新特性11:碰一碰深潜——注册时序、假信号、七级排查链与传输段没测到的部分

1、引言

篇 10 交了坐标的账,这篇交会话的账——而会话这本账,是真机碰了前后八次才对平的。

按理说不该这么难:接口面篇 4 就对着 d.ts 确认了,注册、组包、回调签名全部编译期核对过,双端设备也都在手上。结果第一次碰下去,两台设备毫无反应。后面七轮,断点从应用层一路退到系统层:先是注册时序不对,再是锁屏把注册作废,然后是信任关系没建立、WiFi 没开、数据类型选错、uri 格式传错——每一层修好,下一层的问题才露出来。最后一轮,双端回调全部触发、坐标真值到手,文件体传输段却又断在 softbus 层,成为本篇写作时的没测到的部分。

这篇的价值不在「碰通了」三个字,在七级排查链:检测→注册→锁屏→信任→传输→系统解析→派发,每一级都有独立的日志特征,碰一碰无反应时按链排查,哪级断了一眼定位。以及三个反直觉的发现——注册挂按钮上等于没注册、share() 秒回是假信号、receive() resolve 不等于传输完成。这三条每一条都能让一个熟练的鸿蒙开发者排查半天。

环境声明:发送端 Mate 60 Pro(ALN-AL80,HarmonyOS 7.0.0.107 / API 26 Release);接收端 MatePad Edge(QXS-W00,PC 形态);实测工程轻碰台(com.qingkouwei.lightknock),诊断页双角色注册(knockShare + dataReceive)带 L0-L5 六级打点。完整排查每一步的结论都有当步的日志与截图为证,下文引用处都标了步骤序号。

2、效果展示与完整排查总账

先给结局:第前后八次碰通,双端诊断页读数——

pad 接收侧:dataReceive 两次触发(间隔 9225ms)、坐标真值 (1481,1135)/(1483,1245)、receive resolve 0-2ms;手机发送侧:knockShare 两次触发(间隔 17325ms)、share resolve 1-2ms、coordinate=undefined 判空分支命中。

HarmonyOS 7 新特性11:碰一碰深潜——注册时序、假信号、七级排查链与传输段没测到的部分-1.png

前后八次总账,每轮断在哪一级、日志特征是什么:

轮断点级日志/现象特征修复动作
1注册+锁屏bundleName not registered;截图指纹图标aboutToAppear 自动注册
2传输softbus DiscCoap publish fail(WiFi 未开,第 4 轮才确诊)
3注册手机端无 AddCallback success双端进程强杀重启(install 不重启进程)
4传输ifconfig 仅 lo 无 wlan0双端开 WiFi/蓝牙
5信任GetTrustedDeviceList# size:0双端同华为账号 + pad「碰一碰」开关
6系统解析发送侧回调通、系统层静默(uri 问题第 7 轮才确诊)
7系统解析instantshare Both content and uri are emptyuri 改 fileUri 格式 + utd 改 FILE 型
8通(传输段留边界)双端回调+坐标全到;softbus CloseSession -426115034见第 6 节没测到的部分

HarmonyOS 7 新特性11:碰一碰深潜——注册时序、假信号、七级排查链与传输段没测到的部分-2.png

注意第 5 轮前后 GetTrustedDeviceList 的对照:同账号前 size:0,同账号后列表里出现两台设备(deviceTypeId 14=手机、17=平板)。信任层通没通,这一行日志就是依据。

3、Kit 与 API:会话面的接口与约束

3.1 注册/解绑的签名细节

harmonyShare 的会话面接口,d.ts 原文核对过的三条签名规矩:

// 发送侧:无 registry 重载,回调签名 Callback<SharableTarget>
function on(event: 'knockShare', callback: Callback<SharableTarget>): void;
function off(event: 'knockShare', callback?: Callback<SharableTarget>): void;

// 接收侧:必须带 RecvCapabilityRegistry(windowId + utd 能力表)
function on(event: 'dataReceive', capability: RecvCapabilityRegistry,
  callback: Callback<ReceivableTarget>): void;
// ⚠️ off 的第二参是 registry 不是 callback——解绑要拿注册时同一个对象
function off(event: 'dataReceive', capability: RecvCapabilityRegistry,
  callback?: Callback<ReceivableTarget>): void;

实测补三条系统侧行为:

  1. 注册送达的系统依据是 ShareManagerService: AddCallback success + RegisterRecvUtdSummary(hilog C04614 域)。应用侧 try-catch 没报错不等于注册送达,看系统日志才算数。
  2. 手机(device type 14)注册 dataReceive 会被系统拒:CheckCapability# Capability not supported——设计如此,手机在碰一碰里只能当发送方。双角色诊断页在手机上必然半边失败,这不是 bug。
  3. 注册绑定窗口生命周期:页面 pop(aboutToDisappear 解绑)或应用退后台/锁屏,注册即失效。第一次在手机锁屏态下碰,系统 Motion 层检测到了碰、动效都拉起来了,但 Publish 层报 bundleName not registered,回调不派发,动效 3 秒自毁——锁屏是注册的隐形杀手。

3.2 数据面约束(说法出处:开发指南原文)

约束项上限出处
分享数据描述总大小≤ 200 KBShare Kit 开发指南·卡片数据
SharedRecord 记录数≤ 500 条同上
dataReceive 沙箱接收仅文件型;文本走原始接收逻辑(系统打开方式/保存面板),不进沙箱回调d.ts dataReceive 注释原文
receive(receiveUri)收已存在目录(系统落文件进去),传文件路径无效d.ts 参数注释原文
SharedRecord.urifileUri 格式(file://...),裸沙箱路径被系统解析层拒收第 7 轮真机日志 1003703001

第三条是前后八次里最贵的学费,第 5 节展开讲。

4、逻辑流:会话状态机与七级排查链

物碰(加速度计+触屏)

MotionType2500 / OneTouch 拉起

AddCallback success?

双端解锁亮屏+应用前台?

TrustedDeviceList 非空?

softbus 发现广播成功?

SharedData 解析通过?

knockShare / dataReceive

receive() 后文件体异步传输

onDataReceived + 文件落地

手势无冲击/碰位不在注册窗口

not registered(手动注册/进程未重启)

锁屏态注册作废

设备列表为空(不同账号/开关未开)

DiscCoap fail(WiFi/BT 关)

uri empty 1003703001(裸路径/文本型)

CloseSession -426115034(没测到的部分)

检测层

注册层

锁屏层

信任层

传输层

解析层

派发层

应用回调

传输段

断

每级断点有独立日志特征\n按链排查逐级定位

七级链的用法:碰无反应时从后往前问三个问题——诊断页有没有新读数(派发层)?系统日志有没有 AddCallback success(注册层)?碰的瞬间有没有 MotionType2500(检测层)?三个答案把断点区间先切成三段,再在段内按日志特征细分。完整排查实测下来,这套问法平均两轮定位一级,比从头顺查快得多。

5、实战:三个反直觉发现的真机实录

5.1 环境与前置

系统/IDE/芯片同篇 10 环境声明。诊断页 KnockLinkPage 双角色注册 + L0-L5 打点(L0 注册态、L1 坐标、L2 发送链、L3 接收链、L4 幂等序列、L5 等待观察)。以下三段实录全部来自真机日志原文。

5.2 发现一:注册挂按钮上等于没注册

第一版诊断页把 doRegister() 挂在「注册双角色」按钮上——页面注释还写着「进入即注册」,实际没写 aboutToAppear。结果碰的瞬间系统报:

CollaborationFwk/Publish: SetCurrentBundleName:
  bundleName = com.qingkouwei.lightknock not registered

对照线上工程 AeroPlan 的 ProjectDetailSheet(已上线的碰一碰实现):aboutToAppear 里 initShareFeatures()、aboutToDisappear 里 releaseShareFeatures()——注册必须绑页面生命周期自动走。碰一碰是「系统在碰的瞬间查前台发布者」的模型,不是「应用主动发起」的模型:用户碰的时候不会先替你点一下注册按钮,窗口在前台但回调没挂上,系统眼里你就是没注册。

修复一行代码(aboutToAppear 调 doRegister),pad 端系统日志立刻出现 AddCallback success。另一个连体坑:hdc install 不重启应用进程——装完新包旧进程还在跑旧代码,第 3 轮差点把「自动注册无效」当成误诊。规矩:装包后必须 aa force-stop 再 aa start。

5.3 发现二:share() 秒回是假信号

第 5、6 轮,手机发送侧已经稳定触发:knockShare 回调、L2 share resolve 2ms——应用层全绿。pad 侧却只有 L0,dataReceive 一次没响。手机端诊断页当时的读数(累计四次触发,share resolve 全部 0-1ms):

HarmonyOS 7 新特性11:碰一碰深潜——注册时序、假信号、七级排查链与传输段没测到的部分-3.png

第 7 轮把手机系统日志(C04611 instantshare 域)捞出来才看到真相:

SystemShareNapi/SharedData.cpp: ParseTypedRecordFromParams#
  Both content and uri are empty
SharedData.cpp: ReadFromWantParams# failed
HuaweiShare: getShareData error Parse data failed. 1003703001

我传的 SharedRecord.uri 是裸沙箱路径 ctx.filesDir + '/knock_demo.txt',系统解析层要的是 fileUri 格式(file://...)。裸路径被判「uri 为空」直接拒收——而 share() 的 Promise 照样 resolve,应用层零感知。1ms 的 resolve 不是「发得快」,是「系统收下了 API 调用、还没走到解析」。

对照 AeroPlan 再次印证:它的 thumbnailUri 用的就是 fileUri.getUriFromPath(...)。同一轮还修了第二个数据面坑:utd 用 PLAIN_TEXT 时,按 d.ts 原文文本数据走「原始接收逻辑」(系统打开方式面板),根本不进 dataReceive 沙箱回调——沙箱接收只认文件型。两处修复:

// 修复前(第 5-6 轮,假信号):
// utd: utd.UniformDataType.PLAIN_TEXT,
// uri: ctx.filesDir + '/knock_demo.txt',

// 修复后(第 7-8 轮,真派发):
const rec: systemShare.SharedRecord = {
  utd: utd.UniformDataType.FILE,                          // 沙箱接收仅文件型
  uri: fileUri.getUriFromPath(ctx.filesDir + '/knock_demo.txt'), // fileUri 格式
  title: '轻碰链路演示',
  description: '篇10/11 对端轮合成数据',
};

排查规矩从这里立起来:分享类问题必看系统服务日志(instantshare C04611 / ShareManagerService C04614),应用层 Promise 状态不可信。

5.4 发现三:幂等与时延的实测账

第 8 轮全链路时序账,应用层与系统传输段的分界线画在 receive() resolve 那一刻:

pad 轻碰台 (dataReceive) softbus (传输段) 系统分享服务 (instantshare) 手机轻碰台 (knockShare) pad 轻碰台 (dataReceive) softbus (传输段) 系统分享服务 (instantshare) 手机轻碰台 (knockShare) 这次没测到的部分,如实列在这里:传输段断开 onDataReceived 未触发 接收目录空 碰触发 L4 第1次 间隔序列 [—] share(SharedData) FILE 型 + fileUri resolve 2ms ⚠️ 假信号:仅受理 SharedData 解析通过 文件体入传输队列 会话建立 + 元数据 dataReceive 触发 L1 坐标 (1481,1135) receive(目录) resolve 0ms ⚠️ 假信号:仅受理 TransTdcUnPackAllTlvData 分片接收中… CloseSession ret=-426115034

前后八次碰击攒下的会话数据,按篇 4 立的三方案说法对账:

数据项实测值结论
接收侧触发间隔9225ms(两次独立物碰)无重复派发,系统未做合并
发送侧触发间隔17325ms / 10087ms(多轮)一次物碰对应一次回调
单次物碰回调次数1未观察到抖动重发
share/receive resolve0-3ms仅 API 调用段,见 5.3 假信号
碰窗动效自毁(失败轮)约 3s配对超时窗口实测值

幂等键三方案(纯 uri / uri+时间窗 / 一次性票据)的选型结论:本实测规模下系统没出现过重复派发,纯 uri 幂等键暂时够用;但 9 秒与 17 秒的间隔序列提醒——用户完全可能对同一内容连碰两次(手抖重试),业务侧幂等窗口建议 ≥20s(覆盖两次独立碰的最小实测间隔一倍余量)。超时取消侧:失败轮的动效 3 秒自毁 + HandleOnOneHopCancel 日志说明系统自带超时回收,应用侧不需要自建取消定时器,但 aboutToDisappear 的解绑必须写——第 5 轮手机诊断页被误触 pop 后,正是解绑逻辑把注册干净地收掉了,没留僵尸回调。

6、避坑指南与没测到的部分

坑1:200KB/500 条约束的验证方式

现象:约束在文档里,超限行为没人见过——这次的数据量(一条文本记录)远够不到上限。

根因:约束校验发生在系统解析层,和 5.3 的 uri 拒收同一层;超限预期是解析失败而非崩溃,但未实测不冒充实测。

方案:业务侧把「组包时自算 description 字节数 + 记录数」做成发送前断言(超限就地裁剪或分批),别赌系统层的错误提示——那一层的失败应用侧未必有感知(假信号教训同源)。

验证:本篇验证到「约束说法出处为开发指南原文」为止;超限行为的真机实测列入待办,不编。

坑2:传输段断开——receive() resolve ≠ 传输完成

现象:第 8 轮全链路绿:双端回调触发、坐标到手、receive() resolve 0-2ms——但接收目录是空的,onDataReceived 未触发,全盘搜不到落地文件。

根因:文件体传输走 softbus 异步段,pad 侧日志可见分片接收进行中(TransTdcUnPackAllTlvData# recv data next time)后会话被断:TransSdk: CloseSession# ret=-426115034。断点在系统传输层,非应用代码可控——这就是本篇的没测到的部分。

方案对比:

策略说明取舍
轮询接收目录onDataReceived 不来就定时 listFiles能用但脏,超时语义自定
等 onDataReceived+超时降级回调为准,30s 不到转人工提示推荐,语义干净
只信 receive() resolve假信号重演禁止

最佳实践:应用层以 onDataReceived 为传输完成的唯一依据,receive() resolve 只当「系统受理」;受理与完成之间给超时降级路径。

验证:验证到「回调+坐标链跑通、文件体传输断开」为止;传输段恢复条件(重碰即恢复?需重启分享服务?)等后面再测。

坑3:碰的姿势与窗口位置是实测变量

现象:同一套代码,同样的操作,有时系统认碰(通知栏提醒+动效),有时毫无动静。

根因:检测层是「手机加速度计 + 对端触屏」双信号与:慢贴只有触屏有读数(THP_UpdatePadCollision)加速度计无感;手机屏幕贴到对端屏幕会产生误触(第 5 轮把诊断页 pop 了)。碰位还必须落在注册窗口区域内——碰桌面壁纸不算。

方案:手机背面顶端、带冲击感轻碰对端屏幕上注册窗口所在区域,保持 2-3 秒;实测前把对端应用窗口点回前台聚焦。

验证:第 8 轮按此姿势双端检测+派发全通;失败轮日志可回放对照(单端检测/误触 pop 各有特征)。

7、总结

前后八次碰下来,碰一碰的会话可靠性账本合上了:七级排查链每级有日志依据,三个假信号(按钮注册、share resolve、receive resolve)各有一句系统日志拆穿,幂等与时延有了第一手实测数。回头看,第前后八次的成功截图只值一分钟,每一级断点的日志特征才是能留下来的东西——not registered、size:0、DiscCoap fail、uri empty、CloseSession,这五句日志就是碰一碰排障的五张 X 光片,以后谁再遇到「碰了没反应」,按链走一遍就知道断在哪。# HarmonyOS 7 新特性9:闪控窗深潜——闪控球互斥双向实测、三级降级阶梯与两种悬浮权限的边界

1、引言

闪控窗篇(本卷篇 1/7/8)讲的都是「一个应用自己的悬浮窗」:怎么拉起来、状态机怎么走、刷新怎么节流。但真实桌面上悬浮者不止一个——闪控球(floatingBall,API 20 起的另一种悬浮形态)、画中画、全局悬浮窗(TYPE_FLOAT),都和闪控窗抢同一块前台空间。官方开发指导给了一句约束:「已启动闪控球或画中画时无法启动闪控窗」,错误码 1300034。

一句话约束留下三个工程问题:互斥是双向的吗(球占着时闪控窗起不来,反过来呢)?冲突发生后应用该怎么办(报错给用户还是退一档)?以及一个更基础的权限问题——闪控窗用的 FLOAT_VIEW 和全局悬浮窗用的 SYSTEM_FLOAT_WINDOW 是两种完全不同的权限模型,误用后者会让应用连装都装不上。这篇在轻规划的冲突诊断台上把三件事逐个实测,诊断台形态照旧自声明:对照实验台,非生产设置页。

环境声明:HarmonyOS 7.0.0.107 (API 26) Release,实测工程轻规划(com.qingkouwei.lightlab,CSDN 卷载体);DevEco Studio 26.0.0(SDK 26.0.0.105 Release),真机 Mate 60 Pro(ALN-AL80,麒麟 9000S)。代码在该环境编译签名部署运行,读数全部真机实测。

2、效果展示与诊断台结构

诊断台五组实验区加一条日志流:权限边界、冲突双向、bind 联动、三级降级、恢复主窗,日志流按 tag 直显原文。

冲突实录界面:专注窗已启动(闪控窗这一级),点「a 窗→启球」后球启动被拒,阶梯自动降到应用内浮层——闪控窗卡片仍悬浮在诊断台之上,日志区记下 1300034 与降级账。

HarmonyOS 7 新特性9:闪控窗深潜——闪控球互斥双向实测、三级降级阶梯与两种悬浮权限的边界-1.png

三级阶梯走到底的样子:注入 801(不支持类)后阶梯从应用内浮层降到页面内联,悬浮卡消失、专注信息内联进页面,状态行报 L=INLINE · CONFLICT 1 · UNSUPPORTED 1。

HarmonyOS 7 新特性9:闪控窗深潜——闪控球互斥双向实测、三级降级阶梯与两种悬浮权限的边界-2.png

诊断台不生产悬浮能力,只调用 SparkEngine(篇 1/7/8 已过审的闪控窗引擎)与 BallController(闪控球控制器封装)制造前置状态,冲突与降级逻辑在 FallbackLadder 里按错误码分类记账。

3、Kit 与 API:两种悬浮权限与闪控球接口面

3.1 两种悬浮权限不是一回事

这是全卷被问得最多的一个误区,单独列一节:

维度闪控窗 FLOAT_VIEW全局悬浮窗 SYSTEM_FLOAT_WINDOW
权限类型user_grant,运行时弹窗申请受控开放,AGC 申请 ACL 证书
证书排期不卡卡审核排期
设备面Phone / Tablet / 2in1仅 PC / 2in1
误声明后果运行时拒绝(201)安装直接失败
形态系统托管模板窗任意 WindowType.TYPE_FLOAT 窗

最后一行是重点:module.json5 里声明了 SYSTEM_FLOAT_WINDOW 而没拿到 ACL 证书,应用连安装这一步都过不了——不是运行时报错,是装不上。本卷全部工程刻意只走 FLOAT_VIEW,就是为绕开这道排期;诊断台的权限边界这组把 FLOAT_VIEW 的运行时弹窗实录留档(requestPermissionsFromUser → true)。

3.2 闪控球接口面(API 20 起)

闪控球是另一种悬浮形态:小窗球体挂屏幕侧边,跨应用常驻。接口面(开发指导直读):isFloatingBallEnabled() 能力查询、create(config) 取控制器、startFloatingBall(params) / updateFloatingBall / stopFloatingBall、on('stateChange'/'click') 监听、getFloatingBallWindowInfo()、restoreMainWindow(want) 恢复主窗。

两个约束决定了这次实验的设计:权限 USE_FLOAT_BALL 是受限权限,仅对题目搜索、账单记录、商品比价、抢单、翻译、金融盯盘六类场景开放——实测工程不在场景内,球侧实验按「机制可达、场景不冒用」说法跑(create/start 的接口行为可读,不伪装业务场景);同一应用只能启动一个闪控球、同设备最多两个,超出替换最早的——这解释了 bind 联动的实录里 1300022 Repeated 的触发条件。

权限边界读数:isFloatingBallEnabled()=true 且 syscap=true,USE_FLOAT_BALL 为 system_grant 装机自动授予无弹窗——能力面在,场景面受限,两层分开看。

闪控球与闪控窗的形态差异值得单独一段,因为互斥矩阵的根子在这里:闪控窗是应用内容的悬浮延伸(模板卡承载应用 UI,尺寸上限实测 1134×851px,篇 7/8 的主题);闪控球是系统侧边栏的入口挂件(70-98vp 宽 40vp 高的四模板小球,跨应用常驻,位置记忆+侧边吸附)。一个占的是「内容面」,一个占的是「入口面」,但系统把两者算作同一种资源——前台悬浮名额。1300034 的文案「conflicts with other floating windows」里的 other,指的就是这个名额的另一占用者。理解名额语义后,互斥双向就收敛成一条名额规则的两个方向:谁先占住前台悬浮名额,谁就把后启动者挡在门外。

bind 接口把两种形态接起来:绑定后点窗可收为球、点球可展为窗,状态机里对应 IN_FLOATING_BALL=5 这个态——窗的内容没销毁,只是换了载体。bind 联动的 ok 1ms 读数说明绑定本身极轻,重的是两边的状态同步(这次实录的 1300022/1300025 都出在同步的边上)。

4、逻辑流

冲突判定与降级阶梯的决策流:

0 + STARTED 回调

1300034 冲突

201 无权限

801 不支持

是

否

1300034

请求启动悬浮能力

L1 闪控窗 start

L1 生效

L2 应用内浮层

L3 页面内联

浮层可用?

L2 生效

专注信息内联进页面

球侧请求启动

球降级:不启动,提示入口在主窗

错误码分类记账规则(FallbackLadder):

1300034/1300033

201

801

其他

错误码

分类

CONFLICT 降一级

NO_PERM 降一级+引导授权

UNSUPPORTED 直降 L3

ABNORMAL 降一级+记原文

阶梯状态 + 计数入账

分类的工程理由:冲突与无权限是环境态(换个时机或授权后闪控窗这一级仍可能可用),降一级留回升余地;801 是设备态(换时机也不会变),直降到底不浪费中间层。回升按钮在诊断台上手动触发,产品侧由状态回调驱动。

阶梯的实现核心是一个记账结构而非三层 UI:FallbackLadder 只持有「当前级别 + 四类计数」,三层 UI 各自读级别决定显隐——降级逻辑与展示逻辑分离后,注入错误码做实验(诊断台的四个降级按钮)与真实错误码走的是同一条 classify 函数,实验路径不绕过产品路径。这条设计让注入路径的读数与真冲突路径的读数可以直接并列比较:kind 与 level 的对应关系在两条路径上完全一致,阶梯的分类规则才算被验证过。

应用内浮层与闪控窗的能力差要写给用户看:无权限依赖、退后台即不可见、无系统级置顶。诊断台把这三条直接印在浮层上——降级体验的诚实度决定用户会不会把浮层误当闪控窗投诉。

5、实战:五组实验读数

5.1 互斥双向

a 向:专注窗 STARTED 后 startFloatingBall → 1300034,错误文案 This operation conflicts with other floating windows.[FBWindow][startFloatingBall],耗时 6ms,阶梯从闪控窗降一档(CONFLICT 计数 +1)。

b 向:停窗→启球(12ms 成功)→独立闪控窗 start → 1300034,文案 This operation conflicts with other floating windows.Failed to start float view.。两侧同码不同文案([FBWindow] 球侧 vs float view 窗侧),互斥双向成立——官方那句约束的反向同样为真。

5.2 三级阶梯

注入 1300034 → 闪控窗降应用内浮层(CONFLICT);注入 801 → 应用内浮层降页面内联(UNSUPPORTED);回升按钮尝试重建。阶梯状态行全程可读:L=IN_APP_OVERLAY · CONFLICT 1 → L=INLINE · CONFLICT 1 · UNSUPPORTED 1。浮层与闪控窗的能力差写在界面上:无权限依赖、退后台即不可见——降级不假装自己还是闪控窗,能力缩水直接标在界面上。

5.3 bind 联动与状态类错误码

bind(窗↔球控制器绑定)ok 1ms;随后实录四个状态类错误码:unbind 1300025(球状态不支持此操作)、bindView stop 1300002(窗状态异常)、ball stop 1300022(Repeated,重复停止)、bindView stateChange → 6(ERROR)。这组负结果的价值在触发条件:1300022 只在「对已停止的球再 stop」时出现,1300025 只在「球处于非绑定兼容态时 unbind」出现——状态类错误码是状态机的边守卫,按边读码才不会误判。

5.4 恢复主窗负结果

窗已停止态直调 restoreMainWindow → 1300025。官方四条件(从未点击过闪控窗/闪控窗不在前台/主窗 PAUSED/主窗在后台)之外,这次补一条实测:窗已停止时恢复调用同样被拒——恢复的前提是「有一个可恢复的窗」,状态守卫先于业务条件。

5.5 权限边界

FLOAT_VIEW:requestPermissionsFromUser 弹窗实录 → true(user_grant)。USE_FLOAT_BALL:isFloatingBallEnabled=true、system_grant 装机自动授予无弹窗——但场景受限(六类场景),实测不冒用。两种权限的「拿到」方式与「能用」范围是两回事,权限边界这组把两层分开记。

5.6 画中画互斥

官方说法里 1300034 的完整表述是「已启动闪控球或画中画」,闪控球半边在 5.1 里测过,画中画这半边这次补齐:诊断台加了画中画这组,用 PiPWindow(API 11 起,@kit.ArkUI 导出)承载一个 XComponent 建画中画会话,双向各跑一次。

方向前置被拒接口错误码与文案时延
窗→PiP闪控窗 STARTEDstartPiP()1300034「The window is already started or is about to start.」1ms
PiP→窗PiP STARTED(state=2,130ms 起窗)floatView.start()1300034「与其他悬浮窗冲突:本应用已启动闪控球或画中画窗口」create 成功后 start 即拒

两个发现。其一,互斥在画中画方向同样双向成立,且两侧文案再次不同源:PiP 侧是英文系统文案(d.ts 标注该错误码 since 26.0.0),窗侧是中文业务文案——排障时看文案即可分辨是哪侧拒的,这条规律从闪控球半边延续到画中画半边。其二,画中画这组里窗侧被拒后降级阶梯自动降了一档(FLOAT_VIEW→IN_APP_OVERLAY,kind=CONFLICT),阶梯对「冲突来源」不挑剔:闪控球占的和画中画占的走同一条降档路径,这验证了阶梯分类规则(环境态冲突降一级留回升)的通用性。

画中画生命周期读数一并入档:create 成功(模板缺省=视频播放)→ startPiP resolve 130ms、stateChange 1→2 → stopPiP resolve 2ms、stateChange 3→4。补测现场(PiP 黑块在屏 + 应用内浮层在屏 + 状态行 CONFLICT 1):

HarmonyOS 7 新特性9:闪控窗深潜——闪控球互斥双向实测、三级降级阶梯与两种悬浮权限的边界-3.png

6、避坑

6.1 STARTED 回调滞后 promise,ensure 后立即读相位必错

现象:互斥双向实验第一次跑就报「实验无效:窗未 STARTED(phase=CREATING)」,而 hilog 显示窗早已 STARTED(start promise 21ms resolve,STARTED 回调滞后 741ms)。

根因:篇 7 立过的规矩——start() 的 promise resolve 不代表启动完成,须等 onStateChange 的 STARTED。诊断台那组实验在 ensure() 返回后立即读相位,读到的是回调到达前的 CREATING。

方案对比:A 加轮询等待(waitViewStarted,100ms 间隔 6s 上限);B 改事件驱动(实验方法挂状态回调续跑,代码结构大改)。

最佳实践:实测台选 A(简单可重入),产品代码选 B(回调续跑不占轮询)。

验证:加等待后一次跑通,1300034 实录留档。

6.2 闪控窗会盖住自己的诊断台

现象:互斥双向实验跑完后闪控窗卡片悬在诊断台中部,冲突双向与 bind 联动两行的按钮全部被盖,后续点击全被闪控窗吸收,实验静默无响应——一度误判为按钮坐标漂移。

根因:闪控窗是系统级悬浮窗,z 序高于应用内一切组件;诊断台的按钮区恰在闪控窗默认位置(屏幕中部)覆盖范围内。

方案对比:A 每轮实验前点闪控窗关闭钮(FloatViewRightButtonId,dump 可取稳定 id);B 诊断台布局整体下移避开闪控窗默认区(闪控窗位置可拖拽记忆,避不开)。

最佳实践:选 A,并把「悬浮窗实测先关悬浮窗」写进实测规矩——悬浮窗既是实验对象又是取景障碍物,双重身份要先卸其一。

补测画中画时又撞见同款坑的第二个变体:应用内浮层也会盖按钮。那一跑降级到浮层后,橙色浮层悬在屏幕中部,「停PiP」按钮被盖、点击被浮层吸收,dump 里按钮 bounds 被压到 11px 宽(遮挡的布局学特征)。处置同规矩:先点「回升」卸掉浮层再点目标按钮。教训合并成一句:诊断台上任何悬浮物(系统级闪控窗、应用级浮层)在实测点击前都必须先卸掉,dump bounds 异常变窄是「被盖」的第一信号。

验证:点关闭钮(dump id 定位 1142,757)后按钮恢复响应,后续几组连跑通。

6.3 状态类错误码按状态机的边读

现象:bind 联动一次实录四个错误码(1300022/1300025/1300002/ERROR=6),初看像接口不稳定。

根因:四个码分属四条不同的状态边:重复停止、非兼容态解绑、异常态停止、绑定后窗侧进入 ERROR。诊断台的停净前置(prepareClean)在球已 STOPPED 时再 stop,必然撞 1300022——是实验前置自己制造的边,不是接口抖动。

最佳实践:状态类错误码入文时带「从哪个状态到哪条边」的上下文,裸码表会误导读者以为码是随机出现的。

验证:四条边逐条对上状态机迁移表,无一条是未预期边。

7、总结,以及这次没测到的部分

冲突治理的三个答案:互斥双向成立且两侧文案不同源(排障时看文案后缀能分辨是哪侧拒的——闪控球与画中画两个方向都验证过这条规律);降级阶梯按错误码的环境态/设备态分类降档,冲突留回升、不支持直降到底,且阶梯对冲突来源不挑剔(球占的和画中画占的走同一条降档路径);两种悬浮权限的边界在「拿到」与「能用」两层,误声明 SYSTEM_FLOAT_WINDOW 的代价是安装失败而非运行报错——后者能在测试里发现,前者直接到不了测试。

这次没测到的部分,列在这里:

  • 闪控球场景受限(六类场景),球侧实验只读接口行为不冒用业务场景,球的产品跑通未实测
  • IN_FLOATING_BALL=5 状态需手动点窗收球触发,自动化注入未覆盖该手势,状态读数留人工复核
  • 三级阶梯的后两级为注入错误码驱动(真冲突路径只到应用内浮层),页面内联的真实触发条件(801 设备)本机不可达
  • 闪控窗遮挡诊断台的实测坑为 Mate 60 Pro 单机型观察(1260×2720 下闪控窗默认位置恰盖按钮区),其他分辨率设备位置不同,坑的形态会变但「悬浮窗既是对象又是障碍物」的双重身份不变

悬浮能力的工程难点从来不在「悬浮」本身,在「和谁一起悬浮」。互斥矩阵、降级阶梯、权限边界三张表画清楚,剩下的才是代码——而三张表的每一格,都该有真机读数或官方原文撑着。

在这里插入图片描述

在这里插入图片描述

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐