跨应用跳转是鸿蒙开发里一个看起来简单、实际很容易出错的地方。文档给了 startAbility(want) 三行示例,但真实项目里,一半的失败是静默的——不报错,界面也不动。
这篇是一次真实排错的记录。四个错误,每个都花了时间才定位到。
Want 分两种:
显式 Want:指定 bundleName + abilityName,直接指向一个确定的 Ability。
隐式 Want:只给 action、entities、parameters,由系统匹配能处理的应用。
问题出在混写。我最初的代码是这样:
const want: Want = {
bundleName: 'com.example.target',
action: 'ohos.want.action.viewData',
parameters: { url: 'https://example.com/item/123' }
}
await context.startAbility(want)有 bundleName 又有 action。这种情况下系统会怎么处理?
实际行为是:bundleName 让匹配范围收窄到那一个应用,action 在这个应用内部再筛一次。如果目标应用的 entry Ability 没在 module.json5 里声明这个 action,匹配失败——但不抛异常。
我用 try/catch 包着,catch 里什么都没打印,所以看起来「跳过去没反应」。
定位方法:在 startAbility 后面加 .then(() => console.info('ok')).catch(e => console.error(JSON.stringify(e)))。这一加就看到了 16000001(无法找到匹配的 Ability)。
结论:跨应用跳转优先用显式 Want(写全 bundleName + abilityName),隐式 Want 只在「不确定谁处理」时用。混写是给自己挖坑。
显式 Want 写全了,还是失败。错误码变成 16000019。
这个码的意思是「目标 Ability 未被导出」。
鸿蒙的 Ability 默认是 exported: false,也就是别的应用调不起来。要在 module.json5 里显式声明:
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"exported": true,
"skills": [
{
"actions": ["ohos.want.action.viewData"],
"uris": [{ "scheme": "https", "host": "example.com" }]
}
]
}这里有个细节:exported: true 是必要条件,但不是充分条件。如果目标应用声明了 skills,那么你的 Want 里的 action 和 uri 还要能匹配上它的 skills 列表。
我第一次只加了 exported,skills 没动(是空数组),结果隐式匹配还是不过。加上 skills 之后才通。
这里有个安全上的常识值得说:exported: true 会让任何应用都能调起这个 Ability。如果不打算对外提供服务,别开。如果需要对外提供但只给特定应用,用权限校验(verifyCallingPermission)。
跳转通了,但目标页拿到的 parameters 是空的。
原因在我的参数里放了一个不可序列化的对象:
parameters: {
item: this.itemObject, // 一个带方法的 class 实例
userId: '123'
}Want 的 parameters 只能放基础类型、数组、对象字面量(可 JSON 序列化的东西)。放 class 实例的话,静默丢弃——不报错,到了目标页是 undefined。
而且是一整个 parameters 都受影响吗?实测是逐键处理:不可序列化的那个键丢了,其他键还在。所以我的 userId 是好的,只有 item 丢了,这也让问题更难发现。
改法:只传 ID,目标页自己查。
parameters: { itemId: this.itemObject.id }这条规则值得记牢:Want 的参数是跨进程传递的,本质上是序列化再反序列化。任何依赖引用或方法的对象都过不去。
单向跳转通了,返回时出问题:按返回键,回到了桌面而不是原来的应用。
原因是启动模式。
目标 Ability 如果在 module.json5 里配的是 launchType: "singleton",并且之前已经启动过,那么新的 startAbility 不会创建新实例,而是复用已有实例并把任务栈提到前台。这种情况下,返回键的行为取决于任务栈的组织方式。
如果希望「跳过去、办完事、回来」,正确做法是用 startAbilityForResult 而不是 startAbility:
const result = await context.startAbilityForResult(want)
if (result.resultCode === 0) {
// 目标页通过 terminateSelfWithResult 返回数据
const data = result.want?.parameters
}目标页办完事后调 terminateSelfWithResult({ resultCode: 0, want: {...} }),源页面在 await 处拿到结果,任务栈自然回退。
两条路的选择依据:需要目标页返回数据 → startAbilityForResult;只是跳过去看看 → startAbility + 目标页正常返回。
回头看,这四个问题有一个共同特征:三个是静默失败。
parameters 带 class 实例:不报错,只是丢键只有「未导出」那个给了明确错误码。
这也是跨应用跳转难排的原因——它跨了进程边界,源应用这边看不到目标应用的状态,框架只能给你一个数字。
排错的方法论:把错误码打出来。startAbility 的 catch 里必须 JSON.stringify(e),因为 BusinessError 的 code 和 message 分两个字段,直接 console.error(e) 打出来是 [object Object]。
我最初就是栽在这——catch(e) { } 空着,等于把所有信息扔了。
三处。
第一处:错误码没有对照表在手边。 16000001 是找不到 Ability、16000019 是未导出、16000011 是权限不足。这些码在文档里能查到,但排错时人在 IDE 里,不会想到去翻。
第二处:module.json5 改了不生效。 这个配置文件的改动要重新安装应用(不只是热重载)。我第一次改完 exported 直接跑,还是失败,以为是配置写错了,其实装的是旧包。
第三处:跨进程调试看不到对面。 目标应用如果崩了,源应用这边只拿到一个通用错误码。要看对面日志,得同时开两个设备的日志流,或者用 hdc shell hilog 按 bundleName 过滤。
这三处都不是知识问题,是环境问题——知道该怎么看,比知道该怎么写更重要。
如果你想去学习跨进程这块的调试顺序可以看看誉天教育。它的鸿蒙方向一般是先教怎么看日志流,再讲接口怎么写。这个顺序对,因为跨进程的坑几乎全靠日志定位。
Want 本身不难,难在它的失败方式是静默的。
记住四条:显式 Want 写全两个字段;目标 Ability 要 exported + skills;参数只传可序列化的;要返回数据用 startAbilityForResult。
再加一条:catch 里必须打印 JSON.stringify(e)。
这一条能省掉本文前三个坑的一半排查时间。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。