鸿蒙跨设备实时绘图同步:从零到一实现分布式画板

完整代码以整理:DrawTogether

一、为什么我要写这篇帖子?

你是否想过:手机A上随手画一笔,平板B立刻显示相同的笔迹,就像两块画布被神奇地连接在了一起?这个场景常见于远程辅导、在线会议、设计协作等。传统做法需要搭建WebSocket、处理设备发现、解决网络抖动......繁琐且容易出错。

而鸿蒙的分布式能力宣称可以"让多设备像一台设备一样协同"。那么,只用鸿蒙原生API,不依赖任何第三方服务,能否实现实时绘图同步? 答案是肯定的。我花了一天多时间,从零写了一个"DrawTogether"应用,最终效果:

  • 设备自动发现(登录同一华为账号即可)
  • 实时同步延迟 < 50ms(用户无感知)
  • 曲线平滑(每个点立即发送,无折线感)
  • 支持颜色/粗细切换、清空双向同步
平板绘画 手机同步

平板没有录屏截了一张图,手机的是录屏 平板绘制,手机实时同步。

实现画板功能并不是我的主要目的、能实现两台设备之间实时同步数据才是我的目的。所以一开始做的时候,画板我只写了简单的功能能画就行,能清空既可以每画一笔多台设备同步显示。本文将完整记录技术选型、核心代码、踩坑经验与优化策略,希望能帮你少走弯路。

二、技术选型与架构

鸿蒙提供了三种分布式数据方案:键值型数据库(KVStore)关系型数据库(RDB)分布式数据对象(DataObject)

2.1 为什么不用分布式数据对象?

我最初被"分布式数据对象"吸引------它用起来就像本地变量一样简单。但文档里一行小字让我心凉半截:

当前仅分布式迁移场景对第三方应用开放Call调用权限,其余所有Call调用场景均限定为系统内部调用。

对于第三方应用的多端协同(如两人同时观看绘图),Call调用受到权限限制。因此,我放弃了分布式数据对象。

2.2 为什么不选关系型数据库(RDB)?

关系型数据库(RDB)虽然也支持分布式同步,但它适用于结构化数据和复杂查询(如多条件筛选、关联统计)。而绘图同步场景的特点完全相反:

维度 绘图同步需求 RDB 特点 结论
数据模型 键值对(笔画ID → 点数组) 表格、字段、索引 KVStore 更匹配
写入频率 极高(每秒 60~100 次) 事务、锁、索引维护开销大 KVStore 更轻量
查询方式 仅按笔画ID或时间顺序读取 支持复杂SQL 不需要复杂查询
同步粒度 每次增量同步一个点或一条消息 表级同步(或带条件同步) KVStore 更精细
开发复杂度 put/get 两行代码 需预定义表结构、写SQL KVStore 更简单

此外,RDB 的分布式同步机制相对"重"。而 KVStore 专为高频读写和低延迟场景设计,且支持设备协同模式(DEVICE_COLLABORATION),自动按设备隔离数据,天然避免多端同时写入同一 key 的冲突。

因此,对于高频追加、无需复杂查询、追求实时性的绘图同步场景,KVStore 是最佳选择。

2.3 最终方案

能力 选型 原因
数据存储与同步 DeviceKVStore + 手动sync 多设备协同模式,自动按设备隔离数据,手动同步可控
绘图协议 增量消息(BEGIN / DELTA / END / CLEAR) 支持实时追加,减少传输量
设备发现 distributedDeviceManager 获取在线可信设备列表,无需手动配对
UI框架 ArkTS + Canvas 响应式状态管理,高性能绘图

2.4 运行环境要求

  • 两台鸿蒙5.0+设备登录同一华为账号
  • 开启WLAN和蓝牙 ,并允许多设备协同
  • 应用动态申请ohos.permission.DISTRIBUTED_DATASYNC权限

2.5 工程目录

