ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

HarmonyOS 7 NearLink:星闪广播advertisingId串线与会话收口

HarmonyOS 7 NearLink:星闪广播advertisingId串线与会话收口 一、那次“轮换成功”其实停掉了新广播PulseBeacon 是一个展馆导览侧的小工具手机持续发送星闪广播展项接收端只认当前 30 秒时隙里的短令牌。页面叫AdvertiseControlPage本轮任务 ID 是NL-1118。最初的 Demo 只有两个按钮——开始、停止——连续运行没有问题接入定时轮换后HiLog 偶尔出现“epoch5 stop id42”而界面已经显示 epoch6、advertisingId42。更麻烦的是下一行又说广播停止成功接收端从此收不到包。我先怀疑 payload 编码再怀疑星闪开关最后才发现真正的问题startAdvertising()返回的是异步产生的advertisingId而“停止旧广播、启动新广播、状态回调到达”是三条不同时间线。只用一个全局advertisingId旧轮换的尾部就有机会读到新轮换刚写入的值。代码语义看起来是“停止旧句柄”运行时却可能变成“拿最新句柄替旧会话收尾”。二、先把工程问题钉在时间线上官方在 2026 年 9 月 9 日更新的星闪广播指南里给出了四个关键接口startAdvertising()返回PromisenumberstopAdvertising(advertisingId)负责停止指定广播状态通过advertisingStateChange的on/off管理。这个 API 形态决定了两个事实句柄不是调用开始时就存在回调也不天然属于页面眼中的“当前任务”。当轮换请求来自三个入口——30 秒定时器、运维按钮、应用回前台后的补偿——旧实现会出现这样的交错epoch5 请求停止 id41前台补偿又触发一次启动epoch6 得到 id42epoch5 的异步尾部读取成员变量此时它已经是 42stopAdvertising(42)成功UI 还停留在“广播中”。这里不能靠多加一个布尔值解决。isStarting只能挡住同时启动挡不住旧 Promise、旧回调和新会话交叉。最终我把状态机收敛成IDLE → STARTING → ADVERTISING → ROTATING → ADVERTISING → STOPPED每次真正启动递增epoch每个异步分支只使用自己捕获的局部句柄。三、项目结构不大但所有权必须单一工程名是PulseBeaconAPI 版本为 26。和广播相关的文件被刻意压到四个页面不再直接调用 NearLink Kitentry/src/main/ets/ ├── pages/AdvertiseControlPage.ets ├── nearlink/NearLinkAdvertiserSession.ets ├── nearlink/PayloadCodec.ets └── model/AdvertiseSnapshot.etsPayloadCodec只负责把NL-1118、时隙和校验字节编码进serviceDataNearLinkAdvertiserSession独占advertisingId、状态回调和轮换串行链页面只订阅快照。这样做的价值不在“分层好看”而在于只有一个对象有资格执行stopAdvertising()也只有它知道某个 id 属于哪一代。四、第一段代码把参数构造变成纯函数轮换时最怕参数一边被改、一边还在异步启动。下面的构造函数每次都创建新的Uint8Array和参数对象调用者拿到后不再修改。Demo 固定使用服务 UUIDFFFFFFFF-1234-5678-ABCD-000000001118广播间隔 160、功率为中档最终界面显示的 payload 哈希A83F09C2也来自同一份字节序列。import{advertising}fromkit.NearLinkKit;constSERVICE_UUIDFFFFFFFF-1234-5678-ABCD-000000001118;exportfunctionbuildParams(slot:number):advertising.AdvertisingParams{constbytesnewUint8Array([0x4E,0x4C,0x11,0x18,(slot8)0xFF,slot0xFF,0xA8,0x3F]);constsettings:advertising.AdvertisingSettings{interval:160,power:advertising.TxPowerMode.ADV_TX_POWER_MEDIUM};constdata:advertising.AdvertisingData{serviceUuids:[SERVICE_UUID],serviceData:[{serviceUuid:SERVICE_UUID,serviceData:bytes.buffer}],includeDeviceName:false};return{advertisingSettings:settings,advertisingData:data};}纯函数解决的是“参数归属”并不解决“会话归属”。ArrayBuffer在这里由新建数组提供调用结束后不保留可变引用如果以后把编码搬到 Worker传输前还要确认是否发生所有权转移。另一个边界是广播包容量任务 ID、时隙和摘要应该放紧凑字段不能把 JSON 字符串原样塞进去。重复调用buildParams()没有副作用所以定时器合并前后都安全。五、第二段代码句柄和代次一起捕获核心修正放在NearLinkAdvertiserSession。所有轮换请求先追加到flight形成单通道开始时捕获myEpoch停止时捕获oldId。成员变量只表达“当前快照”不再充当异步操作的输入。import{advertising}fromkit.NearLinkKit;import{hilog}fromkit.PerformanceAnalysisKit;exportclassNearLinkAdvertiserSession{privateepoch:number0;privateadvertisingId:number-1;privateflight:PromisevoidPromise.resolve();privatedisposed:booleanfalse;rotatePayload(slot:number):Promisevoid{this.flightthis.flight.then(async(){if(this.disposed)return;constmyEpochthis.epoch;constoldIdthis.advertisingId;if(oldId0){hilog.info(0x1118,PulseBeacon,epoch${myEpoch}stop oldId${oldId});awaitadvertising.stopAdvertising(oldId);if(myEpoch!this.epoch||this.disposed)return;}constnewIdawaitadvertising.startAdvertising(buildParams(slot));if(myEpoch!this.epoch||this.disposed){awaitadvertising.stopAdvertising(newId);return;}this.advertisingIdnewId;hilog.info(0x1118,PulseBeacon,taskNL-1118 epoch${myEpoch}advertisingId${newId}stateADVERTISING);});returnthis.flight;}}关键点是oldId和newId都是局部常量。旧分支即使晚回来也只能停止自己拿到的newId不能碰后来一代写入的成员值。这里把代次检查放在await之后因为异步空档正是状态变化发生的地方。若stopAdvertising(oldId)抛出 36100099串行链应在上层记录并决定是否重试不能吞错后直接启动否则设备侧可能同时看到两份载荷。串行链也有一个常见陷阱如果前一次 Promise 失败后续.then()不会再执行。因此生产版在追加新任务前会用.catch()把失败转成已记录的完成态并把 UI 状态置为ERROR。Demo 的最终统计错误数是 0但故障通道仍然存在而不是为了截图把异常分支删掉。六、第三段代码回调只注册一次页面退出时完整释放状态回调不是轮换回调。它描述系统广播能力的变化应该在会话对象生命周期内只注册一次。页面每次点击都on()会让一次系统事件被处理多次只写off(advertisingStateChange)又可能误伤同模块其他订阅者。这里保留稳定函数引用并在dispose()中先让代次失效再停止当前句柄最后精确取消订阅。privatereadonlyonStateChange(info:advertising.AdvertisingStateChangeInfo):void{if(this.disposed)return;hilog.info(0x1118,PulseBeacon,taskNL-1118 epoch${this.epoch}systemState${info.state});};startSession():void{advertising.on(advertisingStateChange,this.onStateChange);this.rotatePayload(0x1118);}asyncstopSession():Promisevoid{constoldIdthis.advertisingId;this.advertisingId-1;this.epoch;if(oldId0)awaitadvertising.stopAdvertising(oldId);}asyncdispose():Promisevoid{if(this.disposed)return;this.disposedtrue;awaitthis.stopSession().catch((){});advertising.off(advertisingStateChange,this.onStateChange);}dispose()的顺序不能颠倒先标记失效能阻止正在返回的启动分支把状态写回再停止句柄避免页面消失后仍发射最后off()因为停止过程本身仍可能产生一次状态变化。AdvertiseControlPage.aboutToDisappear()调用它同时清掉 30 秒轮换定时器。重复释放通过disposed幂等化避免导航返回栈反复触发时出现二次停止。调试时我专门把日志格式固定为task / epoch / advertisingId / state而不是只写“start success”。当日志从epoch5 oldId41进入epoch6 advertisingId42后任何 epoch5 的晚回调都会被计入staleDrops不再改变 UI。IDE 中最终可见的关键行是taskNL-1118 epoch6 advertisingId42 stateADVERTISING。七、把三类触发源压成一次轮换单通道能保证顺序却不能阻止队列堆积。回前台、手动按钮和定时器可能在几百毫秒内各提交一次轮换如果全部执行接收端会看到毫无价值的短命广播。我在入口加了“最新时隙覆盖”队列执行前只保留最后一个 slot中间请求计入coalesced。本轮压力测试触发 14 次意图实际完成 12 次轮换合并 2 次。另一条边界是系统星闪关闭。错误 36100003 不适合指数重试因为能力没有恢复前重试只会耗电UI 应进入“等待系统开启”监听能力恢复后由用户明确重启。36100099 才进入有限重试最大两次并且每次仍使用新的 epoch。权限 201 则直接提示授权不把它伪装成网络波动。八、结果不是“没崩”而是每一代都能解释在真机连续运行 6 分钟PulseBeacon 完成 12 次轮换当前 advertisingId 为 42epoch 为 6payload 哈希为A83F09C2。两次重叠请求被合并我注入的一次旧状态回调被计数并丢弃错误数为 0。更重要的是停止页后没有继续发包重新进入页面会创建新会话而不是复用已释放回调。手机页在 11:18 显示任务NL-1118、状态“广播中”、轮换周期 30 秒、轮换 12 次、合并 2 次、旧回调丢弃 1 次。这里的数字与 HiLog 是同一份AdvertiseSnapshot没有为了展示再维护一套字段。九、最后留下的工程判断星闪广播轮换看起来只是“停一下再开一下”真正难点却是异步句柄的所有权。只要一个 API 返回可用于后续停止、取消或关闭的 id就应把 id 与产生它的会话代次绑定只要有全局状态回调就应使用稳定引用并明确注册、注销边界。这个方案也不是无限扩张的通用框架。PulseBeacon 只有单广播通道所以 Promise 串行链足够如果产品需要并发多个广播应按业务 key 维护多个独立 session而不是让所有任务共享一个 epoch。另一方面30 秒轮换服务于短令牌不适用于要求广播持续稳定的设备发现页。技术正确之外还要让状态机与业务语义一致。参考资料华为开发者文档《发送星闪广播》更新于 2026-09-09https://developer.huawei.com/consumer/cn/doc/HarmonyOS-Guides/nearlink-send-advertising
返回列表