轻规划鸿蒙开发实战7:接管 Ability Kit 跨设备流转,EntryAbility 全链路 onContinue 数据打包与无缝接续

背景介绍

我们在单设备上进行自我管理规划时,经常会遇到“场景的迁徙”。例如:在通勤地铁上,用户手持手机在“轻规划”中艰难地用虚拟键盘书写五年后的愿景信;回到家后,用户希望立刻将这个“书写现场”无缝地流转到放在桌上的鸿蒙平板电脑中,利用平板更开阔的屏幕与实体键盘继续撰写。

如果重新登录再找到这个输入框,不仅繁琐,而且刚刚打出的草稿还来不及保存。
轻规划鸿蒙开发实战7:接管 Ability Kit 跨设备流转,EntryAbility 全链路 onContinue 数据打包与无缝接续-1.png
HarmonyOS NEXT 提供了原生应用级流转底座——Ability Kit(应用接续)

在应用接续体系下,只要两台设备登录同一个华为账号、处于近场范围内,平板电脑的 Dock 栏上就会自动浮现手机端的“轻规划”流转图标。用户轻轻一戳,手机端编辑界面立刻关闭,而在平板端以一模一样的输入内容、甚至是光标停留位置无缝打开。

今天,我们将全链路拆解在主 Ability(EntryAbility)中重写 onContinue 数据封装协议与反序列化现场恢复的技术实战。


1. 架构纵览:应用接续与现场状态重建管线

应用流转由系统底层通过分布式总线在设备间热备数据。源端通过回调将状态写入 Want 数据包,宿端在冷启动或热拉起时解析此包重建界面。职责划分如下:

轻规划鸿蒙开发实战7:接管 Ability Kit 跨设备流转,EntryAbility 全链路 onContinue 数据打包与无缝接续.png

1.1 应用接续的核心机制

应用接续(Continuation)基于 HarmonyOS 系统的分布式流转管理框架,允许用户在一个设备上运行的应用以极低的开销流转到另一个设备上并无缝续接。

  • 分布式调度系统(Distributed Schedule Service, DSS):负责跨设备生命周期的协调与任务流转的统一调度,监测近场内登录同一账号的活动设备。
  • 分布式软总线(Distributed Soft Bus):提供低时延、高带宽的设备间数据传输信道,保障应用数据包及控制命令的高速传输。
  • Want 协议包:作为设备间传递信息的载体,它是连接源端与宿端的数据桥梁。
1.2 关键生命周期解析

当接续流程被触发时,源端和宿端会经历如下生命周期变换:

  1. 源端触发期:系统回调源端 UIAbility 的 onContinue(wantParams)。开发者在该接口中收集当前的 UI 状态及临时数据,打包存入 wantParams
  2. 中间传输期:系统通过安全传输信道将 wantParams 及拉起参数发往目标设备。
  3. 宿端拉起期:目标端设备判断应用实例是否存在。若不存在(冷启动),则创建实例并执行 onCreate(want);若已在后台(热启动),则直接执行 onNewWant(want)。在这两个阶段中,开发者需提取 want.parameters 并进行数据重建。
  4. 源端终结期:接续传输完毕后,源端应用根据系统的接续决策(通常为迁出并销毁)调用 onDestroy() 释放本地资源。

2. 源端状态封包:重写 onContinue 协议

为了能让对端完美还原流转前的状态,我们必须在源端 Ability 的 onContinue 钩子中,将当前所在的“业务页面路由”、“编辑的文本草稿”及“光标偏移量”全部存入 wantParams

2.1 接续结果返回值与系统裁决

onContinue 中,我们必须返回 AbilityConstant.OnContinueResult 状态字:

  • AGREE:允许应用流转。系统随后将启动跨设备传输。
  • REJECT:拒绝此次流转。这常用于正在进行敏感本地事务、网络环境严重不佳或本地数据尚未就绪的紧急防御机制。
  • MISMATCH:设备间应用版本号不一致导致的数据格式冲突风险。
2.2 跨设备流转传输的数据载体对比

对于不同体量的数据,我们应该采取不同的承载策略,避免因过度填充数据引起传输超时和性能骤降。

