HarmonyOS鸿蒙Next中API 11如何实现worker子线程中接收处理的数据在UI中刷新显示

HarmonyOS鸿蒙Next中API 11如何实现worker子线程中接收处理的数据在UI中刷新显示 问题描述:我目前开发一款应用作为传感器的数据采集和显示终端,

连接方式:通过WiFi连接传感器,使用UDP socket 通信获取传感器数据,当给传感器发送开始采集命令后,传感器会以100Hz的频率发送数据,每次数据长度位512 ArrayBuffer;当接收道数据后需要将数据转换处理位一幅灰度图在UI上显示。

目前实现:1. 创建udpSocket.ets 文件,创建UdpSocket类实现UDP Socket创建,绑定和控制命令发送与数据接收回调处理

  1. 创建index.ets page页面添加设置,开始采集,停止采集控制命令按钮和Image显示框。

3.为实现后台高频率数据接收创建一个worker子线程运行UdpSocket类的数据接收处理方法。

目前遇到的问题:无法有效的将worker子线程运行UdpSocket类的数据接收处理结果的Image同步到UI实时显示。尝试过使用 @Observed 装饰类UdpSocket,在UI组件中使用

@State udpSocket:UdpSocket = new UdpSocket()

Image(this.udpSocket.imagePixelMap)刷新UI界面,但是会造成UI界面卡死,出现Reason:APP_INPUT_BLOCK错误。

请问如何能实现将worker子线程运行UdpSocket类的数据接收处理结果的Image高效的同步到UI显示。我原来是QT客户端开发的,为实现鸿蒙端采集终端第一次学习鸿蒙开发,还请鸿蒙大佬指点一二。


更多关于HarmonyOS鸿蒙Next中API 11如何实现worker子线程中接收处理的数据在UI中刷新显示的实战教程也可以访问 https://www.itying.com/category-93-b0.html

5 回复

Worker 和 UI 线程之间不能直接共享对象(如类实例、@Observed 对象、PixelMap 等),所有跨线程通信必须通过 postMessage 进行序列化数据传递。

参考代码

// 假设你已经初始化好 UDP Socket 并开始接收数据
// 每当收到一组 512 字节的传感器数据,解析并生成灰度图像数据 buffer(假设为 Uint8Array)

// 模拟:处理得到灰度图像像素数据(例如 width * height 的 Uint8Array,比如 256x256)
function processSensorDataToImage(sensorData: ArrayBuffer): Uint8Array {
  // TODO: 你自己的图像合成/解析算法,将 512字节或其他格式数据转为灰度像素 Buffer
  let pixelData: Uint8Array = new Uint8Array(256 * 256); // 假设图像宽高为 256x256,单通道灰度
  // ... 填充 pixelData ...
  return pixelData;
}

// 收到传感器数据后:
socket.on('data', (data: ArrayBuffer) => {
  let grayPixels: Uint8Array = processSensorDataToImage(data);

  // 向 UI 线程发送图像像素数据
  postMessage({
    type: 'imageData',
    pixels: grayPixels.buffer, // 发送 ArrayBuffer
    width: 256,
    height: 256
  });
});
import image from '@ohos.multimedia.image';

@Entry
@Component
struct Index {
  @State pixelMap: image.PixelMap | null = null;
  private worker: worker.Worker | null = null;

  aboutToAppear() {
    // 创建并启动 Worker
    this.worker = new worker.Worker('entry/ets/workers/worker'); // 注意路径匹配你的 worker 文件

    this.worker.onMessage((msg) => {
      if (msg.type === 'imageData') {
        const { pixels, width, height } = msg;

        // 1. 从 ArrayBuffer 创建 ImageSource
        let arrayBuffer = new ArrayBuffer(pixels.byteLength);
        let view = new Uint8Array(arrayBuffer);
        view.set(new Uint8Array(pixels));

        // 2. 构造 ImageSource 并生成 PixelMap(关键步骤)
        image.createImageSource(arrayBuffer).then((imageSource) => {
          imageSource.createPixelMap().then((pm) => {
            this.pixelMap = pm; // 更新 @State,触发 UI 刷新
          }).catch((err) => {
            console.error('Create PixelMap failed: ' + JSON.stringify(err));
          });
        }).catch((err) => {
          console.error('Create ImageSource failed: ' + JSON.stringify(err));
        });
      }
    });
  }