复制代码
DrawTogether/
└── AppScope/
    └── entry/
        └── src/main/
            ├── ets/
            │   ├── pages/           # 主页面
            │   │   └── Index.ets
            │   ├── components/      # 通用UI组件
            │   │   ├── DrawingBoard.ets    # 画板核心
            │   │   └── Toolbar.ets         # 工具栏
            │   ├── model/           # 数据结构
            │   │   ├── DrawPoint.ets
            │   │   └── DrawStroke.ets
            │   ├── service/         # 全局服务(同步、分布式)
            │   │   ├── SyncService.ets
            │   │   └── DistributedService.ets
            │   ├── constants/           # 常量
            │   │   └── AppConstants.ets
            │   ├── manager/           # 权限管理
            │   │   └── PermissionManager.ets
            │   ├── utils/           # 工具类
            │   │   └── Logger.ets
            │   └── entryability/    # 应用入口
            ├── resources/           # 资源文件
            └── module.json5         # 模块配置

三、核心实现:从数据模型到画板组件

3.1 数据模型

定义点、笔画和消息类型,为后续同步打下基础。

javascript 复制代码
// 点坐标
export interface DrawPoint {
    x: number;
    y: number;
}

// 完整笔画(包含唯一ID和点序列)
export interface DrawStroke {
    id: string;
    points: DrawPoint[];
}

// 消息类型枚举
export enum MessageType {
    BEGIN = 0,   // 开始新笔画(包含起点)
    DELTA = 1,   // 增量点(单个点)
    END = 2,     // 结束笔画
    CLEAR = 3    // 清空画板
}

// 消息格式
export interface DrawingMessage {
    type: MessageType;
    strokeId?: string;
    points?: DrawPoint[];
    timestamp: number;
}

3.2 分布式服务封装(DistributedService)

负责KVStore初始化、设备列表获取、数据变化监听和消息发送。注意 :只处理insertEntries,因为每条消息的key都是唯一的(draw_${strokeId}_${timestamp}),不会触发updateEntries,这样可以避免重复回调。

javascript 复制代码
import { distributedKVStore } from '@kit.ArkData';
import { distributedDeviceManager } from '@kit.DistributedServiceKit';
import { common } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';
import { Logger } from '../utils/Logger';
import { AppConstants } from '../constants/AppConstants';

const TAG = 'DistributedService';

// 定义数据变化回调函数类型
export type DataChangeCallback = (key: string, value: string) => void;

export class DistributedService {
  private static instance: DistributedService;
  private kvManager?: distributedKVStore.KVManager;
  private kvStore?: distributedKVStore.DeviceKVStore;
  private deviceManager?: distributedDeviceManager.DeviceManager;
  private isInitialized: boolean = false;

  // 保存当前注册的数据变化回调,以便在 off 时移除
  private currentDataChangeCallback?: DataChangeCallback;

  static getInstance(): DistributedService {
    if (!DistributedService.instance) {
      DistributedService.instance = new DistributedService();
    }
    return DistributedService.instance;
  }

  async init(context: common.UIAbilityContext): Promise<boolean> {
    if (this.isInitialized) {
      Logger.info(TAG, 'Already initialized');
      return true;
    }
    try {
      const kvManagerConfig: distributedKVStore.KVManagerConfig = {
        bundleName: context.applicationInfo.name,
        context: context
      };

      this.kvManager = distributedKVStore.createKVManager(kvManagerConfig);
      const options: distributedKVStore.Options = {
        createIfMissing: true,
        encrypt: false,
        autoSync: true,
        kvStoreType: distributedKVStore.KVStoreType.DEVICE_COLLABORATION,
        securityLevel: distributedKVStore.SecurityLevel.S1
      };
      this.kvStore = await this.kvManager.getKVStore<distributedKVStore.DeviceKVStore>(
        AppConstants.KV_STORE_ID, options
      );
      this.deviceManager = distributedDeviceManager.createDeviceManager(context.applicationInfo.name);
      this.isInitialized = true;
      Logger.info(TAG, 'DistributedService initialized successfully');
      return true;
    } catch (err) {
      const error = err as BusinessError;
      Logger.error(TAG, `Init failed: ${error.code} - ${error.message}`);
      return false;
    }
  }