传输维度 wantParams 键值对 分布式数据库 (KVStore / RdbStore) 分布式文件系统 (DFS)
数据量级 建议小于 100KB 数 KB 到数 MB 的结构化数据 较大体积的图片、视频、长音频等非结构化文件
时延特性 极低(随着 Binder 及系统通道一并流转) 依赖底层多端同步同步机制(百毫秒级) 视网络情况而定(秒级)
主要用途 页面路由、焦点状态、文本框光标位置、表单轻量草稿 用户应用数据库、大批量未提交表单、历史操作日志 待流转的半成品文件、手写板大图片、大音频草稿
生命周期 随流转事件一次性交付,宿端读取后直接失效 跨设备持久化保存,源端与宿端直接双向同步 宿端根据沙箱路径按需读取,需要分布式路径管理
2.3 源端封包核心代码

在应用接续过程中,源端使用全局共享的 AppStorage 将前台 UI 组件的交互状态收集起来,并在 EntryAbility 中进行封装。

import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit';

export default class EntryAbility extends UIAbility {
  /**
   * 当应用流转被触发时,系统会主动回调此方法
   * @param wantParams - 用于封包和传输的数据结构字典,开发者需在此塞入待接续状态
   * @returns 返回接续决策,AGREE 代表同意接续传输,REJECT 代表拒绝
   */
  onContinue(wantParams: Record<string, Object>): AbilityConstant.OnContinueResult {
    try {
      console.info("EntryAbility", "Entering onContinue lifecycle...");

      // 1. 从全局进程内存缓存 AppStorage 中,动态读取当前前台 UI 组件暂存的编辑现场状态
      // 获取当前用户所停留在的页面路由,默认退回到 'VisionEditPage'
      const currentRoute = AppStorage.get<string>('currentRoute') || 'VisionEditPage';
      
      // 读取愿景信写信框的实时输入草稿,若无则默认为空字符串
      const editingDraft = AppStorage.get<string>('visionLetterDraft') || '';
      
      // 读取光标在写信框中的实时字符偏移索引,确保接续后光标位置分毫不差
      const selectionOffset = AppStorage.get<number>('visionInputCursorOffset') || 0;

      // 2. 将序列化的上下文状态数据压入 wantParams 数据容器中进行远端通信打包
      // 目标页面路由
      wantParams['targetRoute'] = currentRoute;
      // 输入框中的文字草稿
      wantParams['editingDraft'] = editingDraft;
      // 记录的光标当前位置
      wantParams['cursorOffset'] = selectionOffset;

      console.info("EntryAbility", `onContinue packet details - Route: ${currentRoute}, Draft length: ${editingDraft.length}, Cursor: ${selectionOffset}`);
      
      // 3. 返回 AGREE 决策字,同意底层流转系统立刻打包打包数据包,并流转向宿端设备
      return AbilityConstant.OnContinueResult.AGREE;
    } catch (err) {
      // 异常保护:若因为状态提取或未知空指针产生严重异常,应立即返回 REJECT,拦截本次流转
      // 这样能有效规避宿端设备拉起后因没有拿到入参而出现严重渲染失败的情况,降低稳定性风险
      console.error("EntryAbility", "onContinue save state failed with error: ", JSON.stringify(err));
      return AbilityConstant.OnContinueResult.REJECT;
    }
  }
}

3. 宿端状态解析与界面重构

在接收流转的设备端,系统会自动冷启动或热激活“轻规划”应用。我们必须在 onCreate(冷启动场景)或 onNewWant(热启动场景)中拦截 Want 参数,并判断拉起原因是 LAUNCHREASON_CONTINUATION

3.1 宿端双通道(Cold & Hot)状态接管策略

宿端面临两种系统运行时状态:

  1. 冷启动(Cold Start):宿端在此之前未打开过“轻规划”,进程被系统拉起。此时会优先走 onCreate(want, launchParam) 生命周期。
  2. 热拉起(Hot Standby):宿端应用已在后台挂起或被用户压在后台。此时应用进程并未消亡,系统不会再次调用 onCreate,而是直接通过 onNewWant(want, launchParam) 机制将新的 Want 传入。

为了保障在这两个不同入口上均能万无一失地捕获流转入参,我们将解包重建逻辑抽象成独立私有函数 handleRestoreContinuation(want, launchParam) 进行重载处理。

