HarmonyOS开发实战( Beta5版)线程间通信场景最佳实践

简介

在应用开发中,经常会需要处理一些耗时的任务,如果全部放在主线程中执行就会导致阻塞,从而引起卡顿或者掉帧现象,降低用户体验,此时就可以将这些耗时操作放到子线程中处理。通常情况下,子线程可以独立完成自己的任务,但是很多时候需要将数据从主线程传递到子线程,或者将子线程的执行结果返回给主线程。本篇文章将通过以下几种场景和示例,呈现如何在OpenHarmony应用开发中实现主线程和子线程的数据通信。

独立的耗时任务

如果耗时任务是可以独立执行的,只需要在任务执行完毕后将结果返回给主线程,可以通过以下方式实现。

首先,引入TaskPool模块。

复制代码
import { taskpool } from '@kit.ArkTS';

然后,实现子线程需要执行的任务。

复制代码
@Concurrent // 在Task中执行的方法,需要添加@Concurrent注解,否则无法正常调用。
export function loadPicture(count: number): IconItemSource[] {
  let iconItemSourceList: IconItemSource[] = [];
  // 遍历添加6*count个IconItem的数据
  for (let index = 0; index < count; index++) {
    const numStart: number = index * 6;
    // 此处循环使用6张图片资源
    iconItemSourceList.push(new IconItemSource($r('app.media.nearby'), `item${numStart + 1}`));
    iconItemSourceList.push(new IconItemSource($r('app.media.scan'), `item${numStart + 2}`));
    iconItemSourceList.push(new IconItemSource($r('app.media.shop'), `item${numStart + 3}`));
    iconItemSourceList.push(new IconItemSource($r('app.media.cards'), `item${numStart + 4}`));
    iconItemSourceList.push(new IconItemSource($r('app.media.album'), `item${numStart + 5}`));
    iconItemSourceList.push(new IconItemSource($r('app.media.applet'), `item${numStart + 6}`));
  }
  return iconItemSourceList;
}

最后,通过TaskPool中的execute方法执行Task。

复制代码
// ...
// 创建Task
let lodePictureTask: taskpool.Task = new taskpool.Task(loadPicture, 30);
// 执行Task,并返回结果
taskpool.execute(lodePictureTask).then((res: IconItemSource[]) => {
  // loadPicture方法的执行结果
  this.iconItemSourceList = res; 
})
// ...

多个任务同时执行

如果有多个任务同时执行,由于任务的复杂度不同,执行时间会不一样,返回数据的时间也是不可控的。如果主线程需要所有任务执行完毕的数据,可以通过下面这种方式实现。

复制代码
// ...
let taskGroup: taskpool.TaskGroup = new taskpool.TaskGroup();
taskGroup.addTask(new taskpool.Task(loadPicture, 30));
taskGroup.addTask(new taskpool.Task(loadPicture, 20));
taskGroup.addTask(new taskpool.Task(loadPicture, 10));
taskpool.execute(taskGroup).then((ret: IconItemSource[][]) => {
  for (let i = 0; i < ret.length; i++) {
    for (let j = 0; j < ret[i].length; j++) {
      this.iconItemSourceList.push(ret[i][j]);
    }
  }
})
// ...

在该场景中,将需要执行的Task放到了一个TaskGroup里面,当TaskGroup中所有的Task都执行完毕后,会把每个Task运行的结果都放在一个数组中返回到主线程,而不是每执行完一个Task就返回一次,这样就可以在返回的数据里拿到所有的Task执行结果,方便主线程使用。

除此以外,如果Task需要处理的数据量较大(比如一个列表中有10000条数据),把这些数据都放在一个Task中处理也是比较耗时的。那么就可以将原始数据拆分成多个列表,并将每个子列表分配给一个独立的Task进行执行,并且等待全部执行完毕后拼成完整的数据,这样可以节省处理时间,提升用户体验。

Task任务与主线程通信

如果一个Task,不仅需要返回最后的执行结果,而且需要定时通知主线程状态、数据的变化,或者需要分段返回数量级较大的数据(比如从数据库中读取大量数据),可以通过下面这种方式实现。

首先,实现一个方法,用来接收Task发送的消息。

复制代码
function notice(data: number): void {
  console.info("子线程任务已执行完,共加载图片: ", data);
}

然后,在需要执行的Task任务中,添加sendData()接口将消息发送给主线程。

