在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

一、前置思考

应用流转(Continuation)是鸿蒙分布式体验的"临门一脚"——用户在手机上编辑到一半的文档,只需在超级终端中点击平板图标,文档就能无缝衔接到平板上继续编辑。这不仅仅是"打开同一个页面",而是页面状态(滚动位置、表单内容、光标位置)完全一致的体验。

本文聚焦:

  • onContinue/onRestore的完整生命周期与最佳实践
  • WantParams传递的序列化限制与突破方案
  • 流转异常恢复的容错设计
  • 多端同时编辑的状态一致性问题

真实痛点场景:

  1. 状态不完整:流转后滚动条回到顶部,表单数据丢失了一部分
  2. 流转中断:网络抖动导致流转到一半失败,两边都处在"半死不活"状态
  3. 重复流转:用户在手机上点了两次流转,平板上弹出两个确认框
  4. 类型丢失:WantParams中的数字在目标设备上变成了字符串

二、核心原理

2.1 流转完整生命周期

                源端(Source)                  目标端(Target)
                    │                              │
  ┌─────────────────┼──────────────────────────────┼─────────────────┐
  │  1.触发阶段      │                              │                 │
  │                 ├── startContinuation()         │                 │
  │                 │   弹出设备选择器              │                 │
  └─────────────────┼──────────────────────────────┼─────────────────┘
                    │                              │
  ┌─────────────────┼──────────────────────────────┼─────────────────┐
  │  2.序列化阶段    │                              │                 │
  │                 ├── onContinue(wantParams)      │                 │
  │                 │   打包所有需要传递的状态      │                 │
  │                 │   返回true → 允许流转         │                 │
  │                 │   返回false → 拒绝流转        │                 │
  └─────────────────┼──────────────────────────────┼─────────────────┘
                    │                              │
  ┌─────────────────┼──────────────────────────────┼─────────────────┐
  │  3.传输阶段      │                              │                 │
  │                 ├── WantParams通过软总线传输──→│                 │
  │                 │   加密传输+完整性校验         │                 │
  └─────────────────┼──────────────────────────────┼─────────────────┘
                    │                              │
  ┌─────────────────┼──────────────────────────────┼─────────────────┐
  │  4.恢复阶段      │                              │                 │
  │                 │                         ┌────┤ onRestore()    │
  │                 │                         │    │ 反序列化状态   │
  │                 │                         │    │ 重建UI         │
  └─────────────────┼──────────────────────────┼────┼─────────────────┘
                    │                         │
  ┌─────────────────┼─────────────────────────┼─────────────────────┐
  │  5.清理阶段      │                         │                     │
  │                 ├── onStop()              │                     │
  │                 ├── onDestroy()           │                     │
  │                 │   释放资源              │   发送ACK确认        │
  └─────────────────┼─────────────────────────┼─────────────────────┘
                    │                         │
                    ▼                         ▼
                 流转完成,源端冻结/销毁      目标端正常运行

2.2 WantParams序列化机制深度解析

WantParams是流转的数据载体,但它有明显的限制:

// WantParams支持的数据类型
type WantParamsValue = string | number | boolean |
  object | undefined | null |
  WantParamsValue[];

// ❌ 不支持的类型
// - Function / Arrow Function → 不可序列化
// - Date → 需要传时间戳,目标端new Date(timestamp)
// - Map / Set → 需转为Array
// - ArrayBuffer → 需Base64编码为string
// - 自定义类实例 → 需提供toJSON()方法

// ✅ 推荐序列化方案
class ContinuationSerializer {
  // 包装不可序列化的数据
  static serializeState(state: EditorState): Record<string, Object> {
    const params: Record<string, Object> = {};
    
    // 基本类型直接赋值
    params['scrollY'] = state.scrollY;
    params['documentId'] = state.documentId;
    
    // Date → 时间戳
    params['lastEditTime'] = state.lastEditTime.getTime();
    
    // 复杂对象 → JSON字符串
    params['formData'] = JSON.stringify(state.formData);
    
    // ArrayBuffer → Base64
    params['thumbnailData'] = this.arrayBufferToBase64(state.thumbnail);
    
    // 回调 → 标记类型(目标端重新绑定)
    params['callbackTypes'] = state.callbackNames;
    
    return params;
  }
  
