Administrator
发布于 2026-08-30 / 119 阅读
0
0

鸿蒙分布式能力开发:跨设备迁移与数据流转的技术实现

系列文章《鸿蒙开发从入门到专家》第 9 篇(高级篇)

前言

性能优化搞定了,应用在自己手机上跑得飞快。但鸿蒙真正的杀手锏不是单设备体验,而是"万物互联"。

想象一下这些场景:手机上看到一半的视频,随手一划流转到平板继续看;手机上编辑的文档,靠近电脑就自动同步过去;跑步时用手机听歌,回家切到音箱无缝衔接。这不是科幻,是鸿蒙分布式能力的日常。

分布式开发是鸿蒙区别于 Android 和 iOS 的标志性能力。本文从技术实现角度,讲解跨设备迁移和数据流转的完整开发流程。

一、分布式能力的技术底座

1.1 分布式软总线

鸿蒙设备之间通过"分布式软总线"自动发现、自动组网,开发者无需关心底层网络协议。只要设备登录同一华为账号、在同一局域网或开启蓝牙,它们就会自动识别彼此。

手机 ── WiFi/蓝牙 ── 平板
   │                    │
   └── 分布式软总线 ────┘
            │
         智慧屏

1.2 三种分布式场景

场景

说明

典型应用

跨设备迁移

应用从一个设备完全迁移到另一个设备

视频流转、文档编辑

跨设备协同

多个设备同时运行同一应用的不同部分

手机扫码 + 平板展示、手机遥控 + 智慧屏播放

数据流转

数据在不同设备间同步

剪贴板同步、相册同步、待办同步

二、跨设备迁移开发

2.1 启用迁移能力

首先在 module.json5 中声明分布式能力:

{
  "module": {
    "abilities": [
      {
        "name": "EntryAbility",
        "srcEntry": "./ets/entryability/EntryAbility.ets",
        "continueType": [
          {
            "keyword": "video_playback"
          }
        ]
      }
    ]
  }
}

continueType 用于标识迁移类型,系统会根据这个类型匹配可以接收迁移的设备。

2.2 保存与恢复迁移数据

// entryability/EntryAbility.ets
import { AbilityConstant, UIAbility, Want } from '@kit.AbilityKit'
import { window } from '@kit.ArkUI'

export default class EntryAbility extends UIAbility {
  // 页面当前状态
  private currentVideoId: string = ''
  private currentProgress: number = 0

  // 应用需要迁移时触发:保存当前状态
  onContinue(wantParam: Record<string, object>): AbilityConstant.OnContinueResult {
    console.info('准备迁移到另一台设备')
    
    // 将要迁移的数据放入 wantParam
    wantParam['videoId'] = this.currentVideoId
    wantParam['progress'] = this.currentProgress
    wantParam['timestamp'] = Date.now()
    
    return AbilityConstant.OnContinueResult.AGREEMENT
  }

  // 应用在目标设备上恢复时触发
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 判断是否是迁移启动
    if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
      let continueParams = want.parameters as Record<string, object>
      this.currentVideoId = continueParams['videoId'] as string
      this.currentProgress = continueParams['progress'] as number
      
      console.info(`从其他设备迁移过来,恢复视频: ${this.currentVideoId}, 进度: ${this.currentProgress}`)
    }
  }

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      if (!err) {
        // 通知页面恢复状态
        AppStorage.setOrCreate('restoreVideoId', this.currentVideoId)
        AppStorage.setOrCreate('restoreProgress', this.currentProgress)
      }
    })
  }
}

2.3 页面中触发迁移

// pages/VideoPlayerPage.ets
import { continuationManager } from '@kit.AbilityKit'

@Entry
@Component
struct VideoPlayerPage {
  @State videoId: string = ''
  @State progress: number = 0
  @State isPlaying: boolean = false

  // 触发迁移到附近设备
  async continueToOtherDevice() {
    try {
      // 获取可迁移的设备列表
      let devices = await continuationManager.getContinuationDevices()
      
      if (devices.length === 0) {
        promptAction.showToast({ message: '附近没有可迁移的设备' })
        return
      }

      // 选择第一个可用设备(实际应用应展示设备选择弹窗)
      let targetDevice = devices[0]
      
      // 发起迁移
      await continuationManager.startContinuationDeviceManager(
        targetDevice.deviceId,
        {
          videoId: this.videoId,
          progress: this.progress
        }
      )
      
      // 迁移成功后,本设备可以关闭或暂停
      this.isPlaying = false
      promptAction.showToast({ message: '已迁移到 ' + targetDevice.deviceName })
    } catch (err) {
      console.error('迁移失败:', err.message)
      promptAction.showToast({ message: '迁移失败' })
    }
  }