  build() {
    Column() {
      // 控制按钮等 UI ...

      // 显示图像
      if (this.pixelMap != null) {
        Image(this.pixelMap)
          .width('100%')
          .height('100%')
      } else {
        Text('等待图像数据...')
          .width('100%')
          .height('100%')
          .textAlign(TextAlign.Center)
      }
    }
    .width('100%')
    .height('100%')
  }

  aboutToDisappear() {
    this.worker?.terminate();
  }
}

更多关于HarmonyOS鸿蒙Next中API 11如何实现worker子线程中接收处理的数据在UI中刷新显示的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


高频数据更新触发频繁渲染,主线程无法及时处理输入事件;PixelMap对象无法直接跨线程传递,需使用特殊数据共享机制.

解决办法

1/ 跨线程数据共享架构

// 使用Sendable共享对象

@Sendable

class SensorDataBuffer {

  private buffer: ArrayBuffer = new ArrayBuffer(512);

  private updateFlag: boolean = false;

  // 原子操作更新数据

  updateData(newData: ArrayBuffer): void {

    this.buffer = newData;

    this.updateFlag = !this.updateFlag;

  }

  // 主线程读取接口

  getLatestData(): ArrayBuffer {

    return this.buffer;

  }

}

2/ Worker线程数据处理

// Worker线程代码

const parentPort = worker.workerPort;

const sharedBuffer = new SensorDataBuffer();

// 接收传感器原始数据

udpSocket.on('message', (data: ArrayBuffer) => {

  const processedData = processToGrayScale(data); // 灰度转换

  sharedBuffer.updateData(processedData);

  

  // 使用节流控制刷新频率(100Hz降为30Hz)

  throttlePostMessage(30, () => {

    parentPort.postMessage({ type: 'frameUpdate' });

  });

});

3/ 主线程渲染优化

// 主线程代码

@Component

struct SensorDisplay {

  @StorageLink('imageBuffer') buffer: PixelMap | null = null;

  

  aboutToAppear() {

    workerPort.onmessage = (event: MessageEvent) => {

      if (event.data.type === 'frameUpdate') {

        // 使用离屏Canvas避免直接操作UI

        const offscreenCanvas = new OffscreenCanvas(640, 480);

        drawToCanvas(sharedBuffer.getLatestData(), offscreenCanvas);

        

        // 通过分帧渲染降低负载

        this.buffer = offscreenCanvas.transferToImageBitmap();

      }

    };

  }

  build() {

    Image(this.buffer)

      .onAppear(() => {

        // 开启高优先级渲染通道

        window.setPreferredRenderMode(RenderMode.HIGH_PERFORMANCE);

      })

  }

}

核心方案:使用postMessage + ImageBitmap跨线程传输

从API version 11开始,ImageBitmap支持并发线程绘制,可通过postMessage将图像数据从Worker线程传输到主线程。

实现步骤:

  1. Worker线程处理数据并创建ImageBitmap

    // worker.ets (Worker线程)
    import { worker } from '@kit.ArkTS';
    
    const workerPort = worker.workerPort;
    
    // 处理传感器数据并生成图像
    function processSensorData(data: ArrayBuffer): ImageBitmap {
      // 1. 将512字节ArrayBuffer转换为灰度图数据
      // 2. 创建ImageBitmap对象
      // 示例伪代码:
      const imageData = new ImageData(width, height);
      // ...填充灰度数据...
      return createImageBitmap(imageData);
    }
    
    workerPort.onmessage = (e: MessageEvents) => {
      const sensorData: ArrayBuffer = e.data;
      const imageBitmap = processSensorData(sensorData);
      
      // 通过postMessage传输ImageBitmap到主线程
      workerPort.postMessage({type: 'imageUpdate', data: imageBitmap});
    }
    
  2. 主线程接收并更新UI

    // index.ets (主线程)
    @Entry
    @Component
    struct SensorDisplay {
      @State pixelMap: PixelMap | null = null;
      private workerInstance: worker.ThreadWorker;
    
      aboutToAppear() {
        this.workerInstance = new worker.ThreadWorker("entry/ets/workers/worker.ets");
        
        this.workerInstance.onmessage = (e: MessageEvents) => {
          if (e.data.type === 'imageUpdate') {
            // 将ImageBitmap转换为PixelMap用于显示
            this.pixelMap = e.data.data;
          }
        };
      }
    
      build() {
        Column() {
          Image(this.pixelMap)
            .width('100%')
            .height('80%')
          
          Button('开始采集')
            .onClick(() => {
              this.workerInstance.postMessage('start');
            })
        }
      }
    }
    