  // 目标端反序列化
  static restoreState(params: Record<string, Object>): EditorState {
    const state: EditorState = new EditorState();
    
    state.scrollY = params['scrollY'] as number;
    state.documentId = params['documentId'] as string;
    state.lastEditTime = new Date(params['lastEditTime'] as number);
    state.formData = JSON.parse(params['formData'] as string);
    state.thumbnail = this.base64ToArrayBuffer(params['thumbnailData'] as string);
    
    return state;
  }
  
  private static arrayBufferToBase64(buffer: ArrayBuffer): string {
    const bytes: Uint8Array = new Uint8Array(buffer);
    let binary: string = '';
    for (let i: number = 0; i < bytes.byteLength; i++) {
      binary += String.fromCharCode(bytes[i]);
    }
    return btoa(binary);
  }
  
  private static base64ToArrayBuffer(base64: string): ArrayBuffer {
    const binary: string = atob(base64);
    const bytes: Uint8Array = new Uint8Array(binary.length);
    for (let i: number = 0; i < binary.length; i++) {
      bytes[i] = binary.charCodeAt(i);
    }
    return bytes.buffer as ArrayBuffer;
  }
}

2.3 大数据量流转方案

WantParams有200KB限制,大数据场景需要分块方案:

// 方案一:distributedKVStore(适用于频繁读写)
async function transferViaKVStore(
  kvStore: distributedKVStore.SingleKVStore,
  key: string,
  data: string
): Promise<void> {
  await kvStore.put(key, data);
  // WantParams只传key,目标端通过key从KVStore读取
  wantParams['dataKey'] = key;
  wantParams['dataSize'] = data.length;
}

// 方案二:distributedObject(适用于实时同步对象)
const distObj: distributedObject.DistributedObject =
  distributedObject.createDistributedObject();
distObj.setSessionId(sessionId);
distObj['documentData'] = largeDataJson;
// 目标端通过监听对象变化获取数据

// 方案三:分块传输(适用于超大文件)
const CHUNK_SIZE: number = 150 * 1024; // 150KB per chunk
async function transferLargeFile(
  filePath: string,
  targetDeviceId: string
): Promise<void> {
  const totalSize: number = getFileSize(filePath);
  const totalChunks: number = Math.ceil(totalSize / CHUNK_SIZE);
  
  // WantParams传递元数据
  wantParams['fileTransferId'] = this.generateTransferId();
  wantParams['totalChunks'] = totalChunks;
  wantParams['fileName'] = getFileName(filePath);
  
  // 分块通过Session传输
  for (let i: number = 0; i < totalChunks; i++) {
    const chunk: ArrayBuffer = readFileChunk(filePath, i * CHUNK_SIZE, CHUNK_SIZE);
    await session.send(chunk);
  }
}

三、异常恢复完整方案

3.1 流转超时处理

class ContinuationTimeoutGuard {
  private static readonly TIMEOUT_MS: number = 30000; // 30秒超时
  private timerId: number = -1;

  async startWithTimeout(
    continuationPromise: Promise<void>,
    onTimeout: () => void
  ): Promise<void> {
    return new Promise<void>((resolve, reject) => {
      this.timerId = setTimeout(() => {
        onTimeout();
        reject(new Error('流转超时'));
      }, ContinuationTimeoutGuard.TIMEOUT_MS);

      continuationPromise.then(() => {
        clearTimeout(this.timerId);
        resolve();
      }).catch((err: Error) => {
        clearTimeout(this.timerId);
        reject(err);
      });
    });
  }
}

3.2 事务型流转

保证流转的原子性——要么完全成功,要么完全回滚:

class TransactionalContinuation {
  private stateBackup: Record<string, Object> | null = null;

  // 源端:备份+流转
  async migrateState(
    wantParams: Record<string, Object>,
    targetDeviceId: string
  ): Promise<boolean> {
    // 1. 备份当前状态
    this.stateBackup = { ...wantParams };

    try {
      // 2. 执行流转
      await continuationManager.startContinuation({ wantParams });

      // 3. 等待目标端ACK(最多10秒)
      const ack: boolean = await this.waitForAck(10000);
      if (!ack) {
        throw new Error('目标端未确认');
      }

      // 4. 成功 → 清理源端状态
      this.onMigrationSuccess();
      return true;

    } catch (e) {
      // 5. 失败 → 回滚
      this.onMigrationFailure();
      return false;
    }
  }