3.2 宿端解包与现场还原核心代码
  /**
   * 宿端应用进程被创建时的初始化回调
   * @param want - 系统传入的启动信息意图包,流转时内部包含从源端打包过来的 wantParams
   * @param launchParam - 包含启动原因等关键参数
   */
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    console.info("EntryAbility", "onCreate triggered.");
    // 拦截并在冷启动流程中执行接续数据还原
    this.handleRestoreContinuation(want, launchParam);
  }

  /**
   * 当应用实例已经存在于后台,系统被流转再次热拉起时的回调
   * @param want - 系统传入的新意图包,流转热拉起时内部携带数据
   * @param launchParam - 启动参数
   */
  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    console.info("EntryAbility", "onNewWant triggered.");
    // 拦截并在热启动流程中执行接续数据还原
    this.handleRestoreContinuation(want, launchParam);
  }

  /**
   * 双通道复用接续数据还原引擎
   * @param want - 源端传递来的 Want
   * @param launchParam - 启动参数描述对象
   */
  private handleRestoreContinuation(want: Want, launchParam: AbilityConstant.LaunchParam) {
    // 1. 通过 launchReason 状态位进行安全校验,确认本次拉起动作确为“流转接续(CONTINUATION)”
    // 这可以有效规避普通用户通过点击图标、分享链接或者服务卡片进入该生命周期时发生路由抢占和状态混乱的问题
    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
      console.info("EntryAbility", "Application launched via continuation. Restoring context...");

      // 获取携带的自定义流转参数
      const wantParams = want.parameters;
      if (wantParams) {
        // 2. 从入参中安全解构出流转上下文键值对,并进行强类型转换
        const targetRoute = wantParams['targetRoute'] as string;
        const editingDraft = wantParams['editingDraft'] as string;
        const cursorOffset = wantParams['cursorOffset'] as number;

        // 3. 将反序列化还原的数据状态再次同步推入宿端本地的全局 AppStorage 中
        // 覆盖宿端旧的路由指向
        AppStorage.setOrCreate('currentRoute', targetRoute);
        // 重写宿端写信编辑框中的草稿文字
        AppStorage.setOrCreate('visionLetterDraft', editingDraft);
        // 重置光标偏移量
        AppStorage.setOrCreate('visionInputCursorOffset', cursorOffset);
        
        // 4. 触发流转恢复的全局响应式开关。UI 树会根据该标志自动触发定制路由重定向
        AppStorage.setOrCreate('isRestoredFromContinuation', true);
        
        console.info("EntryAbility", `Successfully restored data. Target Route: ${targetRoute}, Draft len: ${editingDraft ? editingDraft.length : 0}`);
      } else {
        // 稳定性风险防护:若底层传输的数据为空,需要在这里提供兜底策略,规避非授权访问或非法数据导致的应用奔溃
        console.warn("EntryAbility", "Continuation parameters are empty. Applying fallback routing...");
        AppStorage.setOrCreate('isRestoredFromContinuation', false);
      }
    }
  }

轻规划鸿蒙开发实战7:接管 Ability Kit 跨设备流转,EntryAbility 全链路 onContinue 数据打包与无缝接续-2.png


4. 极客避坑:UI 组件侧的反向监听与闪屏优化

即使 Ability 层成功把数据还原到了 AppStorage,但如果前台 UI 组件还没有完全初始化完毕(冷启动时),路由就发起了跳转,这会导致路由加载失败或界面闪烁。

4.1 避坑深度解析:闪白与跳页冲突的物理成因

在冷启动接续时,从底座框架的 C++ 端完成内存载入,再到 JS 引擎实例化 UI 树(即 Index.etsaboutToAppear 生命周期被执行),需要一定的时间片。
若在此瞬间(如在进程刚刚跑起来,而组件还没有生成任何虚拟节点时)由进程强制下发页面跳转指令,此时路由栈(RouterNavigation)极大概率抛出“容器未就绪”或找不到当前实例的严重错误,表现为界面黑屏或闪烁。

为了规避这种由于组件生命周期异步不同步引起的视觉灾难,我们需要在主入口执行一层微小的时间缓冲,并在首帧组件渲染后,利用 @Watch 全局监听机制完成优雅重定向。

4.2 避坑指南:延时挂载与 Watch 机制
@Entry
@Component
struct MainIndex {
  // 使用 @StorageLink 动态绑定 AppStorage 中的流转状态,且在发生改变时自动触发 onRestoreTriggered 回调方法
  @StorageLink('isRestoredFromContinuation') @Watch('onRestoreTriggered') isRestored: boolean = false;
  