  // 获取完整设备信息
  getOnlineTrustedDevices(): distributedDeviceManager.DeviceBasicInfo[] {
    if (!this.deviceManager) return [];
    try {
      return this.deviceManager.getAvailableDeviceListSync();
    } catch (err) {
      Logger.error(TAG, `Get devices failed: ${err}`);
      return [];
    }
  }

  // 获取设备 networkId 列表
  getOnlineTrustedDeviceIds(): string[] {
    const devices = this.getOnlineTrustedDevices();
    return devices
      .map((device: distributedDeviceManager.DeviceBasicInfo) => device.networkId)
      .filter((id) => typeof id === 'string') as string[];
  }
  /**
   * 订阅数据变化
   * @param callback 数据变化回调函数
   */
  onDataChange(callback: DataChangeCallback): void {
    if (!this.kvStore) {
      Logger.error(TAG, 'KVStore not ready, cannot subscribe');
      return;
    }
    // 保存回调引用,以便后续取消订阅
    this.currentDataChangeCallback = callback;
    try {
      this.kvStore.on('dataChange', distributedKVStore.SubscribeType.SUBSCRIBE_TYPE_ALL, (data) => {
       // 只更新插入的信值
        if (data.insertEntries && data.insertEntries.length > 0) {
          for (let i = 0; i < data.insertEntries.length; i++) {
            const entry = data.insertEntries[i];
            // 确保 value 存在且能够转换为字符串
            if (entry.value && entry.value.value !== undefined) {
              callback(entry.key, entry.value.value.toString());
            }
          }
        }
      });
      Logger.info(TAG, 'Data change listener registered');
    } catch (error) {
      Logger.error(TAG, `Failed to register data change listener: ${error}`);
    }
  }

  /**
   * 取消订阅数据变化
   * 应在组件销毁时调用,避免内存泄漏
   */
  offDataChange(): void {
    if (!this.kvStore) {
      Logger.warn(TAG, 'KVStore not ready, cannot unregister');
      return;
    }
    try {
      // 移除之前注册的监听器
      this.kvStore.off('dataChange');
      this.currentDataChangeCallback = undefined;
      Logger.info(TAG, 'Data change listener unregistered');
    } catch (error) {
      Logger.error(TAG, `Failed to unregister data change listener: ${error}`);
    }
  }

  // 发送消息(写入 + 立即手动同步)
  async sendMessage(key: string, value: string): Promise<boolean> {
    if (!this.kvStore) {
      Logger.error(TAG, 'KVStore not ready, cannot send');
      return false;
    }
    try {
      await this.kvStore.put(key, value);
      const deviceIds = this.getOnlineTrustedDeviceIds();
      if (deviceIds.length > 0) {
        // 手动同步,立即推送
        this.kvStore.sync(deviceIds, distributedKVStore.SyncMode.PUSH_ONLY, AppConstants.SYNC_DELAY_MS);
      }
      return true;
    } catch (err) {
      Logger.error(TAG, `Send message failed: ${err}`);
      return false;
    }
  }

  isReady(): boolean {
    return this.isInitialized && this.kvStore !== undefined;
  }
}

3.3 同步协议实现(SyncService)------ 最核心的部分

发送端:每个点立即发送,保证曲线平滑。

接收端:维护笔画状态,实现消息去重,防止跨笔画错误连接。

javascript 复制代码
import { DrawingMessage, MessageType } from '../model/DrawingMessage';
import { DrawPoint } from '../model/DrawStroke';
import { DistributedService } from '../services/DistributedService';
import { Logger } from '../utils/Logger';

const TAG = 'SyncService';

export interface SyncCallback {
  onStrokeBegin: (strokeId: string, firstPoint: DrawPoint) => void;
  onStrokeDelta: (strokeId: string, points: DrawPoint[]) => void;
  onStrokeEnd: (strokeId: string) => void;
  onClear?: () => void;
}

export class SyncService {
  private static instance: SyncService;
  private distributedService: DistributedService;
  private callback?: SyncCallback;
  private initialized: boolean = false;

