本文主线以 HarmonyOS 6.0 新增能力为切口:
clarifyNonShare(6.0.2(22) 起)、手机 ↔ PC/2in1 碰一碰与沙箱接收、绑定窗口的 capability 注册(6.0.0(20) 起)。碰一碰基础能力可回溯到 5.0,文中只作铺垫,一笔带过。文中代码是为说明问题自行编写的完整示例,API 名称、枚举取值与版本号等事实性信息均标注官方出处;涉及真机传输表现的部分已注明,未做任何实测数据编造。

引子:一句"手掌一攥文件就飞过去",V哥翻了三天文档
需求评审上产品说得很轻松:
用户手掌在另一台手机背面一攥,手里的文件就飞过去了;对着电脑隔空一抓,手机内容就落进 PC。
听着像调一个系统接口的事。V哥打开 Share Kit 文档准备抄示例,第一眼就发现方向不对:网上的代码几乎都只写 harmonyShare.on('knockShare', cb),却没人告诉你这俩事件到底差在哪、回调里不立刻发数据会怎样、PC 端为什么要多传一个 windowId。 这三件事,才是这篇要讲清楚的重点。
一、Share Kit 两兄弟:knockShare 与 gesturesShare 差在哪
碰一碰和隔空传送,在代码里就是两个事件名,都由 harmonyShare 抛出:
| 事件 | 触发方式 | 起始版本 | 说明 |
|---|---|---|---|
knockShare |
设备背部轻贴(碰一碰) | 5.0.0(12) | 基础能力,5.0 时代就有 |
gesturesShare |
空中手势(隔空传送) | 6.0.0(20) | 6.0 才开放,是本文的切口之一 |
这就是第一个原创判断:想落在 6.0 以上,隔空传送(gesturesShare)本身就是现成切口------它是 6.0.0(20) 才开放的。碰一碰虽老,但从 6.0.0(20) 起多了一个"带窗口注册"的能力重载,手机 ↔ PC/2in1 的沙箱接收也从这个版本起可用。所以写新功能时,建议两个事件一起注册,覆盖"贴"和"抓"两种动作。
官方在指南里对碰一碰的描述很直接:当宿主应用收到系统发出的碰一碰事件回调,可能因某些原因无法发起分享时,需及时终止,避免用户长时间等待(碰一碰内容分享)。这句话其实是全文的纲:回调里你必须"给个说法"。
二、注册这道坎:windowId 到底要不要绑
规划表里常写"注册必须绑 windowId",但翻 API 参考会发现这事没那么绝对。harmonyShare.on 有两种重载:
- 不带配置:
on('knockShare', callback)------ 从 5.0.0(12) 起就有; - 带窗口配置:
on('knockShare', capability: SendCapabilityRegistry, callback)------ 6.0.0(20) 起新增。
SendCapabilityRegistry 是 6.0.0(20) 才出生的注册配置项,字段有俩:windowId(指定可轻贴的窗口)和 sendOnly: boolean(默认 false,双端都为 true 时双向分享被拦截,仅碰一碰支持)。
V哥的自创判断:手机单窗口可以不绑,PC/2in1/Tablet 必须绑。 手机一个应用基本就一个前台窗口,系统知道往哪发;但 PC 上可能开了好几个窗口,你不告诉系统"可轻贴的是主窗口",轻贴就可能落到不可见的窗口上------这正是规划表那句"不绑就等着窗口不可见时乱回调"的真实含义,只不过它不是语法报错,而是多窗口场景下的体验错乱。官方在 off 的说明里也明确写道:推荐在 PC/2in1 和 Tablet 上使用带 windowId 的注册方式。