  // 绑定当前流转目标路由页面,一旦宿端开始接收接续流程,这将被更新为目标编辑页面
  @StorageLink('currentRoute') currentRoute: string = 'MainTab';

  /**
   * 全局流转状态被源端变更且回填后的响应函数
   */
  onRestoreTriggered() {
    // 一旦捕获到接续恢复动作开启信号
    if (this.isRestored) {
      console.info("MainIndex", "Continuation flag changed. Preparing route redirection...");
      
      // 延时 100ms 等待宿端设备的主窗口、UI 容器以及主路由栈彻底加载挂载完成,规避冷启动瞬间黑白屏/组件闪烁
      setTimeout(() => {
        // 确认接续现场确实指定了编辑信件页面,则执行内部跳转方法
        if (this.currentRoute === 'VisionEditPage') {
          this.navigateToEditPage();
        }
        // 重置流转恢复标识为 false,断开监听闭环,防止进入死循环或者不合规的重复跳转操作
        this.isRestored = false;
      }, 100);
    }
  }

  /**
   * 模拟导航跳转函数,实际开发中可以对接 UIContext.getRouter() 或 Navigation 框架的 pathStack 动态处理
   */
  private navigateToEditPage() {
    console.info("MainIndex", "Navigating to: VisionEditPage");
    // 执行跳转相关的导航逻辑...
  }

  build() {
    Column() {
      Text('轻规划 - 主页框架')
        .fontSize(24)
        .margin(20)
    }
    .width('100%')
    .height('100%')
  }
}

通过这一层缓冲保护,接续时平板端会在极度丝滑且没有任何中间白屏的视觉体验下直接显示手机端的编辑界面,达成了商业级的体验水准。


5. 流转连接与稳定性风险规避

在实际多设备交互的生产环境中,流转流程的稳定性还依赖于网络与账号授权的状态。开发者必须在后台注意以下情况,避免可能产生的程序稳定性风险与未授权越权问题:

  1. 账号一致性检测:系统级流转默认进行了同账号双重身份的判定,但在业务数据解密解包时,必须再次校核宿端本机的运行时上下文是否发生了变更,防止出现跨账号信息越权。
  2. 多端版本格式校对:如果用户手机端应用版本是高版本(含有新功能的状态数据),而平板端是未升级的低版本,极易导致反序列化时属性不存在而引发应用闪退。开发者应该在 handleRestoreContinuation 中加入 try-catch 防御,并在解包失败时,主动重置回默认主页,而非直接抛错崩溃。
  3. 断网与弱网兜底:流转底层虽然通过分布式软总线自建通道,但长草稿传输或数据库增量同步依旧高度依赖本地局域网带宽。如果接续在弱网下失败,宿端需向用户展示友好的系统级弹窗提示,如“当前设备数据流转失败,请检查两端设备网络是否畅通”。

6. 总结与下期预告

通过重写 EntryAbility 的流转生命周期,我们彻底打破了终端的物理界限,为“轻规划”赋予了强大的多端无缝工作流接续能力。

在完成了分布式数据库多端同步与跨端接续这两个关于设备外部协同的技术后,我们的视线需要移向安全防线。用户写下的愿景信、人生规划里多多少少包含个人隐私,在公共场合(如公交、地铁上)查看时,如何防止旁边的人偷窥?

在下一篇文章中,我们将踏入系统级主动防御大门:Device Security Kit AI 防窥保护,人脸识别敏感视线追踪与毛玻璃屏动态遮蔽! 敬请期待。# 轻规划鸿蒙开发实战6:Share Kit 碰一碰近场快传,外链 aeroplan 协议与 Base64 字节流无缝对接

@[

toc]

背景介绍

在社交和日常团队协作场景下,用户经常有分享规划目标(如一份精心定制的“程序员三十岁前财务自由计划”、“西藏骑行旅行路线”等)的需求。

传统的分享极其繁琐且存在体验割裂:用户需要点击分享,将长文本复制到剪贴板或生成一张大图,接着手动切换打开第三方即时通讯软件,发送给朋友;而接收方则需下载并保存图片或复制文本,再次返回到目标 App 中进行导入解析。这种漫长且伴随着频繁跨应用跳转的操作路径,不仅容易因为系统内存不足导致后台进程被清理,还大幅降低了分享的转化率。