  // 发送端
  private currentStrokeId: string = '';

  // 接收端状态
  private receivingStrokeId: string = '';
  private receivingPoints: DrawPoint[] = [];

  // 消息去重集合
  private processedKeys: Set<string> = new Set();

  private constructor() {
    this.distributedService = DistributedService.getInstance();
  }

  static getInstance(): SyncService {
    if (!SyncService.instance) {
      SyncService.instance = new SyncService();
    }
    return SyncService.instance;
  }

  init(callback?: SyncCallback): void {
    if (this.initialized) {
      Logger.warn(TAG, 'SyncService already initialized');
      return;
    }
    this.callback = callback;
    this.distributedService.onDataChange((key: string, value: string) => {
      if (key.startsWith('draw_')) {
        this.handleRemoteMessage(key, value);
      }
    });
    this.initialized = true;
    Logger.info(TAG, 'SyncService initialized');
  }

  setCallback(callback?: SyncCallback): void {
    this.callback = callback;
  }

  isInitialized(): boolean {
    return this.initialized && this.distributedService.isReady();
  }

  // 发送端接口 
  startStroke(firstPoint: DrawPoint): void {
    if (!this.isInitialized()) return;
    this.currentStrokeId = `${Date.now()}_${Math.random().toString(36).substring(2, 8)}`;
    const msg: DrawingMessage = {
      type: MessageType.BEGIN,
      strokeId: this.currentStrokeId,
      points: [firstPoint],
      timestamp: Date.now()
    };
    this.sendMessage(msg);
  }

  // 每个点立即发送 
  addPoint(point: DrawPoint): void {
    if (!this.isInitialized() || !this.currentStrokeId) return;
    const msg: DrawingMessage = {
      type: MessageType.DELTA,
      strokeId: this.currentStrokeId,
      points: [point],
      timestamp: Date.now()
    };
    this.sendMessage(msg);
  }

  endStroke(): void {
    if (!this.isInitialized() || !this.currentStrokeId) return;
    const msg: DrawingMessage = {
      type: MessageType.END,
      strokeId: this.currentStrokeId,
      points: [],
      timestamp: Date.now()
    };
    this.sendMessage(msg);
    this.currentStrokeId = '';
  }

  sendClear(): void {
    if (!this.isInitialized()) return;
    const msg: DrawingMessage = {
      type: MessageType.CLEAR,
      timestamp: Date.now()
    };
    this.sendMessage(msg);
  }

  private async sendMessage(msg: DrawingMessage): Promise<void> {
    const key = `draw_${msg.strokeId ?? 'clear'}_${msg.timestamp}`;
    const value = JSON.stringify(msg);
    await this.distributedService.sendMessage(key, value);
  }

  // 接收端处理
  public handleRemoteMessage(key: string, value: string): void {
    // 去重
    if (this.processedKeys.has(key)) {
      Logger.debug(TAG, `Duplicate message ignored: ${key}`);
      return;
    }
    this.processedKeys.add(key);
    if (this.processedKeys.size > 500) {
      const entries = Array.from(this.processedKeys);
      for (let i = 0; i < 250; i++) {
        this.processedKeys.delete(entries[i]);
      }
    }

    try {
      const msg: DrawingMessage = JSON.parse(value);
      switch (msg.type) {
        case MessageType.BEGIN:
          this.receiveBegin(msg.strokeId!, msg.points!);
          break;
        case MessageType.DELTA:
          this.receiveDelta(msg.strokeId!, msg.points!);
          break;
        case MessageType.END:
          this.receiveEnd(msg.strokeId!);
          break;
        case MessageType.CLEAR:
          this.receiveClear();
          break;
        default:
          Logger.warn(TAG, `Unknown message type: ${msg.type}`);
      }
    } catch (err) {
      Logger.error(TAG, `Parse message failed: ${err}, value=${value}`);
    }
  }