  build() {
    Column() {
      // 视频播放器组件
      VideoPlayer({
        videoId: this.videoId,
        progress: this.progress,
        isPlaying: this.isPlaying
      })

      Row({ space: 16 }) {
        Button(this.isPlaying ? '暂停' : '播放')
          .onClick(() => this.isPlaying = !this.isPlaying)
        
        Button('迁移到平板')
          .onClick(() => this.continueToOtherDevice())
      }
    }
  }
}

2.4 API 26 迁移能力增强

HarmonyOS NEXT 5.0 对迁移能力做了多项增强:

  • 同应用跨 Ability 迁移:不再局限于同一个 EntryAbility,可以迁移到应用内的其他 Ability

  • 异步保存数据:onContinue 支持异步接口,可以保存更大量的数据

  • 立即启动字段:应用可配置 continueInstantly,在 pull 场景下提前启动目标应用

// 异步保存大量数据
async onContinue(wantParam: Record<string, object>): Promise<AbilityConstant.OnContinueResult> {
  // 异步保存用户编辑内容
  let content = await this.loadDocumentContent()
  wantParam['documentContent'] = content
  wantParam['documentVersion'] = this.version
  
  return AbilityConstant.OnContinueResult.AGREEMENT
}

三、跨设备数据流转

迁移是"整个应用搬过去",而数据流转是"数据在多个设备间同步"。后者的应用场景更广泛。

3.1 分布式 KVStore 实现剪贴板同步

import { distributedKVStore } from '@kit.ArkData'

class CrossDeviceClipboard {
  private kvStore: distributedKVStore.SingleKVStore | null = null

  async init(): Promise<void> {
    let context = getContext()
    let kvManager = distributedKVStore.createKVManager({
      context: context,
      bundleName: context.applicationInfo.name
    })

    this.kvStore = await kvManager.getKVStore('clipboard_sync', {
      createIfMissing: true,
      encrypt: true,           // 加密传输
      autoSync: true,          // 自动同步
      kvStoreType: distributedKVStore.KVStoreType.SINGLE_VERSION,
      securityLevel: distributedKVStore.SecurityLevel.S2
    })

    // 监听远程数据变化
    this.kvStore.on('dataChange', distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_REMOTE, (entries) => {
      for (let entry of entries) {
        if (entry.key === 'clipboard_content') {
          let content = entry.value.value as string
          console.info('收到其他设备的剪贴板内容:', content)
          // 写入本机剪贴板
          this.setLocalClipboard(content)
        }
      }
    })
  }

  async syncClipboard(content: string): Promise<void> {
    await this.kvStore?.put('clipboard_content', content)
    await this.kvStore?.put('clipboard_timestamp', Date.now())
  }

  private setLocalClipboard(content: string): void {
    // 调用系统剪贴板 API
    // ...
  }
}

3.2 分布式关系型数据库实现待办同步

import { relationalStore } from '@kit.ArkData'

class DistributedTodoDB {
  private rdbStore: relationalStore.RdbStore | null = null

  async init(): Promise<void> {
    let context = getContext()
    
    const config: relationalStore.StoreConfig = {
      name: 'distributed_todo.db',
      securityLevel: relationalStore.SecurityLevel.S2,
      customDir: 'distributed'  // 关键:启用分布式目录
    }

    this.rdbStore = await relationalStore.getRdbStore(context, config)

    await this.rdbStore.executeSql(`
      CREATE TABLE IF NOT EXISTS todos (
        id TEXT PRIMARY KEY,
        title TEXT NOT NULL,
        completed INTEGER DEFAULT 0,
        create_time INTEGER,
        update_time INTEGER,
        device_id TEXT
      )
    `)

    // 订阅数据同步事件
    this.rdbStore.on('dataChange', (event) => {
      console.info('数据已同步,变更类型:', event)
      // 通知 UI 刷新
      AppStorage.setOrCreate('todoSyncTrigger', Date.now())
    })
  }

  async addTodo(title: string): Promise<void> {
    let uuid = generateUUID()
    let values = {
      id: uuid,
      title: title,
      completed: 0,
      create_time: Date.now(),
      update_time: Date.now(),
      device_id: getDeviceId()
    }
    await this.rdbStore?.insert('todos', values)
    // insert 后会自动触发分布式同步
  }

  async toggleTodo(id: string, completed: boolean): Promise<void> {
    let values = {
      completed: completed ? 1 : 0,
      update_time: Date.now()
    }
    let predicates = new relationalStore.RdbPredicates('todos')
    predicates.equalTo('id', id)
    await this.rdbStore?.update(values, predicates)
  }