HarmonyOS NEXT 提供了强大的系统级近场通信能力——Share Kit(碰一碰分享)
轻规划鸿蒙开发实战6:Share Kit 碰一碰近场快传,外链 aeroplan 协议与 Base64 字节流无缝对接-1.png
当用户开启碰一碰时,两台登录不同华为账号的鸿蒙手机轻轻触碰,就可以无缝唤起系统级的快传气泡。我们在“轻规划”(AeroPlan)中,通过定制专属的 aeroplan:// 链接协议,将九宫格核心数据打包为 Base64 字节流,两机一碰,即刻在对端拉起并完整复原这套精美规划。

今天,我们将从协议定制、配置文件声明、Base64 编解码落盘全链路进行实战解构。


1. 架构纵览:碰一碰近场通信与外链唤醒管线

当分享发起时,A 设备负责将内存中的复杂规划数据序列化为短文本链接;碰一碰激活后,系统在近场建立临时 P2P 通信信道,将链接投射给 B 设备;B 设备捕捉到 aeroplan:// 后自动反向冷启动并解析入库。其底层的核心流转管线如下:

接收端应用 (轻规划) 接收端系统 (Want 路由) 发送端系统 (Share Kit) 发送端应用 (轻规划) 接收端应用 (轻规划) 接收端系统 (Want 路由) 发送端系统 (Share Kit) 发送端应用 (轻规划) 物理碰一碰 (NFC握手 ->> 建立临时蓝牙/Wi-Fi P2P通道) 数据序列化 (JSON ->> Base64 压缩字节流) 1 注册 SharedData & 呼起 ShareController 2 极速投递 scheme: aeroplan://import_plan 3 解析 Want / 匹配 module.json5 中的 skills 规则 4 拉起应用生命周期 (onCreate / onNewWant) 5 拦截 URI ->> 提取 Base64 字符串 ->> 还原 JSON 数据 6 UI 层弹窗确认 ->> 写入本地超轻量级关系数据库 (RDB) 7

通过这一物理碰一碰设计,我们将传统流程中至少 6 次的主动操作压缩为“碰一碰 -> 确认导入” 2 步,消除了用户在跨应用数据流转过程中的繁琐体验。


2. 发送端:Share Kit 碰一碰数据注册与投射

使用 Share Kit 发送数据,我们需要向系统服务注册当前需要投射的分享数据卡片。这需要借助于鸿蒙系统自带的 @kit.ShareKit 模块。在实际开发中,由于用户的规划数据(包括各节点任务、打卡周期、优先级等)是结构化的内存对象,我们需要将其序列化为字节流,再转换为 Base64 格式,拼装在自定义的外链中。

碰一碰数据投射核心代码
import { systemShare } from '@kit.ShareKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { util } from '@kit.ArkTS';

/**
 * 近场快传助手类,负责将本地内存规划序列化为 Base64 并拉起系统分享面板
 */
export class NearFieldShareHelper {
  /**
   * 触发碰一碰近场数据分享
   * @param context UIAbility 上下文,用于提供拉起系统级气泡所需的生命周期上下文
   * @param planTitle 规划的标题,例如“西藏骑行路线”
   * @param planData 规划的 JSON 字符串数据
   */
  public static async triggerNearFieldShare(context: common.UIAbilityContext, planTitle: string, planData: string): Promise<void> {
    try {
      // 1. 将复杂的规划 JSON 数据转化为字节流,用于后续进行 Base64 编码,确保在传输链路上的高抗干扰性
      const textEncoder = new util.TextEncoder();
      // encodeInto 返回 Uint8Array,这属于直接内存映射操作,比普通的循环转换性能高出 60% 以上
      const rawBytes = textEncoder.encodeInto(planData);
      
      // 2. 利用系统工具类,同步将字节流转换为标准的 Base64 字符串,用于外链中安全传递
      const base64Helper = new util.Base64Helper();
      const base64Str = base64Helper.encodeToStringSync(rawBytes);
      
      // 3. 构建专属自定义 Scheme 协议外链(URI),确保接收端收到后能正确路由到轻规划应用
      // 考虑到标题中可能包含中文字符和特殊标点,使用 encodeURIComponent 对标题进行安全编码
      const shareUrl = `aeroplan://import_plan?title=${encodeURIComponent(planTitle)}&data=${base64Str}`;

      // 4. 创建系统级共享数据块,UTD(Uniform Type Descriptor)声明为链接类型
      const shareData = new systemShare.SharedData({
        utd: 'usid.link', // 'usid.link' 为鸿蒙官方约定的超链接标准类型标识符
        title: `【轻规划分享】${planTitle}`,
        description: "快来碰一碰导入我为你定制的专属规划!",
        content: shareUrl // 包含 Base64 字节流的 Scheme 地址
      });

      // 5. 构建 ShareController 并配置预览模式与选择模式,启动近场共享气泡
      const controller = new systemShare.ShareController(shareData);
      
      // 唤起系统分享控制器
      await controller.show(context, {
        previewMode: systemShare.SharePreviewMode.DETAIL, // 详细预览模式,显示标题与说明
        selectionMode: systemShare.ShareSelectionMode.SINGLE // 单目标传输模式,防止近场干扰误投
      });
      
      console.info("NearFieldShareHelper", "Share controller shown successfully");
    } catch (err) {
      // 捕获可能产生的业务级错误,防止因系统接口调用失败导致应用发生崩溃
      const error = err as BusinessError;
      console.error("NearFieldShareHelper", `Share trigger failed: Code: ${error.code}, Message: ${error.message}`);
    }
  }
}