所以V哥封装时把两种注册分开:手机走 registerOnPhone,PC/2in1 走先取 windowId 再绑定的 registerOnWindow(见第五节代码)。
三、把数据装进 SharedData:一条链接怎么变一张卡
回调拿到的 SharableTarget 只是个"执行句柄",真正要发的东西得自己装进 systemShare.SharedData。最常用的是链接分享,utd 类型填 utd.UniformDataType.HYPERLINK:
typescript
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
import { systemShare, harmonyShare } from '@kit.ShareKit';
import { fileUri } from '@kit.CoreFileKit';
const data = new systemShare.SharedData({
utd: utd.UniformDataType.HYPERLINK,
content: 'https://yourapp.example.com/video/20260910',
title: '碰一碰分享卡片标题',
description: '碰一碰分享卡片描述'
});
// thumbnailUri 不是必填,但有它会让接收端卡片更直观
data.thumbnailUri = fileUri.getUriFromPath(this.context.filesDir + '/poster.jpg');
title + description + thumbnailUri 三个字段会决定卡片模板(纯图片 / 沉浸式大卡 / 白卡上下),官方有专门的设计指南。一个隐蔽坑 :预览图建议走沙箱 fileUri,别直接塞 PixelMap 或外部不可读路径;云端大图来不及下载时,可以先发核心数据、后用 updateShareData 补图,避免卡在下载上超时。
链接分享真正的价值在"落地":utd 配成 general.hyperlink 后,交给 App Linking 接手(见第七节),应用没装也能跳。
四、3 秒铁律:share / clarifyNonShare / reject 三选一
这是全文最该抄进架构评审的一节。官方在最佳实践页对碰一碰的措辞是:
收到碰一碰分享事件回调后,需尽快调用
sharableTarget.share()方法发起分享,超过 3 秒可能会失败。
V哥的提炼:回调里你只有三条路,且必须在窗口期内选一条------发、澄清、拒绝,缺一不可"给说法"。 clarifyNonShare 和 reject 同样是有效答复,别只在能发时才响应,异常时也得回。
| 方法 | 起始版本 | V哥什么时候用它 |
|---|---|---|
share(data) |
5.0.0(12) | 当前界面确有可分享内容,装好 SharedData 直接发 |
clarifyNonShare({message}) |
6.0.2(22) | 界面本身不支持分享(如空白页),礼貌告知并引导去可分享页 |
reject(errorCode) |
5.0.3(15) | 网络/下载失败等 genuinely 出错,上报错误码让用户知道原因 |
自创口诀:三秒不回话,系统直接判超时------数据要提前备好,不能等用户点了再现拼。 这意味着分享数据(链接、标题、缩略图)最好在进页面时就预备好,回调里只取不拼;任何耗时 IO(云端下载、压缩)都别堵在回调里。

五、异常别装死:clarifyNonShare(6.0.2 起)与 reject 的分工
clarifyNonShare 是 6.0.2(22) 才补上的"温和终止":它和 reject 的定位完全不同。
clarifyNonShare:当前界面"不是不能发,是不该发"------比如你在一个设置页被碰了。它带一句message引导用户去正确页面,体验软。官方明确它仅支持碰一碰分享功能(隔空传送用不了)。reject:是真的出错了(下载失败、业务异常),传SharableErrorCode让系统弹窗说明原因,体验硬。
下面是V哥自写的封装,把注册、三秒兜底、三选一都收在一个 KnockShareController 里:
typescript
// KnockShareController.ets ------ 自写的碰一碰/隔空传送注册 + 三秒兜底封装
import { harmonyShare, systemShare } from '@kit.ShareKit';
import { uniformTypeDescriptor as utd } from '@kit.ArkData';
import { fileUri } from '@kit.CoreFileKit';
import { window } from '@kit.ArkUI';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
// 系统回调后约 3 秒内必须给出答复,否则本次传送被判超时失败
const RESPOND_DEADLINE_MS = 3000;
export interface SharePayload {
link: string;
title: string;
description: string;
thumbPath?: string;
}
export class KnockShareController {
private context: common.UIAbilityContext;
private watchdog?: number;
constructor(context: common.UIAbilityContext) {
this.context = context;
}
// 手机单窗口:直接注册,无需 windowId
registerOnPhone(handler: (t: harmonyShare.SharableTarget) => void): void {
const cb = (t: harmonyShare.SharableTarget) => this.guard(handler, t);
harmonyShare.on('knockShare', cb);
harmonyShare.on('gesturesShare', cb);
}
// PC / 2in1 / Tablet:先取窗口 ID 再绑定注册
async registerOnWindow(handler: (t: harmonyShare.SharableTarget) => void): Promise<void> {
const win = await window.getLastWindow(this.context);
const windowId = win.getWindowProperties().id;
const cb = (t: harmonyShare.SharableTarget) => this.guard(handler, t);
harmonyShare.on('knockShare', { windowId }, cb);
harmonyShare.on('gesturesShare', { windowId }, cb);
}
// 三秒发条:进入回调即倒计时,业务须在窗口期内答复(系统才真正判超时,这里是提醒你别在回调里做耗时 IO)
private guard(handler: (t: harmonyShare.SharableTarget) => void, target: harmonyShare.SharableTarget): void {
this.watchdog = setTimeout(() => {
target.clarifyNonShare({ message: '分享准备超时,请稍后重试' });
}, RESPOND_DEADLINE_MS) as unknown as number;
handler(target);
}
// 业务侧组装数据后调用:发完即拆发条
send(target: harmonyShare.SharableTarget, payload: SharePayload): void {
const data = new systemShare.SharedData({
utd: utd.UniformDataType.HYPERLINK,
content: payload.link,
title: payload.title,
description: payload.description
});
if (payload.thumbPath) {
data.thumbnailUri = fileUri.getUriFromPath(payload.thumbPath);
}
target.share(data)
.then(() => this.clearWatchdog())
.catch((e: BusinessError) => this.terminate(target, e));
}
private terminate(target: harmonyShare.SharableTarget, e: BusinessError): void {
this.clearWatchdog();
if (e.code === harmonyShare.SharableErrorCode.DOWNLOAD_ERROR) {
target.reject(harmonyShare.SharableErrorCode.DOWNLOAD_ERROR);
} else {
target.clarifyNonShare({ message: '当前内容暂不支持分享' });
}
}
private clearWatchdog(): void {
if (this.watchdog !== undefined) {
clearTimeout(this.watchdog);
this.watchdog = undefined;
}
}
unregisterOnPhone(): void {
this.clearWatchdog();
harmonyShare.off('knockShare');
harmonyShare.off('gesturesShare');
}
async unregisterOnWindow(): Promise<void> {
this.clearWatchdog();
const win = await window.getLastWindow(this.context);
const windowId = win.getWindowProperties().id;
harmonyShare.off('knockShare', { windowId });
harmonyShare.off('gesturesShare', { windowId });
}
}
注意 off 时必须传和 on 一致的事件名与(PC 端的)windowId/callback,否则注销不掉,残留监听会和其它应用的碰一碰打架。
六、手机 ↔ PC/2in1:沙箱接收与 windowId 绑定
6.0.0(20) 起,碰一碰不再只是手机对手机------手机 ↔ PC/2in1 也能碰,且接收端走沙箱。这条链路是 6.0 最值得写的新切口:
- 发送端若是 PC,必须用带
windowId的注册(见第二节),否则系统不知道该从哪个窗口发起; - 接收端在应用沙箱里拿到文件/链接 URI,落盘或预览都在自己目录内,不用额外开权限;
- 文件分享时
content要用fileUri.getUriFromPath把沙箱路径转成 URI,别塞绝对路径或外部不可读路径。
传输表现(速率、稳定性、机型差异)V哥没法在这里编------以真机实测为准 。文档明确这类近场能力要求双端都具备 SystemCapability.Collaboration.HarmonyShare,且建议双方登录同一华为账号或已在信任设备列表。