复制代码
// 通过Task的sendData方法,即时通知主线程信息
@Concurrent
export function loadPictureSendData(count: number): IconItemSource[] {
  let iconItemSourceList: IconItemSource[] = [];
  // 遍历添加6*count个IconItem的数据
  for (let index = 0; index < count; index++) {
    const numStart: number = index * 6;
    // 此处循环使用6张图片资源
    iconItemSourceList.push(new IconItemSource($r('app.media.nearby'), `item${numStart + 1}`));
    iconItemSourceList.push(new IconItemSource($r('app.media.scan'), `item${numStart + 2}`));
    iconItemSourceList.push(new IconItemSource($r('app.media.shop'), `item${numStart + 3}`));
    iconItemSourceList.push(new IconItemSource($r('app.media.cards'), `item${numStart + 4}`));
    iconItemSourceList.push(new IconItemSource($r('app.media.album'), `item${numStart + 5}`));
    iconItemSourceList.push(new IconItemSource($r('app.media.applet'), `item${numStart + 6}`));
    taskpool.Task.sendData(iconItemSourceList.length);
  }
  return iconItemSourceList;
}

最后,在主线程通过onReceiveData()接口接收消息。

复制代码
// ...
let lodePictureTask: taskpool.Task = new taskpool.Task(loadPictureSendData, 30);
// 设置notice方法接收Task发送的消息
lodePictureTask.onReceiveData(notice);
taskpool.execute(lodePictureTask).then((res: IconItemSource[]) => {
  this.iconItemSourceList = res;
})
// ...

这样主线程就可以通过notice()接口接收到Task发送的数据。

Worker和主线程的即时消息通信

在ArkTS中,Worker相对于TaskPool存在一定的差异性,有数量限制但是可以长时间存在。一个Worker中可能会执行多个不同的任务,每个任务执行的时长或者返回的结果可能都不相同,主线程需要根据情况调用Worker中的不同方法,Worker则需要及时地将结果返回给主线程,此时可以通过下面的方法实现。

首先,创建一个Worker,可以根据参数执行不同的任务。

复制代码
import { worker, MessageEvents, ThreadWorkerGlobalScope } from '@kit.ArkTS';
 
const workerPort: ThreadWorkerGlobalScope = worker.workerPort;
// Worker接收主线程的消息,根据数据类型调用对应的方法
workerPort.onmessage = (e: MessageEvents): void => {
  if (typeof e.data === "string") {
    try {
      // 调用方法无入参
      let res: string = workerPort.callGlobalCallObjectMethod("picData", "setUp", 0) as string;
      console.info("worker: ", res);
    } catch (error) {
      // 异常处理
      console.error("worker: error code is " + error.code + " error message is " + error.message);
    }
  } else if (e.data instanceof Array) {
    // 将数据的前4条返回回去
    workerPort.postMessage(e.data.slice(0, 4));
  }
}

然后在主线程中创建这个Worker的对象,在点击Button的时候调用postMessage向Worker发送消息,通过Worker的onmessage方法接收Worker返回的数据。

复制代码
import { worker, MessageEvents } from '@kit.ArkTS';
// ...
@State iconItemSourceList: IconItemSource[] = [];
// 创建Worker对象
workerInstance: worker.ThreadWorker = new worker.ThreadWorker("entry/ets/pages/workers/Worker.ts");
aboutToAppear() {
  // 初始化Worker
  this.initWorker();
  for (let index = 0; index < 20; index++) {
    const numStart: number = index * 6;
    // 此处循环使用6张图片资源
    this.iconItemSourceList.push(new IconItemSource($r('app.media.nearby'), `item${numStart + 1}`));
    this.iconItemSourceList.push(new IconItemSource($r('app.media.scan'), `item${numStart + 2}`));
    this.iconItemSourceList.push(new IconItemSource($r('app.media.shop'), `item${numStart + 3}`));
    this.iconItemSourceList.push(new IconItemSource($r('app.media.cards'), `item${numStart + 4}`));
    this.iconItemSourceList.push(new IconItemSource($r('app.media.album'), `item${numStart + 5}`));
    this.iconItemSourceList.push(new IconItemSource($r('app.media.applet'), `item${numStart + 6}`));
  }
}
initWorker(){
  // 通过onmessage方法接收Worker返回的数据
  this.workerInstance.onmessage = (e: MessageEvents): void => {
    if (e.data instanceof Array) {
      this.iconItemSourceList = e.data;
    }
 }
}
// ...
Button('将图片变成5个', { type: ButtonType.Normal, stateEffect: true })
  .fontSize(14)
  .borderRadius(8)
  .backgroundColor('# 317aff')
  .width(250)
  .height(60)
  .margin({
    top: 30
  })
  .onClick(() => {
    // 将数据传到Worker中
    this.workerInstance.postMessage(this.iconItemSourceList);
  })
// ...

在这段示例代码中,Worker做了2种不同的处理:当传入的数据是个string类型时,调用callGlobalCallObjectMethod同步调用主线程中的接口;当传入Array类型的时候,将Array的前4条数据返回给主线程。这样就可以实现主线程和Worker间的即时通信,方便主线程使用Worker的运行结果。