  private receiveBegin(strokeId: string, points: DrawPoint[]): void {
    if (points.length === 0) return;
    if (this.receivingStrokeId === strokeId) {
      Logger.warn(TAG, `receiveBegin: duplicate BEGIN for stroke ${strokeId}, ignored`);
      return;
    }
    if (this.receivingStrokeId) {
      Logger.warn(TAG, `receiveBegin: force ending previous stroke ${this.receivingStrokeId}`);
      this.callback?.onStrokeEnd(this.receivingStrokeId);
    }
    this.receivingStrokeId = strokeId;
    this.receivingPoints = [];
    this.receivingPoints.push(...points);
    this.callback?.onStrokeBegin(strokeId, points[0]);
    for (let i = 1; i < points.length; i++) {
      this.callback?.onStrokeDelta(strokeId, [points[i-1], points[i]]);
    }
  }

  private receiveDelta(strokeId: string, points: DrawPoint[]): void {
    if (strokeId !== this.receivingStrokeId) {
      Logger.warn(TAG, `receiveDelta: strokeId mismatch, expected ${this.receivingStrokeId}, got ${strokeId}`);
      return;
    }
    if (points.length === 0) return;
    if (this.receivingPoints.length > 0) {
      const lastPoint = this.receivingPoints[this.receivingPoints.length - 1];
      const firstNewPoint = points[0];
      this.callback?.onStrokeDelta(strokeId, [lastPoint, firstNewPoint]);
    }
    for (let i = 1; i < points.length; i++) {
      this.callback?.onStrokeDelta(strokeId, [points[i-1], points[i]]);
    }
    this.receivingPoints.push(...points);
  }

  private receiveEnd(strokeId: string): void {
    if (strokeId !== this.receivingStrokeId) return;
    this.callback?.onStrokeEnd(strokeId);
    this.receivingStrokeId = '';
    this.receivingPoints = [];
  }

  private receiveClear(): void {
    Logger.info(TAG, 'receiveClear');
    this.callback?.onClear?.();
    this.receivingStrokeId = '';
    this.receivingPoints = [];
  }
}

3.4 画板组件(DrawingBoard)

使用Canvas绘图,通过@Prop接收远端笔画数据,@Watch监听变化并重绘。需要注意到是,一开始我们的方案是每5个点,20ms 发送一次,但是真的会卡线条。修改方案实时发送,因为数据量不大只有xy还有id点信息这个方案可行。

javascript 复制代码
import { DrawPoint, DrawStroke } from '../model/DrawStroke';
import { SyncService } from '../services/SyncService';

@Component
export struct DrawingBoard {
  @Prop @Watch('onPenColorChanged') penColor: string = '#000000';
  @Prop @Watch('onPenWidthChanged') penWidth: number = 5;
  @Prop @Watch('onClearTriggerChanged') clearTrigger: number = 0;
  @Prop @Watch('onRemoteStrokesChanged') remoteStrokes: DrawStroke[] = [];

  private settings: RenderingContextSettings = new RenderingContextSettings(true);
  private ctx?: CanvasRenderingContext2D = new CanvasRenderingContext2D(this.settings);
  private localStrokes: Array<DrawPoint[]> = [];
  private isDrawing: boolean = false;
  private currentStrokePoints: DrawPoint[] = [];

  build() {
    Canvas(this.ctx)
      .width('100%')
      .height('100%')
      .backgroundColor('#FFFFFF')
      .onReady(() => {
        this.initCanvasStyle();
        this.redrawAll();
      })
      .onTouch((event: TouchEvent) => {
        this.handleTouch(event);
      })
  }

  private initCanvasStyle(): void {
    if (!this.ctx) return;
    this.ctx.lineWidth = this.penWidth;
    this.ctx.strokeStyle = this.penColor;
    this.ctx.fillStyle = this.penColor;
    this.ctx.lineCap = 'round';
    this.ctx.lineJoin = 'round';
  }

