HarmonyOS 碰一碰与隔空传送实战:真正的坑在 3 秒铁律和生命周期

本文主线以 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 秒可能会失败。

分享 App Linking 直达应用

V哥的提炼:回调里你只有三条路,且必须在窗口期内选一条------发、澄清、拒绝,缺一不可"给说法"。 clarifyNonSharereject 同样是有效答复,别只在能发时才响应,异常时也得回。

方法 起始版本 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.browsabledomainVerify: true),端到端才算通。


上线自检清单

  • 6.0 切口认清楚了?隔空传送 gesturesShare 仅在 6.0.0(20)+ 可用,clarifyNonShare 仅在 6.0.2(22)+ 可用,低版本要降级处理吗?
  • 两个事件都注册了吗?knockShare + gesturesShare 一起覆盖"贴"和"抓"。
  • 手机单窗口用无 capability 重载;PC/2in1/Tablet 用带 windowIdSendCapabilityRegistry 重载了吗?
  • 分享数据(链接、标题、缩略图)进页面时就预备好了吗?回调里只取不拼,没在回调里做耗时 IO 吧?
  • 回调里三条路都接了?share / clarifyNonShare / reject 任一异常都有答复,没有"装死"分支?
  • clarifyNonShare 只在碰一碰用、隔空传送不用,这个边界分清了吗?
  • reject 传的是 SharableErrorCode 枚举而不是随便一个数字吗?
  • off 时传了和 on 一致的事件名与 windowId/callback 吗?页面销毁、退后台都注销了吗?
  • 链接分享的 utd 是 general.hyperlink、且配了 App Linking 兜底(已装/未装两条路径都验证过)吗?
  • 文件分享的 contentfileUri.getUriFromPath 转沙箱 URI 了吗?没塞绝对路径吧?
  • 预览图走沙箱 fileUri、超大云端图用了 updateShareData 延迟补图吗?
  • 双端都具备 SystemCapability.Collaboration.HarmonyShare、账号/信任设备条件在真机验证过了吗?传输速率与稳定性以真机实测为准

参考与出处

本文涉及的事实性信息(API 名称、枚举取值、版本号、官方约束)来自以下官方文档,文中的结构、代码示例、决策流程与自检清单为本人整理编写:


最后一句 :这两个能力真正的难点不在"注册"那一行,而在回调之后那 3 秒------你得提前把数据备好、把异常分支接全、把生命周期对齐。想清楚这三件事,剩下的就是 on 一下、share 一下。

相关推荐
威哥爱编程1 小时前
HarmonyOS 扫码直达接入实战:系统扫、应用落,三步送用户进履约页
华为·harmonyos·arkts
威哥爱编程1 小时前
HarmonyOS 6.0 智感握姿实战:一道安检门、五态分诊、一静一响两个坑
华为·harmonyos·arkts
李游Leo2 小时前
HarmonyOS 7 音频与媒体控制实战 02:实现系统音频内录与状态控制
harmonyos
李游Leo2 小时前
HarmonyOS 7 音频与媒体控制实战 05:排查播放无声、卡顿和杂音问题
harmonyos
大雷神2 小时前
【共创稿事节】HarmonyOS ArkGraphics 3D 实操:用 GLB 节点与 PBR 材质打造智能音箱选配器
harmonyos
贾伟康11 小时前
【HarmonyOS 7新能力|020】LazyLayoutAlgorithm入门实战:从能力边界到最小可运行链路
harmonyos·arkts·arkui·harmonyos 7·lazylayout
梦想不只是梦与想13 小时前
鸿蒙 邀请测试:发布测试版本
harmonyos·appgallery 邀请测试·邀请测试
马剑威(威哥爱编程)14 小时前
【共创稿事节】HarmonyOS 7 应用 Skill 化实战:从“被打开“到“被调用“,把功能递进系统意图分发池
pytorch·深度学习·harmonyos
HarmonyOS_SDK17 小时前
HarmonyOS Push Kit 自分类权益 Skill,助力提升权益申请通过率
harmonyos