  private onMigrationSuccess(): void {
    this.stateBackup = null;
    // 清理源端资源
    // 可选:销毁源端页面
  }

  private onMigrationFailure(): void {
    // 恢复备份状态
    if (this.stateBackup !== null) {
      // 将备份的状态恢复到UI
      this.restoreBackup();
    }
  }

  private async waitForAck(timeoutMs: number): Promise<boolean> {
    return new Promise<boolean>((resolve) => {
      const timer: number = setTimeout(() => {
        resolve(false);
      }, timeoutMs);
      // 实际场景中通过软总线监听ACK事件
      // softbus.on('ack', () => { clearTimeout(timer); resolve(true); });
    });
  }
}

3.3 多端状态一致性

当两端同时编辑时,需要处理冲突:

// 使用分布式对象实现多端协作
class CollaborativeEditor {
  private distObj: distributedObject.DistributedObject | null = null;

  initCollaboration(sessionId: number): void {
    this.distObj = distributedObject.createDistributedObject();
    this.distObj.setSessionId(sessionId);
    this.distObj['content'] = '';
    this.distObj['cursorPosition'] = 0;
    this.distObj['version'] = 0;

    // 监听远端修改
    this.distObj.on('status', (session: string, networkId: string, status: string) => {
      if (status === 'changed') {
        this.onRemoteChange();
      }
    });
  }

  // CRDT风格的冲突解决
  private onRemoteChange(): void {
    if (this.distObj === null) return;

    const remoteVersion: number = this.distObj['version'] as number;
    const localVersion: number = this.localVersion;

    if (remoteVersion > localVersion) {
      // 远端更新 → 应用远端内容
      this.documentContent = this.distObj['content'] as string;
      this.localVersion = remoteVersion;
    } else {
      // 本地更新 → 推送到远端
      this.distObj['content'] = this.documentContent;
      this.distObj['version'] = this.localVersion + 1;
    }
  }
}

四、完整代码架构

Demo中的流转模拟架构:

Layer 1: 源端管理
  ├── 状态快照(表单数据/滚动位置/选中项)
  ├── WantParams序列化引擎
  └── 流转触发&设备选择

Layer 2: 传输层
  ├── 数据大小检测(<200KB直接WantParams)
  ├── 大文件分块策略
  └── 传输进度追踪

Layer 3: 目标端恢复
  ├── WantParams反序列化
  ├── UI状态重建
  ├── 回调重新绑定
  └── 资源重新初始化

Layer 4: 异常处理
  ├── 超时回滚
  ├── 网络重试
  └── 状态一致性校验

五、避坑速查

现象 原因 解决
Number变String 流转后数字变成"42" WantParams序列化时类型丢失 onRestore中用Number()/parseInt()显式转换
Boolean变String if(bool)永远为true 同上 val === true || val === 'true'判断
嵌套对象丢失 流转后嵌套字段为空 嵌套对象未JSON.stringify 所有复杂对象先stringify再放入WantParams
图片不显示 流转后头像/缩略图消失 图片资源路径只在本地有效 流转时传图片的Base64或临时文件路径
视频播放中断 流转后从头播放 播放状态未序列化 onContinue中记录currentTime,onRestore中seekTo
两次确认弹窗 目标端弹出两个确认 用户快速双击 加防抖锁,debounce 500ms
流转后黑屏 目标端白屏/黑屏 onRestore中未处理null/undefined 所有取值加空值判断+默认值
WebSocket断开 流转后聊天消息不更新 连接未重连 onRestore中重新建立WebSocket连接
输入法状态丢失 流转后键盘自动弹出 输入法状态不可序列化 onRestore中手动控制focusBehavior
动画卡住 流转后动画停在中间帧 动画状态丢失 onStop中取消动画,onRestore中重新播放

六、总结

应用流转的本质是状态迁移,不是页面迁移:

  1. onContinue = 打包:把当前所有有意义的状态打包进WantParams,返回false可以拒绝流转
  2. WantParams = 信封:200KB限制,复杂数据要JSON.stringify,二进制要Base64
  3. onRestore = 拆包:反序列化→重建UI→重新绑定回调→重新初始化连接
  4. 异常处理 = 安全网:超时回滚、事务保证、空值兜底

一个高质量的流转实现,应该让用户感知不到"迁移"这个过程——就像页面从未离开过。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