Worker子线程同步调用主线程的接口

如果一个接口在主线程中已经实现了,Worker需要调用该接口,那么可以使用下面这种方式实现。

首先,在主线程实现需要调用的接口,并且创建Worker对象,在Worker上注册需要调用的接口。

复制代码
import { worker } from '@kit.ArkTS';
// 创建Worker对象
const workerInstance: worker.ThreadWorker = new worker.ThreadWorker("entry/ets/pages/workers/Worker.ts");
 
class PicData {
  public iconItemSourceList: IconItemSource[] = [];
  
  public setUp(): string {
    for (let index = 0; index < 20; index++) {
      const numStart: number = index * 6;
      // 此处循环使用6张图片资源
      this.iconItemSourceList.push(new IconItemSource($r('app.media.nearby'), `item${numStart + 1}`));
      this.iconItemSourceList.push(new IconItemSource($r('app.media.scan'), `item${numStart + 2}`));
      this.iconItemSourceList.push(new IconItemSource($r('app.media.shop'), `item${numStart + 3}`));
      this.iconItemSourceList.push(new IconItemSource($r('app.media.cards'), `item${numStart + 4}`));
      this.iconItemSourceList.push(new IconItemSource($r('app.media.album'), `item${numStart + 5}`));
      this.iconItemSourceList.push(new IconItemSource($r('app.media.applet'), `item${numStart + 6}`));
   }
   return "setUpIconItemSourceList success!";
  }
}
 
let picData = new PicData();
// 在Worker上注册需要调用的对象
workerInstance.registerGlobalCallObject("picData", picData);
workerInstance.postMessage("run setUp in picData");

然后,在Worker中通过callGlobalCallObjectMethod接口就可以调用主线程中的setUp()方法了。

复制代码
// ...
try {
  // 调用方法无入参
  let res: string = workerPort.callGlobalCallObjectMethod("picData", "setUp", 0) as string;
  console.info("worker: ", res);
} catch (error) {
  // 异常处理
  console.error("worker: error code is " + error.code + " error message is " + error.message);
}
// ...

最后

小编在之前的鸿蒙系统扫盲中,有很多朋友给我留言,不同的角度的问了一些问题,我明显感觉到一点,那就是许多人参与鸿蒙开发,但是又不知道从哪里下手,因为资料太多,太杂,教授的人也多,无从选择。有很多小伙伴不知道学习哪些鸿蒙开发技术?不知道需要重点掌握哪些鸿蒙应用开发知识点?而且学习时频繁踩坑,最终浪费大量时间。所以有一份实用的鸿蒙(HarmonyOS NEXT)文档用来跟着学习是非常有必要的。

为了确保高效学习,建议规划清晰的学习路线,涵盖以下关键阶段:

希望这一份鸿蒙学习文档能够给大家带来帮助~


鸿蒙(HarmonyOS NEXT)最新学习路线

该路线图包含基础技能、就业必备技能、多媒体技术、六大电商APP、进阶高级技能、实战就业级设备开发,不仅补充了华为官网未涉及的解决方案

路线图适合人群:

IT开发人员: 想要拓展职业边界
零基础小白: 鸿蒙爱好者,希望从0到1学习,增加一项技能。
**技术提升/进阶跳槽:**发展瓶颈期,提升职场竞争力,快速掌握鸿蒙技术

2.视频教程+学习PDF文档

(鸿蒙语法ArkTS、TypeScript、ArkUI教程......)

纯血版鸿蒙全套学习文档(面试、文档、全套视频等)

鸿蒙APP开发必备

​​

总结

参与鸿蒙开发,你要先认清适合你的方向,如果是想从事鸿蒙应用开发方向的话,可以参考本文的学习路径,简单来说就是:为了确保高效学习,建议规划清晰的学习路线

相关推荐
xuanzdhc2 小时前
Linux 基础IO
linux·运维·服务器
不凡的凡2 小时前
鸿蒙图片相似性对比
华为·harmonyos
愚润求学2 小时前
【Linux】网络基础
linux·运维·网络
不想写bug呀3 小时前
多线程案例——单例模式
java·开发语言·单例模式
bantinghy3 小时前
Linux进程单例模式运行
linux·服务器·单例模式
我不会写代码njdjnssj3 小时前
网络编程 TCP UDP
java·开发语言·jvm
小和尚同志4 小时前
29.4k!使用 1Panel 来管理你的服务器吧
linux·运维
Georgewu4 小时前
【HarmonyOS】HAR和HSP循环依赖和依赖传递问题详解
harmonyos
帽儿山的枪手4 小时前
为什么Linux需要3种NAT地址转换?一探究竟
linux·网络协议·安全
李少兄9 天前
解决OSS存储桶未创建导致的XML错误
xml·开发语言·python