  private handleTouch(event: TouchEvent): void {
    const touch = event.touches[0];
    if (!touch) return;
    const point: DrawPoint = { x: touch.x, y: touch.y };
    switch (event.type) {
      case TouchType.Down:
        this.startStroke(point);
        break;
      case TouchType.Move:
        this.moveStroke(point);
        break;
      case TouchType.Up:
        this.endStroke();
        break;
    }
  }

  private startStroke(point: DrawPoint): void {
    this.isDrawing = true;
    this.currentStrokePoints = [point];
    this.drawPoint(point);
    SyncService.getInstance().startStroke(point);
  }

  private moveStroke(point: DrawPoint): void {
    if (!this.isDrawing || this.currentStrokePoints.length === 0) return;
    const last = this.currentStrokePoints[this.currentStrokePoints.length - 1];
    this.drawLine(last, point);
    this.currentStrokePoints.push(point);
    SyncService.getInstance().addPoint(point);
  }

  private endStroke(): void {
    if (!this.isDrawing) return;
    if (this.currentStrokePoints.length > 0) {
      this.localStrokes.push([...this.currentStrokePoints]);
    }
    this.currentStrokePoints = [];
    this.isDrawing = false;
    SyncService.getInstance().endStroke();
  }

  private drawPoint(point: DrawPoint): void {
    if (!this.ctx) return;
    this.ctx.beginPath();
    this.ctx.arc(point.x, point.y, this.penWidth / 2, 0, 2 * Math.PI);
    this.ctx.fill();
  }

  private drawLine(from: DrawPoint, to: DrawPoint): void {
    if (!this.ctx) return;
    this.ctx.beginPath();
    this.ctx.moveTo(from.x, from.y);
    this.ctx.lineTo(to.x, to.y);
    this.ctx.stroke();
  }

  private redrawAll(): void {
    if (!this.ctx) return;

    if (this.ctx.width === 0 || this.ctx.height === 0) return;
    this.ctx.clearRect(0, 0, this.ctx.width, this.ctx.height);
    this.ctx.fillStyle = '#FFFFFF';
    this.ctx.fillRect(0, 0, this.ctx.width, this.ctx.height);
    this.initCanvasStyle();

    // 重绘本地笔画
    for (const stroke of this.localStrokes) {
      if (stroke.length === 0) continue;
      this.drawPoint(stroke[0]);
      for (let i = 1; i < stroke.length; i++) {
        this.drawLine(stroke[i-1], stroke[i]);
      }
    }
    // 重绘远端笔画(每个笔画独立,不会跨笔画连接)
    for (const stroke of this.remoteStrokes) {
      if (stroke.points.length === 0) continue;
      this.drawPoint(stroke.points[0]);
      for (let i = 1; i < stroke.points.length; i++) {
        this.drawLine(stroke.points[i-1], stroke.points[i]);
      }
    }
  }

  private onRemoteStrokesChanged(): void {
    // 远端笔画数据变化,全量重绘
    this.redrawAll();
  }

  private onPenColorChanged(): void {
    if (this.ctx) {
      this.ctx.strokeStyle = this.penColor;
    }
  }

  private onPenWidthChanged(): void {
    if (this.ctx) {
      this.ctx.lineWidth = this.penWidth;
    }
  }

  private onClearTriggerChanged(): void {
    // 清空本地存储的笔画
    this.localStrokes = [];
    this.currentStrokePoints = [];
    this.isDrawing = false;
    this.redrawAll();
  }
}

3.5 主页面(Index)

集成权限申请、设备列表展示、分布式服务初始化和回调注册。

javascript 复制代码
import { common, Permissions } from '@kit.AbilityKit';
import { promptAction } from '@kit.ArkUI';
import { distributedDeviceManager } from '@kit.DistributedServiceKit';
import { DistributedService } from '../services/DistributedService';
import { SyncCallback, SyncService } from '../services/SyncService';
import { Toolbar } from '../components/Toolbar';
import { Logger } from '../utils/Logger';
import { PermissionManager } from '../manager/PermissionManager';
import { DrawStroke } from '../model/DrawStroke';
import { DrawingBoard } from '../components/DrawingBoard';

const TAG = 'Index';