七、没装 App 也能落地:App Linking 兜底
链接分享的最后一公里是"对端没装你的应用怎么办"。官方给的方案是 App Linking:
- 应用已装:App Linking 直接拉起,落地到具体页;
- 应用没装:默认走系统浏览器开网页,配合直达应用市场能力可跳应用市场,安装后靠延迟链接仍能还原之前分享的内容。
所以 content 里那条链接别随便写------它既是分享内容,也是落地路由。配上 module.json5 里的 skills(scheme 用 https、配 entity.system.browsable、domainVerify: true),端到端才算通。
上线自检清单
- 6.0 切口认清楚了?隔空传送
gesturesShare仅在 6.0.0(20)+ 可用,clarifyNonShare仅在 6.0.2(22)+ 可用,低版本要降级处理吗? - 两个事件都注册了吗?
knockShare+gesturesShare一起覆盖"贴"和"抓"。 - 手机单窗口用无 capability 重载;PC/2in1/Tablet 用带
windowId的SendCapabilityRegistry重载了吗? - 分享数据(链接、标题、缩略图)进页面时就预备好了吗?回调里只取不拼,没在回调里做耗时 IO 吧?
- 回调里三条路都接了?
share/clarifyNonShare/reject任一异常都有答复,没有"装死"分支? -
clarifyNonShare只在碰一碰用、隔空传送不用,这个边界分清了吗? -
reject传的是SharableErrorCode枚举而不是随便一个数字吗? -
off时传了和on一致的事件名与windowId/callback吗?页面销毁、退后台都注销了吗? - 链接分享的 utd 是
general.hyperlink、且配了 App Linking 兜底(已装/未装两条路径都验证过)吗? - 文件分享的
content用fileUri.getUriFromPath转沙箱 URI 了吗?没塞绝对路径吧? - 预览图走沙箱
fileUri、超大云端图用了updateShareData延迟补图吗? - 双端都具备
SystemCapability.Collaboration.HarmonyShare、账号/信任设备条件在真机验证过了吗?传输速率与稳定性以真机实测为准。
参考与出处
本文涉及的事实性信息(API 名称、枚举取值、版本号、官方约束)来自以下官方文档,文中的结构、代码示例、决策流程与自检清单为本人整理编写:
- 碰一碰分享(开发指南)
- 隔空传送(开发指南)
- 手机与手机碰一碰内容分享
- harmonyShare(华为分享)ArkTS API 参考
- 碰一碰链接分享(最佳实践)
- 分享 App Linking 直达应用(含 3 秒超时表述)
- 使用 App Linking 实现应用间跳转
- 鸿蒙接入"碰一碰"和"隔空传送"教学(blog)
- HarmonyOS 跨设备分享教程:隔空传送与碰一碰分享(blog,含 3 秒流程)
最后一句 :这两个能力真正的难点不在"注册"那一行,而在回调之后那 3 秒------你得提前把数据备好、把异常分支接全、把生命周期对齐。想清楚这三件事,剩下的就是 on 一下、share 一下。