关键注意事项:

  1. 避免直接操作UI状态:Worker线程中不能直接操作@State等响应式变量,必须通过postMessage通信
  2. 使用高效的数据传输
    • 优先使用transfer参数转移ArrayBuffer所有权(避免拷贝):
    workerPort.postMessage(buffer, [buffer]); // 所有权转移
    
  3. 图像数据处理优化
    • 在Worker中完成所有耗时操作(数据解析、图像生成)
    • 控制刷新频率(100Hz可能过高,可考虑 throttling)

替代方案(如需更低延迟):

如果直接传输图像数据仍存在性能问题,可以考虑:

  1. 共享内存:使用SharedArrayBuffer进行数据共享
  2. Surface直接渲染:通过ImageReceiver获取surface进行直接渲染

为什么您之前的方案会导致UI卡死:

直接在Worker线程中操作@Observed装饰的类会违反ArkUI的线程模型,因为:

  1. 状态更新必须在主线程执行
  2. 高频的UI更新请求阻塞了主线程事件循环

在HarmonyOS Next API 11中,使用TaskDispatcher创建UI任务分发器。在Worker线程中处理完数据后,通过postTask方法将数据封装到Task中,并提交到UI任务队列。UI线程会自动执行该任务,在任务的run方法内直接调用UI组件(如TextImage)的更新方法即可刷新显示。需注意数据传递需序列化,并确保UI更新操作在UI线程执行。

在HarmonyOS Next API 11中,从Worker线程高效更新UI的关键在于使用正确的线程间通信机制和UI更新方式。你的方案方向正确,但直接在主线程中观察和修改Worker内的对象会导致阻塞。以下是推荐的实现方案:

核心方案:使用 postMessageonmessage 进行线程通信,在主线程使用 TaskPool主线程队列 处理数据并更新UI。

1. Worker线程侧(数据处理线程) 在Worker脚本中,处理完UDP数据并生成图像数据(如PixelMap的序列化信息或ArrayBuffer)后,不要直接持有或操作UI组件。通过 postMessage() 将处理结果发送给主线程。

// worker.ets (或 .js) 中
import worker from '@ohos.worker';

const parentPort = worker.parentPort;

// 假设在UDP回调中处理数据得到 pixelMapData
function onUdpDataReceived(arrayBuffer) {
  // 1. 处理512字节ArrayBuffer,生成图像数据...
  // 2. 将图像数据转换为可传输格式,例如:
  //     a) 如果是PixelMap,提取其属性(宽、高、像素数组等)进行序列化。
  //     b) 或直接处理为base64字符串/ArrayBuffer。
  let processedImageData = processToImageData(arrayBuffer);
  
  // 3. 发送给主线程
  parentPort.postMessage(processedImageData);
}

2. 主线程侧(UI线程) 在主线程中:

  • 使用 Worker 对象监听 onmessage 事件接收数据。
  • 在回调中,将接收到的数据派发到主线程的任务队列进行处理,避免在回调中直接进行耗时操作或同步更新UI。
  • 使用 PixelMapcreatePixelMap 或其他API,根据传输的数据重新创建 PixelMap 对象。
  • 最后,更新被 @State 装饰的UI状态变量。