@Entry
@Component
struct Index {
  @State isReady: boolean = false;
  @State deviceList: distributedDeviceManager.DeviceBasicInfo[] = [];
  @State selectedDeviceId?: string = '';
  @State currentTabIndex: number = 0;

  // 画板状态变量
  @State penColor: string = '#000000';
  @State penWidth: number = 5;
  @State clearTrigger: number = 0;  // 每次清空时递增,画板监听此值
  // 远端笔画数据
  @State remoteStrokes: DrawStroke[] = [];

  private dataChangeCallback?: (key: string, value: string) => void;
  private refreshTimer?: number;
  private distributedReady: boolean = false;

  aboutToAppear() {
    this.init();
  }

  aboutToDisappear() {
    if (this.refreshTimer){
      clearInterval(this.refreshTimer);
      this.refreshTimer = undefined
    }
    DistributedService.getInstance().offDataChange();
  }

  async init() {
    const context = getContext(this) as common.UIAbilityContext;
    const permissions: Permissions[] = ['ohos.permission.DISTRIBUTED_DATASYNC'];
    const granted = await PermissionManager.checkAndRequestPermissions(
      context,
      permissions,
      (denied) => {
        Logger.warn(TAG, `权限被拒绝: ${denied.join(', ')}`);
        promptAction.showToast({ message: '需要分布式权限才能同步绘图', duration: 2000 });
      }
    );
    if (!granted) {
      this.isReady = true;
      return;
    }
    await this.initDistributedServices(context);
    this.isReady = true;
  }

  async initDistributedServices(context: common.UIAbilityContext): Promise<boolean> {
    if (this.distributedReady) {
      Logger.info(TAG, 'Distributed services already initialized');
      return true;
    }
    try {
      const initSuccess = await DistributedService.getInstance().init(context);
      if (initSuccess) {
        // 初始化同步服务,并传入回调
        SyncService.getInstance().init(this.syncCallback);
        this.distributedReady = true;
        Logger.info(TAG, 'Distributed services initialized successfully');
        promptAction.showToast({ message: '已开启实时同步', duration: 1500 });

        this.refreshDeviceList();
        this.refreshTimer = setInterval(() => {
          this.refreshDeviceList()
        }, 5000);
        this.registerDataChangeListener();
        return true;
      } else {
        Logger.error(TAG, 'Distributed service init failed');
        promptAction.showToast({ message: '分布式服务初始化失败,仅限本地绘图', duration: 2000 });
        return false;
      }
    } catch (err) {
      Logger.error(TAG, `Init error: ${err}`);
      promptAction.showToast({ message: '分布式服务初始化异常', duration: 2000 });
      return false;
    }
  }

  refreshDeviceList() {
    const devices = DistributedService.getInstance().getOnlineTrustedDevices();
    this.deviceList = devices;
    if (devices.length > 0 && !this.selectedDeviceId) {
      this.selectedDeviceId = devices[0].networkId;
    }
    Logger.info(TAG, `刷新设备列表,共 ${devices.length} 个在线设备`);
  }

  registerDataChangeListener() {
    this.dataChangeCallback = (key: string, value: string) => {
      Logger.debug(TAG, `收到远端数据变化: key=${key}`);
      SyncService.getInstance().handleRemoteMessage(key, value);
    };
    DistributedService.getInstance().onDataChange(this.dataChangeCallback);
  }

  // 创建 SyncCallback
 private syncCallback:SyncCallback = {

      onStrokeBegin: (strokeId, firstPoint) => {
        this.remoteStrokes = [...this.remoteStrokes, { id: strokeId, points: [firstPoint] }];
      },

      onStrokeDelta: (strokeId, points) => {
        const index = this.remoteStrokes.findIndex(s => s.id === strokeId);
        if (index !== -1) {
          const newStrokes = [...this.remoteStrokes];
          const existingStroke = newStrokes[index];
          const newPoints = existingStroke.points.concat(points);
          const newStroke: DrawStroke = {
            id: existingStroke.id,
            points: newPoints
          };
          newStrokes[index] = newStroke;
          this.remoteStrokes = newStrokes;
        }
      },
      onStrokeEnd: (strokeId) => {
        Logger.debug(TAG, `Stroke ended: ${strokeId}`);
      },
     onClear: () => {
       Logger.info(TAG, "Received clear command, clearing remote strokes");
       this.remoteStrokes = [];
       this.clearTrigger++;   // 触发画板清空本地笔画
     }
  }