3. 配置文件:注册自定义 Scheme 协议

为了让接收端能够拦截并唤起 aeroplan://import_plan 协议,我们需要在项目的主模块配置文件 module.json5 中注册对应的 Scheme。在 skillsuris 列表中声明自定义协议,这样当系统检测到此类 URI 时,就会自动唤起我们的应用。

module.json5 核心配置

entry/src/main/module.json5 文件的 abilities 数组中,找到 EntryAbility,并配置以下 skills 字段:

{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "description": "$string:EntryAbility_desc",
        "icon": "$media:layered_image",
        "label": "$string:EntryAbility_label",
        "startWindowIcon": "$media:startIcon",
        "startWindowBackground": "$color:start_window_background",
        "exported": true,
        "skills": [
          {
            "actions": [
              "ohos.want.action.viewData" // 声明该 Ability 具备查看数据资产的能力
            ],
            "entities": [
              "entity.system.default",
              "entity.system.browsable" // 声明可以通过浏览器或者外链的形式进行唤醒跳转
            ],
            "uris": [
              {
                "scheme": "aeroplan",      // 自定义 Scheme 协议头,必须与发送端构建的 shareUrl 完全对应
                "host": "import_plan",     // 主机路径名,用于区分子业务路由
                "linkFeature": "importPlan" // 自定义链接特征描述
              }
            ]
          }
        ]
      }
    ]
  }
}

4. 接收端:Scheme 外链拦截与 Base64 反序列化

当 B 设备接收到碰一碰传来的 aeroplan:// 外链时,系统根据上面的 skills 配置查找到“轻规划”应用并自动唤起。我们需要在 EntryAbility.etsonCreate(冷启动场景)与 onNewWant(热启动场景,应用已驻留后台)生命周期钩子中进行数据劫持与提取。

EntryAbility 接收解析代码
import { UIAbility, Want, AbilityConstant } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
import { util } from '@kit.ArkTS';

/**
 * 接收端主能力类,负责监听外部 Want 调起并解析其中的 Base64 数据
 */
export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 处理应用处于未启动状态(冷启动)时被碰一碰拉起的情况
    this.handleIncomingWant(want);
  }

  onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 应用已经在后台运行(热启动)时被碰一碰拉起,触发此生命周期回调
    // 必须在此重新获取传入的最新 Want,否则拿到的将是首次启动的旧 Want 数据
    this.handleIncomingWant(want);
  }

  /**
   * 拦截并过滤传入的 Want 信息,定位 aeroplan 协议的 URI
   */
  private handleIncomingWant(want: Want) {
    const uri = want.uri;
    // 判断传入的 uri 是否存在且符合约定的 Scheme 规则
    if (uri && uri.startsWith('aeroplan://import_plan')) {
      console.info("EntryAbility", `Incoming scheme detected: ${uri}`);
      this.parseAndImportPlan(uri);
    }
  }

  /**
   * 解析 Scheme 路径,完成 Base64 反序列化并交由 UI 层处理
   * @param uriStr 传入的完整 Scheme URI 字符串
   */
  private parseAndImportPlan(uriStr: string) {
    try {
      // 1. 构建 URL 解析工具类,用于提取 URL 上的查询参数
      const url = new URL(uriStr);
      const dataParam = url.searchParams.get('data');
      if (!dataParam) {
        console.warn("EntryAbility", "Parameter 'data' not found in incoming scheme");
        return;
      }

      // 2. 将 URL 传参中的 Base64 编码字符串还原为底层的字节数组
      const base64Helper = new util.Base64Helper();
      const decodedBytes = base64Helper.decodeSync(dataParam);
      
      // 3. 将字节数组通过 TextDecoder 解码成应用可识别的 JSON 文本字符串
      const textDecoder = new util.TextDecoder('utf-8');
      const planJson = textDecoder.decodeWithStream(decodedBytes);

      // 4. 将解析出的 JSON 暂存到全局 AppStorage,以实现从业务引擎到 UI 渲染页面的跨层级事件总线通知
      AppStorage.setOrCreate('pendingImportPlanData', planJson);
      // 开启标志位,通知应用内的弹窗组件弹出“发现好友分享的新规划,是否导入?”的确认窗
      AppStorage.setOrCreate('triggerImportDialog', true);
      
      console.info("EntryAbility", "Successfully decoded and staged pending plan data");
    } catch (e) {
      // 捕获解码转换过程中的任何异常(如传输中断导致 Base64 结构受损)
      console.error("EntryAbility", "Decode incoming plan failed. Error structure: " + JSON.stringify(e));
    }
  }
}

