组网成功不等于设备可达,软总线的发现窗口只有两分钟
一个跨设备相册投屏功能,两台设备同一账号、同一 Wi-Fi,手动检查蓝牙和分布式开关都开着。你在办公室里演示,发现设备的成功率大概十次里有两次,失败时discoverSuccess不触发,discoverFailure也不给一个明确的 reason,日志里就是安静地什么都没有。重启蓝牙、退出应用重进,有时候能好,有时候还是不行。你开始怀疑软总线是不是只适合做 Demo。
问题不在软总线本身,在于你把“组网成功”当成了“设备一定可达”,又把“调了startDiscovering”当成了“发现会一直进行下去”。鸿蒙的分布式设备发现是一组有时限、有前提条件、由系统功耗策略参与调度的短窗口动作,不是一条常驻的扫描通道。这些前提中任何一个不满足,发现就静默地不工作,不给你一个可以直接定位的异常。
组网成功只是一张入场券,AP 隔离和频段分裂在二层就已经把人拦住了
distributedDeviceManager的发现依赖两台设备处于同一个可信局域网,并且能互相直连通信。但“连在同一个路由器上”和“二层可达”是两个概念。公司办公网常见的 AP 隔离,访客 Wi-Fi 和办公 Wi-Fi 之间默认不通;家用双频路由器开了“智能频段分配”,一台设备连 5GHz、另一台连 2.4GHz,部分路由器同样会隔离这两个频段。在这种情况下,两台设备都显示“已连接 Wi-Fi”,但组网在二层就已经失败了,设备发现自然拿不到对方。
排查时不要把“同 Wi-Fi”当成一个已经解决的问题。让两台设备连同一个 SSID、同一个频段(优先 5GHz),关掉路由器的 AP 隔离和“双频优选”,再试一次。如果发现恢复了,问题从来不在代码里。
锁屏和灭屏会直接切断发现链路,亮屏不是可选项
另一个比 Wi-Fi 更隐蔽的条件是屏幕状态。分布式设备管理的发布和发现依赖屏幕事件以及 Wi-Fi/蓝牙状态,任一端灭屏或者 Wi-Fi/蓝牙关闭,都可能导致发现或发布功能失败。官方问答里把条件列得更完整:双端同账号、可信组网、蓝牙和 Wi-Fi 开启、设备处于亮屏非锁屏状态、位置信息开关开启、未被省电模式限制后台活动。
这意味着你在“手机锁屏放口袋里,平板拿在手上”的场景下做发现,大概率是零结果。不是软总线坏了,是扫描链路被屏幕状态抑制了。发现期间两台设备都需要保持亮屏,这不是一个可以绕过的优化项,是发现能工作的前提。如果业务场景确实需要在暗屏或后台完成发现,得先接受“自动发现可能不触发”这个事实,用碰一碰、扫码或者用户主动触发作为替代入口,而不是赌系统会在省电策略的缝隙里给你留一条扫描通道。
发现窗口只有两分钟,过了窗口不会续,业务层得自己重启
startDiscovering的文档写得很直接:发现状态持续两分钟,超过两分钟会停止发现,最大发现数量 99 个。两分钟之后,即使discoverSuccess没有触发过任何一次,发现也不会自动续期。你在两分零一秒的时候去看设备列表,它就是空的,而startDiscovering当初的调用早已返回,没有任何回调告诉你“窗口已经关了”。
很多“时灵时不灵”的现场,本质上是用户在两分钟窗口之外才去看列表。正确的结构不是调一次startDiscovering然后指望它一直有效,而是把发现当成一个有明确生命周期的短任务:进入发现页时启动,窗口到期前如果还没有结果,要么重新startDiscovering续一个窗口,要么用getAvailableDeviceList()查一次已信任的在线设备作为兜底。已信任设备不走主动发现,它的可用性由组网状态决定,不需要你反复开扫描窗口。
typescript
const DISCOVERY_WINDOW_MS = 2 * 60 * 1000;
async function discoverDevices(dm: distributedDeviceManager.DeviceManager): Promise<void> {
dm.on('discoverSuccess', (data) => { /* 拿到设备,处理 */ });
dm.on('discoverFailure', (data) => { /* reason 可能不具体,记日志 */ });
dm.startDiscovering({ /* 过滤参数 */ });
// 两分钟窗口到期,主动重开,不要让发现悬着
this.discoveryTimer = setTimeout(() => {
dm.stopDiscovering();
dm.startDiscovering({ /* 过滤参数 */ });
// 递归或重新调度,窗口是短任务,不是常驻扫描
}, DISCOVERY_WINDOW_MS);
}
startDiscovering的过滤参数不是装饰性的。publish/subscribe的发现参数可以按服务类型和范围过滤,把不相关的广播滤掉,能减少发现延迟。业务上不需要的发现包,不要让它们进入你的回调。
重试要有上限,权限拒绝和接口不支持不在重试范围内
发现失败之后,一个很自然的动作是重试。但重试的边界需要区分清楚:弱网、设备休眠、临时不可达可以重试;权限拒绝、接口不支持、设备能力不满足重试多少次都不会变成成功。官方建议使用限次指数退避,比如 1、2、4、8 秒加随机抖动,并设置最大次数,不要无限重试。权限拒绝类的错误直接上报给用户,不要把它包在一个退避循环里假装“还在努力”。
DISTRIBUTED_DATASYNC权限是user_grant,只在module.json5里声明不够,运行时需要requestPermissionsFromUser。发现设备还依赖位置信息开关,因为蓝牙扫描在系统中要求定位权限配合。这些条件不满足时,discoverSuccess不会触发,discoverFailure的 reason 也不一定直白指向权限。排查时先把“同账号、同 Wi-Fi 同频段、亮屏、蓝牙 Wi-Fi 位置信息都开、DISTRIBUTED_DATASYNC已授权”过一遍,再去看代码里的发现逻辑。
typescript
async function retryDiscover(dm: distributedDeviceManager.DeviceManager, attempt: number = 0): Promise<void> {
const MAX_ATTEMPTS = 4;
if (attempt >= MAX_ATTEMPTS) return;
try {
await dm.startDiscovering({ /* 过滤参数 */ });
return; // 启动成功,等待 discoverSuccess
} catch (err) {
const e = err as BusinessError;
if (e.code === 201 || e.code === 401) {
// 权限类错误,重试没有意义,直接暴露给用户
throw err;
}
const delay = Math.min(1000 * Math.pow(2, attempt), 8000) + Math.random() * 500;
await new Promise((resolve) => setTimeout(resolve, delay));
return retryDiscover(dm, attempt + 1);
}
}
201和401这类错误码不应该进入退避循环。它们代表的是“当前条件下这件事根本不被允许”,重试只会让用户多等几秒,然后拿到同一个失败。
发现之后建连是另一条链路,超时不等于网络问题
discoverSuccess拿到设备只是第一步。建连和调用走的是另一条链路,这条链路上“超时”并不自动等价于“网络差”。官方排查建议里区分得很清楚:如果连上后调用超时,先看服务端有没有收到请求;收到了却没回复,优先查业务阻塞、异常和序列化,只有同时发生链路断开,才去查弱网和网络切换。把调用超时一律归因于网络,会把一个服务端阻塞的 bug 当成基础设施问题去修。
建连顺序也有硬性的先后关系:申请权限 → 发现设备 → 绑定可信关系 → 建连 → 调用。中间任何一步不成立都不要继续往下调,否则看到的就是随机超时。你的代码如果在bindTarget还没确认成功的时候就发起startAbilityByCall,拿到的超时和“网络差”长得一模一样,但根因是顺序跳步了。
业务层要自己带状态机,软总线只提供通信和发现原语
鸿蒙没有统一的“分布式任务调度加重试”框架,系统底层提供的是跨设备通信和设备发现能力,业务级的可靠性需要自己实现。这意味着发现、建连、调用、重试、幂等,这整条链路的正确性不能寄生在软总线的回调上,它需要一层显式的业务状态机来承载。
一个务实的最小结构是给每次跨设备操作分配taskId + stepId + operationId,每一步只在“业务执行成功且检查点写入成功”之后才提交下一状态;远端未确认时按幂等键重试,外部副作用需要补偿或撤销。发现超时、建连失败、调用无响应,各自对应状态机里的一个可重试步骤,而不是一个散落在各处的catch里随手setTimeout。
typescript
interface CrossDeviceStep {
taskId: string;
stepId: string;
operationId: string; // 幂等键,重复提交用同一值
state: 'discovered' | 'bound' | 'connected' | 'called' | 'done';
}
operationId在重试时必须保持不变。用Date.now()或随机数每次生成新的operationId,对端无法区分“这是重试”还是“这是一个新操作”,幂等就失去了意义。
排查顺序:先过环境清单,再看发现窗口,最后看建连顺序和幂等
下次“十次有八次发现不到设备”的时候,按这个顺序走,比反复改代码快。第一步,过一遍环境清单:同账号、同 SSID 同频段、双端亮屏、蓝牙 Wi-Fi 位置信息开启、省电模式关闭、DISTRIBUTED_DATASYNC已运行时授权。第二步,确认startDiscovering调用时距离上一次启动有没有超过两分钟,超过就重新开窗口,不要指望它自己续。第三步,确认建连顺序没有跳步,bindTarget成功之后再发起调用。第四步,如果调用超时,先查服务端日志看请求有没有到达,再决定是查网络还是查业务阻塞。第五步,把重试收进限次退避,权限类错误不重试,operationId在重试中保持不变。
软总线的发现不是一个“调了就在跑”的常驻服务,它是一个有两分钟时限、依赖亮屏和组网可达、受功耗策略影响的短窗口动作。组网成功是入场券,不是可达证明;锁屏不是“可能影响”,是直接切断发现链路;两分钟窗口过了不会续,业务层得自己重启或者退回已信任设备列表。下一次设备列表空空如也的时候,先别怀疑软总线只配做 Demo——把环境清单和发现窗口各过一遍,答案通常在这两件事里,而不是在startDiscovering的参数上。

更多推荐

所有评论(0)