  build() {
    Column() {
      Text('DrawTogether')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .width('100%')
        .height(56)
        .textAlign(TextAlign.Center)
        .backgroundColor('#F5F5F5')

      Tabs({ barPosition: BarPosition.End, index: this.currentTabIndex }) {
        TabContent() { this.DrawingTabContent() }
        .tabBar('画板')
        TabContent() { this.DeviceListTabContent() }
        .tabBar('设备列表')
      }
      .vertical(false)
      .barMode(BarMode.Fixed)
      .barWidth('100%')
      .layoutWeight(1)
      .vertical(false)
      .scrollable(false)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#FFFFFF')
  }
}

四、踩坑与解决方案

在开发过程中,我遇到了几个典型问题,值得分享。

4.1 消息重复处理导致状态错乱

现象 :同一笔画的BEGIN消息被处理两次,导致receivingStrokeId被意外清空,后续DELTA因strokeId mismatch被忽略,最终绘制出来的已经乱套了。

原因 :分布式数据库的onDataChange可能同时触发insertEntriesupdateEntries,或者网络重传导致同一条消息被多次送达。

解决 :在handleRemoteMessage中维护processedKeys集合,对已处理的消息key直接返回;同时在receiveBegin中增加重复判断。

4.2 新笔画起点与上一笔终点错误连接

现象:画完一条线后再画一个圆,圆中出现连接第一条线终点的四边形。

原因receiveDelta中无条件连接上一个接收点与新点,而receivingPoints在上一笔画结束时未正确清空(或BEGIN时未清空),导致跨笔画残留。

解决 :在receiveBegin中强制清空receivingPoints = [],并增加receivingStrokeId重复判断。

4.3 快速绘制时远端曲线呈折线

现象:在A设备画一个圆,B设备显示的线条呈折线状,不够平滑。

原因:起初采用批量发送策略(每5个点或20ms),导致采样点稀疏间断,接收端只能用稀疏点连线。

解决 :改为每个点立即发送,移除批量缓冲逻辑。实测每秒约60-100条消息,每条消息仅几十字节完全可接受,且曲线平滑度大幅提升。

五、结语

鸿蒙的分布式能力为开发者打开了多设备协同的新世界,但底层API的使用需要更多的探索和沉淀。希望本文能帮助你在分布式应用开发中少踩一些坑,快速实现自己的创意。

本文为原创技术干货,转载需注明出处。如果你觉得有帮助,欢迎点赞、收藏、评论交流。

相关推荐
REDcker2 小时前
RabbitMQ系列03 - AMQP分层与协议流转
分布式·rabbitmq
一点 内容2 小时前
Scrapy框架深度解析:高效构建分布式爬虫的实战指南
分布式·爬虫·scrapy
Rany-2 小时前
分布式光纤传感:新一代管网探漏监测技术
分布式
HMS Core2 小时前
化繁为简:顺丰速运App如何通过 HarmonyOS SDK实现专业级空间测量
华为·harmonyos
星释3 小时前
鸿蒙Flutter实战:30.在Pub上发布鸿蒙化插件
flutter·harmonyos·鸿蒙
前端不太难4 小时前
鸿蒙游戏 Store 设计(AI + 多端)
人工智能·游戏·harmonyos
见山是山-见水是水4 小时前
鸿蒙flutter第三方库适配 - 动态工作流
flutter·华为·harmonyos
HwJack204 小时前
HarmonyOS 编译产物与包结构小知识
华为·harmonyos
硅基诗人4 小时前
Java后端高并发核心瓶颈突破(JVM+并发+分布式底层实战)
java·jvm·分布式