做鸿蒙开发的朋友,应该都遇到过让我到现在还记得的场景:用户明明在系统分享面板里点了你的应用,结果它跟没事人一样,先走一遍启动页,再打开首页,用户还得自己找到“接收”的入口。问题不在分享流程,而在于你的应用压根没有意识到自己是被系统分享拉起来的。这个判断逻辑,是我做 Share Kit 系列时踩得最深的一次坑,把这部分单独拿出来写一篇,就成了系列的第 12 篇。这篇把系统分享拉起的原理、判断步骤、常见坑一次性说清楚,适合正在接分享能力、或者只做完“分享出去”还没处理“分享进来”的鸿蒙开发者参考。
1. 为什么要判断“被系统分享拉起”
1.1 应用入口远比你以为的多
一个正常的鸿蒙应用,入口绝对不止“桌面图标点击”这一条。深链(Deep Link)、服务卡片、通知点击、应用内互相跳转、系统分享面板,都是常见入口。每一条入口带过来的 Want 都不一样:桌面点击时 action 通常是ohos.want.action.home或者干脆没有显式 action;深链带的是你自己注册的 scheme;通知点击会带通知相关参数;而系统分享拉起时,action 一般是ohos.want.action.viewData,uri 走的是systemshare协议。
如果不在入口层做一次分流,把所有入口都当普通启动处理,就会出现开头说的体验:用户在图库里选了一张照片点分享,点进你的应用,看到的是首页,而不是自动进入接收/编辑流程。用户要自己在应用里找“导入”“接收”之类的功能,多走两三步,流失率立刻上来了。
1.2 判断清楚之后能换来什么
把“被系统分享拉起”判断出来,业务上的收益非常直接:
- 用户从分享面板触发后,直接进入内容处理页,而不是应用首页;
- 分享的内容(文本、图片、文件路径等)可以自动带入页面,省去手动粘贴或选择的步骤;
- 可以统计分享来源渠道和数据类型,为后续产品优化做数据支撑;
- 可以避开普通启动时那些不必要的流程,比如登录引导、新手页、版本检测。
举个例子:我做过一个类似“稍后读”的轻量应用,系统分享文本进来时,判断命中之后直接拉起新建笔记页,文本已经填在输入框里,用户只需要点保存。这个流程要是靠用户手动复制粘贴,基本上没人会用第二次。
2. 系统分享拉起的原理与关键参数
2.1 一次系统分享背后发生了什么
分享源(比如图库、文件管理、浏览器)调用系统分享面板,面板会扫描当前设备上所有声明了对应能力的应用,把符合条件的列出来。用户点击某个应用后,系统不会直接把文件“裸传”给你,而是构造一个标准 Want,把分享源和接收方之间需要传递的信息都塞进去,然后以拉起 Ability 的方式唤醒你的应用。
这个唤醒机制和 Deep Link 本质上是同一套东西,区别只在于 action、uri 的“约定”不同。所以判断的核心,就是解析这个 Want。
关键字段我整理成了表格,实际操作时可以先对着表核对:
| Want 字段 | 典型值 | 作用 |
|---|---|---|
| action | ohos.want.action.viewData | 标准数据查看动作,系统分享接收统一用它 |
| uri | systemshare://data/...这类 | 分享内容的通道地址,注意它不一定是真正的文件路径 |
| parameters | mimeType、extra、来源标记等 | 补充内容类型、附加数据和来源信息 |
| entities | entity.system.home | 参与系统入口发现,分享面板扫描也依赖它 |
这里有个我实际踩过的兼容细节:action 一定要写全,大小写也不能错。另外如果你是从老工程迁移过来的,早期版本里偶尔会见到ohos.want.action.sendData这种写法,判断时可以把两个都兼容掉——到底接的是哪种,抓一次日志立刻就能看出来。
2.2 冷启动与热启动,判断逻辑必须兼顾两条路
Stage 模型下,Ability 被拉起时如果进程不存在,会走onCreate;如果进程还在,且 Ability 实例是复用模式(默认的 singleton 单实例),则会走onNewWant。
从分享面板拉起应用时,你的应用大概率不是冷启动——很可能用户刚看完一个东西切到图库去分享,你的应用还挂在后台。所以只写onCreate,冷启动没问题,热启动时分享数据就会在onNewWant里等你,而你没处理,等于白接。反过来只处理onNewWant,冷启动又收不到。
这个细节决定了代码结构:两条生命周期回调必须都接上,走同一套解析逻辑。另外冷启动时 UI 还没创建完成,不要在onCreate里直接跳路由,稳妥的做法是先把解析结果存下来,等onWindowStageCreate里loadContent完成后再分发。
3. 实操:把“分享入口判断”完整落地
3.1 模块声明:让系统分享面板先能看到你
判断逻辑能不能触发,前提是系统分享面板愿意把你的应用列出来。这一步在module.json5里声明 ability 的 skills,核心配置如下:
{ "skills": [ { "entities": ["entity.system.home"], "actions": ["ohos.want.action.viewData"], "uris": [ { "scheme": "systemshare", "type": "text/plain" }, { "scheme": "systemshare", "type": "image/*" } ] } ] }几个关键点:
actions里的ohos.want.action.viewData是系统分享接收方的标准动作,别写错;uris用来收窄可接收的内容类型。只声明text/plain,用户分享图片就找不到你;声明成*/*确实什么都能收到,但副作用是你会在很多无关场景成为候选,后续判断成本更高;entities里的entity.system.home是系统级入口发现的基础,建议保留;- 改完配置必须重新打包、签名、安装,DevEco 的热加载不一定能立刻让系统分享面板感知到变化。
这里对uris的写法多说一句:不同版本对path和type组合的限制略有差异,我建议先按“scheme + type”这一组最简配置跑通,再按业务需要扩展多组 uri,不要一上来就写复杂的 path 正则,排查起来会很痛苦。
3.2 UIAbility 里统一接管 onCreate 和 onNewWant
入口 Ability 的代码结构我推荐这样组织:两个生命周期回调都只做一件事——把 Want 交给统一的解析入口,业务逻辑全部收敛在一个工具类里,方便后续加日志、加埋点。
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'; import { hilog } from '@kit.PerformanceAnalysisKit'; const TAG = 'ShareDemo'; const DOMAIN = 0x0001; export default class EntryAbility extends UIAbility { onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void { hilog.info(DOMAIN, TAG, 'onCreate want: %{public}s', JSON.stringify(want)); // 冷启动时 UI 还没准备好,先把 Want 存起来 ShareEntryHelper.pendingWant = want; } onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void { hilog.info(DOMAIN, TAG, 'onNewWant want: %{public}s', JSON.stringify(want)); // 热启动时 UI 已经在,直接分发 ShareEntryHelper.dispatch(want); } }工具类的解析逻辑,核心是三层过滤:先看 action,再看 uri 的 scheme,最后才取 parameters 里的补充信息。
export class ShareEntryHelper { static pendingWant: Want | null = null; static readonly SHARE_ACTION: string = 'ohos.want.action.viewData'; static readonly LEGACY_ACTION: string = 'ohos.want.action.sendData'; static readonly SHARE_SCHEME: string = 'systemshare'; static dispatch(want: Want): void { const shareInfo = ShareEntryHelper.parse(want); if (shareInfo) { // 分享入口:跳转到接收页 AppRouter.toShareReceiver(shareInfo); } else { // 普通启动:走原有逻辑 AppRouter.toHome(); } } static parse(want: Want): ShareInfo | null { if (want.action !== ShareEntryHelper.SHARE_ACTION && want.action !== ShareEntryHelper.LEGACY_ACTION) { return null; } const uriStr = want.uri; if (!uriStr || !uriStr.startsWith(`${ShareEntryHelper.SHARE_SCHEME}://`)) { return null; } const params = want.parameters as Record<string, Object>; return { uri: uriStr, mimeType: params?.['mimeType'] as string, extra: params?.['extra'] as string, }; } }为什么非要三层过滤,而不是只看 uri?因为只看 uri 的话,任何一个带systemshare前缀的深链都可能误判成分享;而只看 action,又区分不了“查看数据”和真正来自分享面板的请求。action 决定入口类型,uri 决定分享协议,parameters 决定内容细节,三层都过一遍,误判率才会低。
3.3 跳转发散前的页面栈保护
系统分享拉起时,用户当前到底停在哪个页面是不确定的,尤其热启动场景。所以跳转之前必须先检查页面栈,避免同一个接收页被反复 push 出多层来。
用router的场景,可以参考下面这个写法:
import { router } from '@kit.ArkUI'; export class AppRouter { static toShareReceiver(info: ShareInfo): void { const stack = router.getState().pageStack; const pageName = 'pages/ShareReceiverPage'; if (stack.some(page => page.name === pageName)) { // 页面已经在栈里,只更新数据,不再新开 return; } router.pushUrl({ url: pageName, params: { shareInfo: info } }); } }如果你用的是 Navigation 组件,思路一样:在pathStack里先查找目标页面,存在就更新参数,不存在才pushPath。另外冷启动时onCreate里存的pendingWant,最好在onWindowStageCreate的loadContent回调里再统一分发,此时路由容器已就绪,不会出现“跳转时目标页面还没注册”的报错。
4. 常见问题与排查实录
4.1 分享面板里怎么都看不到你的应用
这是接入过程里最高频的问题。我从自己的排查经验里整理了一张速查表:
| 症状 | 可能原因 | 解决办法 |
|---|---|---|
| 分享面板完全不出现你的应用 | skills 配置没生效 | 重新打包签名安装,确认 action 为 viewData |
| 只收得到文本,收不到图片 | uris 的 type 声明太窄 | 增加image/*或按需扩展 type |
| 配置看起来没问题,但就是不匹配 | entities 漏了entity.system.home | 补上这条声明 |
| 老工程迁移后不生效 | action 还停留在sendData | 两个 action 都兼容,抓日志确认实际值 |
| 部分 uri 能收到,部分收不到 | path/type 组合过严 | 先用最简scheme + type组合跑通再细化 |
排查时可以用bm dump -n <bundleName>查看包内模块信息,确认 skills 是否真的注册进去了。不要凭印象猜,直接看系统侧看到的配置。
4.2 onCreate 能进来,但拿不到分享内容
这类问题我遇到过很多次,原因也五花八门。最常见的是parameters里拿不到你以为的字段。分享文本时,内容有时在extra里;分享文件时,数据又可能在 uri 指向的对象上,需要按 Share Kit 的协议进一步读取,不同版本的实现有差异。
所以我在解析逻辑里有一条铁律:不要假设parameters里一定有你想要的字段。最稳的做法是先把完整 Want 打印出来,看系统到底给了什么,再针对性取字段。我自己的项目里就出现过:文本分享时extra在 parameters 里,文件分享时反而要通过 uri 侧解析才能拿到,当时要是写死在单一字段上,后面改起来会很被动。
打印日志时建议用%{public}s加完整 JSON,开发阶段信息越全越好;上生产前再按隐私要求收敛。
4.3 热启动页面栈混乱、重复跳转
典型表现是:应用已经在后台,从分享面板拉起后,接收页被 push 了两次,或者本该更新数据的场景反而新建了页面。根因基本是两类:
- 只处理了
onCreate,onNewWant没有接,热启动路径下判断逻辑根本没触发; - 跳转前没有做页面栈检查,每次都无条件
pushUrl。
还有一种容易被忽略的情况:用户从你的应用发起分享,分享完又点通知或者其他入口回来,此时也会走到onNewWant,但这不算新的分享拉起。建议在业务数据里加一个来源标记,判断时先看这个标记,避免把“回自己的应用”误判成“新的分享进来”。
4.4 怎么模拟和自测
开发阶段不可能每次都去真机上翻图库,命令行aa工具可以模拟大部分拉起场景。我常用的写法类似这样:
aa start -b com.example.sharekit -a EntryAbility \ --uri "systemshare://data/demo" \ --action "ohos.want.action.viewData"aa的具体参数跟当前 SDK 版本有关,以你本机aa help的输出为准。核心是把 action 和 uri 按分享协议传进去,就能验证onCreate分支和页面跳转逻辑。
真机验证的完整路径是这样:打开系统备忘录或者图库,选中一段文本或一张图片,点分享,看你的应用是否出现在面板里。点中之后,观察两个地方:一是hilog里ShareDemo标签的日志,二是页面是否直接进了接收页。这一步建议固定为接入验收清单,每次改完配置都跑一遍。
5. 一点实操心得
5.1 入口判断要收敛成独立模块
我现在的工程里,分享入口判断是独立的一个工具类,不挂在任何页面组件上。这样做的直接好处是:换路由方案(router 换 Navigation)时只改一个文件;加日志埋点时不用翻遍所有页面;后续接入更多分享入口(比如应用间直传、跨设备分享)也只需要扩展同一个解析函数。
另外强烈建议保留一个日志总开关。开发阶段把每次收到的 Want 完整打印出来,上线前关掉。这个习惯帮我少走了很多弯路,因为分享场景的机型差异、系统版本差异真的很大,没有日志,定位问题全靠猜。
5.2 后续可以怎么扩展
这一篇只解决了“判断是不是被系统分享拉起”,判断完成之后还有一整个链路:接收多文件、按 mimeType 分发到不同页面、处理完数据之后怎么回传结果给分享源。这些在我后面的系列里都会展开。
说一个我自己的观察:很多人接分享时,精力全花在“数据怎么发出去”,轮到“接数据”才意识到,入口判断没做好,后面全是白搭。我在实际项目里把判断逻辑收敛好之后,从用户点分享到内容处理页出现的路径,短了不止一半。这个环节成本最低、收益却最明显,值得多花半小时做扎实。