分享方界面:
轻规划鸿蒙开发实战6:Share Kit 碰一碰近场快传,外链 aeroplan 协议与 Base64 字节流无缝对接-2.png

接收方气泡提示:
轻规划鸿蒙开发实战6:Share Kit 碰一碰近场快传,外链 aeroplan 协议与 Base64 字节流无缝对接-3.png

接收方自动弹窗确认并导入数据:
轻规划鸿蒙开发实战6:Share Kit 碰一碰近场快传,外链 aeroplan 协议与 Base64 字节流无缝对接-4.png


5. 极客避坑:Base64 编码的超长 URL 限制与熔断

在 HarmonyOS 系统的 URL 传参解析中,如果我们将一个非常庞大的规划项目(包含成百上千个子任务、复杂日志、背景图片 Base64 串等)强行转化为 Base64,生成的 Scheme 长度可能超过 8KB。在部分旧款设备或高负载场景下,系统拉起 Scheme 时会对超长链接进行截断,导致数据损坏无法解码。

避坑指南:URL 长度容灾策略

为了确保系统的稳定可靠,避免在超大规划分享时发生闪退或乱码错误,我们在打包发送阶段设计了安全熔断与降级策略

const MAX_URL_LENGTH_BYTES = 4096; // 4KB 核心安全限制阈值

if (base64Str.length > MAX_URL_LENGTH_BYTES) {
  // 触发本地降级逻辑:
  // 若规划数据包过大,不再通过 Scheme 中携带明文 Base64 的方式传输,
  // 而是改为调用云端临时中转接口,将数据托管至分布式临时云空间,仅在 Scheme 中携带 6 位随机“提取码”
  console.warn("NearFieldShareHelper", "Data package too large for Scheme URL, degrading to code retrieval");
  
  // 示例伪代码:
  // const extractCode = await CloudSyncService.uploadTempData(planData);
  // const shareUrl = `aeroplan://import_plan?code=${extractCode}`;
}

这一熔断安全垫设计,从应用架构的健壮性上切断了由于超长 URL 限制导致的不可预期稳定性风险,实现了良好的用户体验保障。


6. 总结与下期预告

通过原生 Share Kit 碰一碰近场传输功能,搭配自定义的专属 aeroplan:// 协议与高效的 Base64 字节流编解码处理方案,“轻规划”消灭了效率应用跨设备沟通的繁琐壁垒,让灵感在设备触碰的瞬间实现零摩擦传递。

近场快传解决了“两机碰一碰,数据瞬间达”的物理距离交互。但如果用户手里有多个属于自己的鸿蒙设备(如手机、平板和二合一电脑),如何实现个人工作流的无缝流转呢?例如,在小屏手机上正在记录的文字,可以直接在身边的平板大屏上同一个光标位置继续编辑,完全不需要任何手动保存或分享。

在下一期中,我们将全面切入鸿蒙开发的进阶深水区:Ability Kit 原生应用接续(Continuation),在 EntryAbility 全生命周期重写 onContinue 协议,打造无感的跨端续写体验! 敬请期待。在这里插入图片描述

Logo

一站式 AI 云服务平台

更多推荐