一、前置思考应用流转Continuation是鸿蒙分布式体验的临门一脚——用户在手机上编辑到一半的文档只需在超级终端中点击平板图标文档就能无缝衔接到平板上继续编辑。这不仅仅是打开同一个页面而是页面状态滚动位置、表单内容、光标位置完全一致的体验。本文聚焦onContinue/onRestore的完整生命周期与最佳实践WantParams传递的序列化限制与突破方案流转异常恢复的容错设计多端同时编辑的状态一致性问题真实痛点场景状态不完整流转后滚动条回到顶部表单数据丢失了一部分流转中断网络抖动导致流转到一半失败两边都处在半死不活状态重复流转用户在手机上点了两次流转平板上弹出两个确认框类型丢失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支持的数据类型typeWantParamsValuestring|number|boolean|object|undefined|null|WantParamsValue[];// ❌ 不支持的类型// - Function / Arrow Function → 不可序列化// - Date → 需要传时间戳目标端new Date(timestamp)// - Map / Set → 需转为Array// - ArrayBuffer → 需Base64编码为string// - 自定义类实例 → 需提供toJSON()方法// ✅ 推荐序列化方案classContinuationSerializer{// 包装不可序列化的数据staticserializeState(state:EditorState):Recordstring,Object{constparams:Recordstring,Object{};// 基本类型直接赋值params[scrollY]state.scrollY;params[documentId]state.documentId;// Date → 时间戳params[lastEditTime]state.lastEditTime.getTime();// 复杂对象 → JSON字符串params[formData]JSON.stringify(state.formData);// ArrayBuffer → Base64params[thumbnailData]this.arrayBufferToBase64(state.thumbnail);// 回调 → 标记类型目标端重新绑定params[callbackTypes]state.callbackNames;returnparams;}// 目标端反序列化staticrestoreState(params:Recordstring,Object):EditorState{conststate:EditorStatenewEditorState();state.scrollYparams[scrollY]asnumber;state.documentIdparams[documentId]asstring;state.lastEditTimenewDate(params[lastEditTime]asnumber);state.formDataJSON.parse(params[formData]asstring);state.thumbnailthis.base64ToArrayBuffer(params[thumbnailData]asstring);returnstate;}privatestaticarrayBufferToBase64(buffer:ArrayBuffer):string{constbytes:Uint8ArraynewUint8Array(buffer);letbinary:string;for(leti:number0;ibytes.byteLength;i){binaryString.fromCharCode(bytes[i]);}returnbtoa(binary);}privatestaticbase64ToArrayBuffer(base64:string):ArrayBuffer{constbinary:stringatob(base64);constbytes:Uint8ArraynewUint8Array(binary.length);for(leti:number0;ibinary.length;i){bytes[i]binary.charCodeAt(i);}returnbytes.bufferasArrayBuffer;}}2.3 大数据量流转方案WantParams有200KB限制大数据场景需要分块方案// 方案一distributedKVStore适用于频繁读写asyncfunctiontransferViaKVStore(kvStore:distributedKVStore.SingleKVStore,key:string,data:string):Promisevoid{awaitkvStore.put(key,data);// WantParams只传key目标端通过key从KVStore读取wantParams[dataKey]key;wantParams[dataSize]data.length;}// 方案二distributedObject适用于实时同步对象constdistObj:distributedObject.DistributedObjectdistributedObject.createDistributedObject();distObj.setSessionId(sessionId);distObj[documentData]largeDataJson;// 目标端通过监听对象变化获取数据// 方案三分块传输适用于超大文件constCHUNK_SIZE:number150*1024;// 150KB per chunkasyncfunctiontransferLargeFile(filePath:string,targetDeviceId:string):Promisevoid{consttotalSize:numbergetFileSize(filePath);consttotalChunks:numberMath.ceil(totalSize/CHUNK_SIZE);// WantParams传递元数据wantParams[fileTransferId]this.generateTransferId();wantParams[totalChunks]totalChunks;wantParams[fileName]getFileName(filePath);// 分块通过Session传输for(leti:number0;itotalChunks;i){constchunk:ArrayBufferreadFileChunk(filePath,i*CHUNK_SIZE,CHUNK_SIZE);awaitsession.send(chunk);}}三、异常恢复完整方案3.1 流转超时处理classContinuationTimeoutGuard{privatestaticreadonlyTIMEOUT_MS:number30000;// 30秒超时privatetimerId:number-1;asyncstartWithTimeout(continuationPromise:Promisevoid,onTimeout:()void):Promisevoid{returnnewPromisevoid((resolve,reject){this.timerIdsetTimeout((){onTimeout();reject(newError(流转超时));},ContinuationTimeoutGuard.TIMEOUT_MS);continuationPromise.then((){clearTimeout(this.timerId);resolve();}).catch((err:Error){clearTimeout(this.timerId);reject(err);});});}}3.2 事务型流转保证流转的原子性——要么完全成功要么完全回滚classTransactionalContinuation{privatestateBackup:Recordstring,Object|nullnull;// 源端备份流转asyncmigrateState(wantParams:Recordstring,Object,targetDeviceId:string):Promiseboolean{// 1. 备份当前状态this.stateBackup{...wantParams};try{// 2. 执行流转awaitcontinuationManager.startContinuation({wantParams});// 3. 等待目标端ACK最多10秒constack:booleanawaitthis.waitForAck(10000);if(!ack){thrownewError(目标端未确认);}// 4. 成功 → 清理源端状态this.onMigrationSuccess();returntrue;}catch(e){// 5. 失败 → 回滚this.onMigrationFailure();returnfalse;}}privateonMigrationSuccess():void{this.stateBackupnull;// 清理源端资源// 可选销毁源端页面}privateonMigrationFailure():void{// 恢复备份状态if(this.stateBackup!null){// 将备份的状态恢复到UIthis.restoreBackup();}}privateasyncwaitForAck(timeoutMs:number):Promiseboolean{returnnewPromiseboolean((resolve){consttimer:numbersetTimeout((){resolve(false);},timeoutMs);// 实际场景中通过软总线监听ACK事件// softbus.on(ack, () { clearTimeout(timer); resolve(true); });});}}3.3 多端状态一致性当两端同时编辑时需要处理冲突// 使用分布式对象实现多端协作classCollaborativeEditor{privatedistObj:distributedObject.DistributedObject|nullnull;initCollaboration(sessionId:number):void{this.distObjdistributedObject.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(statuschanged){this.onRemoteChange();}});}// CRDT风格的冲突解决privateonRemoteChange():void{if(this.distObjnull)return;constremoteVersion:numberthis.distObj[version]asnumber;constlocalVersion:numberthis.localVersion;if(remoteVersionlocalVersion){// 远端更新 → 应用远端内容this.documentContentthis.distObj[content]asstring;this.localVersionremoteVersion;}else{// 本地更新 → 推送到远端this.distObj[content]this.documentContent;this.distObj[version]this.localVersion1;}}}四、完整代码架构Demo中的流转模拟架构Layer 1: 源端管理 ├── 状态快照表单数据/滚动位置/选中项 ├── WantParams序列化引擎 └── 流转触发设备选择 Layer 2: 传输层 ├── 数据大小检测200KB直接WantParams ├── 大文件分块策略 └── 传输进度追踪 Layer 3: 目标端恢复 ├── WantParams反序列化 ├── UI状态重建 ├── 回调重新绑定 └── 资源重新初始化 Layer 4: 异常处理 ├── 超时回滚 ├── 网络重试 └── 状态一致性校验五、避坑速查坑现象原因解决Number变String流转后数字变成42WantParams序列化时类型丢失onRestore中用Number()/parseInt()显式转换Boolean变Stringif(bool)永远为true同上用val true || val true判断嵌套对象丢失流转后嵌套字段为空嵌套对象未JSON.stringify所有复杂对象先stringify再放入WantParams图片不显示流转后头像/缩略图消失图片资源路径只在本地有效流转时传图片的Base64或临时文件路径视频播放中断流转后从头播放播放状态未序列化onContinue中记录currentTimeonRestore中seekTo两次确认弹窗目标端弹出两个确认用户快速双击加防抖锁debounce 500ms流转后黑屏目标端白屏/黑屏onRestore中未处理null/undefined所有取值加空值判断默认值WebSocket断开流转后聊天消息不更新连接未重连onRestore中重新建立WebSocket连接输入法状态丢失流转后键盘自动弹出输入法状态不可序列化onRestore中手动控制focusBehavior动画卡住流转后动画停在中间帧动画状态丢失onStop中取消动画onRestore中重新播放六、总结应用流转的本质是状态迁移不是页面迁移onContinue 打包把当前所有有意义的状态打包进WantParams返回false可以拒绝流转WantParams 信封200KB限制复杂数据要JSON.stringify二进制要Base64onRestore 拆包反序列化→重建UI→重新绑定回调→重新初始化连接异常处理 安全网超时回滚、事务保证、空值兜底一个高质量的流转实现应该让用户感知不到迁移这个过程——就像页面从未离开过。