// index.ets 主页面
import worker from '@ohos.worker';
import { BusinessError } from '@ohos.base';

@Entry
@Component
struct Index {
  @State imagePixelMap: pixelMap.PixelMap | undefined = undefined;
  private workerInstance: worker.ThreadWorker | undefined = undefined;

  aboutToAppear() {
    // 初始化Worker
    this.workerInstance = new worker.ThreadWorker('entry/ets/workers/udpWorker.ets');
    // 监听Worker消息
    this.workerInstance.onmessage = (data: worker.MessageEvents) => {
      // 接收到Worker发送的图像数据
      let receivedImageData = data.data;

      // 【关键】使用TaskPool或直接在主线程上下文异步处理数据并更新UI
      // 方法一:使用TaskPool执行耗时转换(如果转换复杂)
      // taskpool.execute(deserializeToPixelMap, receivedImageData).then((pixelMap) => {
      //   this.imagePixelMap = pixelMap;
      // }).catch((err: BusinessError) => {
      //   console.error(`Failed to deserialize pixelmap: ${err.message}`);
      // });

      // 方法二:如果数据已处理得较轻量,直接在主线程事件循环中更新
      // 使用setTimeout或requestAnimationFrame避免阻塞
      setTimeout(() => {
        // 假设 receivedImageData 可直接用于创建或更新PixelMap
        this.updateUiWithImageData(receivedImageData);
      }, 0);
    };

    this.workerInstance.onerror = (error: worker.ErrorEvent) => {
      // 处理错误
    };
  }

  updateUiWithImageData(imageData: any) {
    // 根据你的数据格式,创建或更新PixelMap
    // 例如:pixelMap.createPixelMap(...)
    // 然后赋值给状态变量
    this.imagePixelMap = createOrUpdatePixelMap(imageData); // 替换为你的创建逻辑
  }

  build() {
    Column() {
      // 显示图像
      if (this.imagePixelMap) {
        Image(this.imagePixelMap)
          .width('100%')
          .height('80%')
      }
      Button('开始采集')
        .onClick(() => {
          this.workerInstance?.postMessage({ command: 'start' });
        })
      // ... 其他按钮
    }
  }

  aboutToDisappear() {
    this.workerInstance?.terminate();
  }
}

关键点与优化建议:

  • 数据传输格式: Worker与主线程间传递的数据需是可序列化的。对于PixelMap,不能直接传递对象。你需要传递其像素数据(如ArrayBuffer)加上尺寸、格式等元数据,在主线程重新构造PixelMap。API 11提供了更完善的PixelMap序列化/反序列化支持,请查阅相关API。
  • 降低频率: 100Hz(每秒100次)的UI更新对于移动设备UI线程压力过大。考虑:
    • 在Worker内节流: 累积若干帧数据,或按固定时间间隔(如每秒30-60次)向主线程发送一次最新图像数据。
    • 使用requestAnimationFrame 在主线程更新UI时,使用requestAnimationFrame来对齐屏幕刷新率,避免不必要的重绘。
  • 避免阻塞: onmessage 回调执行应尽量快。将耗时的反序列化或计算操作通过 TaskPool 异步执行,或使用 setTimeout(fn, 0) 将其拆解到下一个事件循环。
  • 状态更新: @State 变量在赋值时会触发UI更新。确保更新操作发生在主线程的ArkUI渲染循环内。

替代方案考虑: 如果图像数据处理逻辑非常重,即使在Worker中处理也担心影响通信,可以考虑:

  • 共享内存(SharedArrayBuffer): 在API允许的情况下,Worker和主线程可访问同一块内存区域,避免数据拷贝。但需要注意同步问题。
  • Native层处理: 对于性能极限场景,可通过Native API(C/C++)实现UDP接收和图像处理,再通过FFI与ArkTS UI交互。

你的应用场景对实时性要求高,重点在于平衡数据接收、处理、传输和UI渲染的速率,避免任何一个环节堆积阻塞。建议先在Worker内实现数据节流,确保主线程更新频率在60fps以内,再逐步优化数据处理和传输效率。

回到顶部