  async queryAllTodos(): Promise<TodoItem[]> {
    let predicates = new relationalStore.RdbPredicates('todos')
    predicates.orderByDesc('update_time')
    
    let resultSet = await this.rdbStore?.query(predicates)
    let todos: TodoItem[] = []
    while (resultSet?.goToNextRow()) {
      todos.push({
        id: resultSet.getString(resultSet.getColumnIndex('id')),
        title: resultSet.getString(resultSet.getColumnIndex('title')),
        completed: resultSet.getLong(resultSet.getColumnIndex('completed')) === 1,
        updateTime: resultSet.getLong(resultSet.getColumnIndex('update_time'))
      })
    }
    resultSet?.close()
    return todos
  }
}

3.3 冲突解决策略

分布式数据同步难免遇到冲突——同一时刻两台设备修改了同一条数据。鸿蒙默认采用"后写入获胜"(Last-Write-Wins)策略,但复杂场景需要自定义:

// 自定义冲突解决
this.kvStore?.on('syncComplete', (results) => {
  for (let result of results) {
    if (result.errorMessage) {
      console.error('同步出错:', result.errorMessage)
      continue
    }
    
    // 检查是否有冲突
    if (result.conflictCount > 0) {
      this.resolveConflicts(result.storeId)
    }
  }
})

private async resolveConflicts(storeId: string): Promise<void> {
  // 读取冲突数据
  let conflicts = await this.kvStore?.getConflictList(storeId)
  
  for (let conflict of conflicts || []) {
    let localValue = conflict.localValue
    let remoteValue = conflict.remoteValue
    
    // 自定义合并策略:取 update_time 更大的
    if (localValue.update_time > remoteValue.update_time) {
      await this.kvStore?.put(conflict.key, localValue)
    } else {
      await this.kvStore?.put(conflict.key, remoteValue)
    }
  }
}

四、设备发现与选择 UI

让用户手动选择目标设备是更好的体验:

@CustomDialog
struct DevicePickerDialog {
  @State devices: continuationManager.ContinuationDevice[] = []
  private controller: CustomDialogController
  private onDeviceSelected?: (device: continuationManager.ContinuationDevice) => void

  aboutToAppear() {
    this.loadDevices()
  }

  async loadDevices() {
    try {
      this.devices = await continuationManager.getContinuationDevices()
    } catch (err) {
      console.error('获取设备列表失败:', err)
    }
  }

  build() {
    Column({ space: 16 }) {
      Text('选择设备')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)

      if (this.devices.length === 0) {
        Text('附近没有可连接的设备')
          .fontSize(14)
          .fontColor('#999999')
      } else {
        List() {
          ForEach(this.devices, (device) => {
            ListItem() {
              Row({ space: 12 }) {
                Image(device.deviceIcon || $r('app.media.icon_device'))
                  .width(40)
                  .height(40)
                Column({ space: 4 }) {
                  Text(device.deviceName)
                    .fontSize(16)
                  Text(device.deviceType)
                    .fontSize(12)
                    .fontColor('#999999')
                }
                .alignItems(HorizontalAlign.Start)
                .layoutWeight(1)
              }
              .width('100%')
              .padding(12)
              .onClick(() => {
                if (this.onDeviceSelected) {
                  this.onDeviceSelected(device)
                }
                this.controller.close()
              })
            }
          })
        }
        .width('100%')
        .maxHeight(300)
      }
    }
    .padding(24)
    .backgroundColor(Color.White)
    .borderRadius(16)
  }
}

五、权限与安全

分布式能力需要额外的权限声明:

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.DISTRIBUTED_DATASYNC"
      },
      {
        "name": "ohos.permission.DISTRIBUTED_DEVICE_STATE_CHANGE"
      }
    ]
  }
}

安全注意事项:

  • 敏感数据务必开启加密(encrypt: true)

  • 分布式数据只在同一华为账号的设备间同步

  • 涉及隐私的数据(如位置、通讯录)需用户二次确认

  • 遵守各地区数据合规要求(GDPR、个人信息保护法等)

小结与下篇预告

本文覆盖了鸿蒙分布式开发的两个核心场景:

  1. 跨设备迁移:通过 onContinue 保存状态,目标设备 onCreate 中恢复,适合视频播放、文档编辑等场景

  2. 数据流转:通过分布式 KVStore 或 RDB 实现数据自动同步,适合待办、笔记、剪贴板等场景

分布式开发的核心心智模型是:设备之间是"自动发现、自动组网"的,开发者只需要关注"什么数据要同步"和"冲突怎么解决"。

下一篇(第 10 篇)是系列最后一篇,进入专家级内容:大型鸿蒙应用的架构设计。我们会讨论模块化拆分(HAP/HSP/HAR)、状态管理分层、MVVM 模式落地,以及工程化最佳实践。

参考阅读


评论