HarmonyOS 鸿蒙应用开发( 八、线程模型及线程间通信 Emitter、Worker和TaskPool介绍)

目录

前言

线程模型概述

Emitter介绍

Worker介绍

TaskPool介绍

使用TaskPool

Priority

Task

示例

注意事项

TaskPool和Worker的对比选择

实现特点对比

适用场景对比

TaskPool注意事项

Worker注意事项

写在最后

其他资源

前言

HarmonyOS(鸿蒙系统)应用的线程模型设计考虑了系统的性能优化和用户体验。在鸿蒙应用开发中,每个进程都有一个主线程(UI)。主推的应用架构采用Stage模型,该模型以场景为中心,将应用划分为不同的Stage(阶段)或Ability(能力)。每个Ability可以理解为一个独立的功能模块,它可以是页面(Page Ability)、服务(Service Ability)或者其他类型的能力。每个Ability有自己的生命周期,并且可以在需要时动态加载和卸载,以此提高系统的资源利用率和响应速度。

线程模型概述

在HarmonyOS应用中每个进程都会有一个主线程(UI线程),用于处理UI更新、事件分发等操作。对于耗时任务,开发者需要创建工作线程进行处理,以避免阻塞主线程影响UI流畅性。

主线程有如下职责:

1.执行UI绘制,主线程负责处理与用户界面相关的所有操作,包括布局计算、渲染以及屏幕刷新等。在鸿蒙系统中,ArkTS引擎用于管理主线程上的UI渲染。

2.管理主线程的ArkTS引擎实例,使多个UIAbility组件能够运行在其之上。

3.管理其他线程(例如Worker线程)的ArkTS引擎实例,例如启动和终止其他线程。

4.分发交互事件,主线程接收并分发来自用户的触摸事件以及其他系统事件给相应的组件进行处理。

5.消息循环:鸿蒙系统基于消息机制实现线程间的通信和任务调度,主线程维护了一个消息队列,通过循环处理这些消息来响应不同的应用程序事件。

6.处理应用代码的回调,包括事件处理和生命周期管理。

7.接收Worker线程发送的消息。

除主线程外,还有一类与主线程并行的独立线程Worker,主要用于执行耗时操作,但不可以直接操作UI。Worker线程在主线程中创建,与主线程相互独立。最多可以创建8个Worker。

同时,在HarmonyOS应用架构中,为了保证应用的流畅性和响应性,非UI相关的耗时操作通常不会在主线程上执行,而是需要创建额外的工作线程或任务来完成。

此外,HarmonyOS采用了Stage模型,将应用划分为多个Ability模块,每个模块可能包含自己的业务逻辑线程。

HarmonyOS提供了两种线程间通信的方式,分别是Emitter和Worker。

  1. Emitter(发射器):Emitter主要适用于线程间的事件同步。它可以在不同的线程之间传递事件,并确保事件的顺序和同步性。通过Emitter,一个线程可以触发一个事件,然后其他线程可以监听并处理这个事件。这有助于不同线程之间的数据共享和协调。
  2. Worker(工作者):Worker主要用于新开一个线程执行耗时任务。当需要执行一些耗时操作时,为了不阻塞主任务的执行,可以使用Worker线程。Worker线程是在主线程的上下文中创建的独立线程,它可以执行一些耗时任务,如网络访问、文件读写等。工作线程可以与主线程并行执行,以提高应用的响应性和性能。

Stage模型只提供了主线程和Worker线程,Emitter主要用于主线程内或者主线程和Worker线程的事件同步, Worker主要用于新开一个线程执行耗时任务。

Emitter介绍

Emitter主要提供线程间发送和处理事件的能力,包括对持续订阅事件或单次订阅事件的处理、取消订阅事件、发送事件到事件队列等。

比如借助Emitter可以实现类似Android中EventBus事件总线的功能。

Emitter的使用步骤如下:

1.订阅事件

import emitter from "@ohos.events.emitter";

// 定义一个eventId为1的事件
let event = {
    eventId: 1
};

// 收到eventId为1的事件后执行该回调
let callback = (eventData) => {
    console.info('event callback');
};

// 订阅eventId为1的事件
emitter.on(event, callback);

2、发送事件

import emitter from "@ohos.events.emitter";

// 定义一个eventId为1的事件,事件优先级为Low
let event = {
    eventId: 1,
    priority: emitter.EventPriority.LOW
};

let eventData = {
    data: {
        "content": "c",
        "id": 1,
        "isEmpty": false,
    }
};

// 发送eventId为1的事件,事件内容为eventData
emitter.emit(event, eventData);

Worker介绍

Worker是与主线程并行的独立线程。创建Worker的线程被称为宿主线程,Worker工作的线程被称为Worker线程。创建Worker时传入的脚本文件在Worker线程中执行,通常在Worker线程中处理耗时的操作,需要注意的是,Worker中不能直接更新Page。

Worker的开发步骤如下:

1、在工程的模块级build-profile.json5文件的buildOption属性中添加配置信息。

  "buildOption": {
    "sourceOption": {
      "workers": [
        "./src/main/ets/workers/worker.ts"
      ]
    }
  }

2、根据build-profile.json5中的配置创建对应的worker.ts文件。

worker.ts文件:

import worker from '@ohos.worker';

let parent = worker.workerPort;

// 处理来自主线程的消息
parent.onmessage = function(message) {
    console.info("onmessage: " + message)
    // 发送消息到主线程
    parent.postMessage("message from worker thread.")
}

 3、主线程中使用如下方式初始化和使用worker。

import worker from '@ohos.worker';

let wk = new worker.ThreadWorker("entry/ets/workers/worker.ts");

// 发送消息到worker线程
wk.postMessage("message from main thread.")

// 处理来自worker线程的消息
wk.onmessage = function(message) {
    console.info("message from worker: " + message)

    // 根据业务按需停止worker线程
    wk.terminate()
}

注意:build-profile.json5中配置的worker.ts的相对路径都为./src/main/ets/workers/worker.ts时,在Stage模型下创建worker需要传入路径 entry/ets/workers/worker.ts;

TaskPool介绍

任务池(taskpool)作用是为应用程序提供一个多线程的运行环境,降低整体资源的消耗、提高系统的整体性能,且您无需关心线程实例的生命周期。您可以使用任务池API创建后台任务(Task),并对所创建的任务进行如任务执行、任务取消的操作。理论上您可以使用任务池API创建数量不受限制的任务,但是出于内存因素不建议您这样做。此外,不建议您在任务中执行阻塞操作,特别是无限期阻塞操作,长时间的阻塞操作占据工作线程,可能会阻塞其他任务调度,影响您的应用性能。

所创建的同一优先级任务的执行顺序可以由你决定,任务真实执行的顺序与您调用任务池API提供的任务执行接口顺序一致。任务默认优先级是MEDIUM。(任务优先级机制暂未支持)

当同一时间待执行的任务数量大于任务池工作线程数量,任务池会根据负载均衡机制进行扩容,增加工作线程数量,减少整体等待时长。同样,当执行的任务数量减少,工作线程数量大于执行任务数量,部分工作线程处于空闲状态,任务池会根据负载均衡机制进行缩容,减少工作线程数量。(负载均衡机制暂未支持)

使用TaskPool

本模块首批接口从API version 9 开始支持。

import taskpool from '@ohos.taskpool';

Priority

表示所创建任务(Task)的优先级。(暂未支持)

系统能力: SystemCapability.Utils.Lang

名称

说明

HIGH

0

任务为高优先级。

MEDIUM

1

任务为中优先级。

LOW

2

任务为低优先级。

Task

表示任务。使用以下方法前,需要先构造Task。

constructor

constructor(func: Function, ...args: unknown[])

Task的构造函数。

系统能力: SystemCapability.Utils.Lang

参数:

参数名

类型

必填

说明

func

Function

任务执行需要传入函数,支持的函数返回值类型请查序列化支持类型

args

unknown[]

任务执行传入函数的参数,支持的参数类型请查序列化支持类型。默认值为undefined。

错误码:

以下错误码的详细介绍请参见语言基础类库错误码

错误码ID

错误信息

10200014

The function is not mark as concurrent.

示例

@Concurrent
function func(args) {
    console.log("func: " + args);
    return args;
}

let task = new taskpool.Task(func, "this is my first Task");

 taskpool.execute

execute(func: Function, ...args: unknown[]): Promise<unknown>

将待执行的函数放入taskpool内部任务队列等待,等待分发到工作线程执行。当前执行模式不可取消任务。

@Concurrent
function func(args) {
    console.log("func: " + args);
    return args;
}

async function taskpoolTest() {
  let value = await taskpool.execute(func, 100);
  console.log("taskpool result: " + value);
}

taskpoolTest();

注意事项

  • 仅支持在Stage模型且module的compileMode为esmodule的project中使用taskpool api。确认module的compileMode方法:查看当前module的build-profile.json5,在buildOption中补充"compileMode": "esmodule"。
  • taskpool任务只支持引用入参传递或者import的变量,不支持使用闭包变量,使用装饰器@Concurrent进行拦截。
  • taskpool任务只支持普通函数或者async函数,不支持类成员函数或者匿名函数,使用装饰器@Concurrent进行拦截。
  • 装饰器@Concurrent仅支持在ets文件使用,在ts文件中创建taskpool任务需使用"use concurrent"。

TaskPool和Worker的对比选择

TaskPool(任务池)和Worker的作用是为应用程序提供一个多线程的运行环境,用于处理耗时的计算任务或其他密集型任务。可以有效地避免这些任务阻塞主线程,从而最大化系统的利用率,降低整体资源消耗,并提高系统的整体性能。

实现特点对比

表1 TaskPool和Worker的实现特点对比

实现

TaskPool

Worker

内存模型

线程间隔离,内存不共享。

线程间隔离,内存不共享。

参数传递机制

采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。

支持ArrayBuffer转移和SharedArrayBuffer共享。

采用标准的结构化克隆算法(Structured Clone)进行序列化、反序列化,完成参数传递。

支持ArrayBuffer转移和SharedArrayBuffer共享。

参数传递

直接传递,无需封装,默认进行transfer。

消息对象唯一参数,需要自己封装。

方法调用

直接将方法传入调用。

在Worker线程中进行消息解析并调用对应方法。

返回值

异步调用后默认返回。

主动发送消息,需在onmessage解析赋值。

生命周期

TaskPool自行管理生命周期,无需关心任务负载高低。

开发者自行管理Worker的数量及生命周期。

任务池个数上限

自动管理,无需配置。

同个进程下,最多支持同时开启8个Worker线程。

任务执行时长上限

无限制。

无限制。

设置任务的优先级

不支持。

不支持。

执行任务的取消

支持取消任务队列中等待的任务。

不支持。

适用场景对比

TaskPool偏向独立任务维度,该任务在线程中执行,无需关注线程的生命周期,超长任务(大于3分钟)会被系统自动回收;而Worker偏向线程的维度,支持长时间占据线程执行,需要主动管理线程生命周期。

常见的一些开发场景及适用具体说明如下:

  • 有关联的一系列同步任务。例如在一些需要创建、使用句柄的场景中,句柄创建每次都是不同的,该句柄需永久保存,保证使用该句柄进行操作,需要使用Worker。
  • 需要频繁取消的任务。例如图库大图浏览场景,为提升体验,会同时缓存当前图片左右侧各2张图片,往一侧滑动跳到下一张图片时,要取消另一侧的一个缓存任务,需要使用TaskPool。
  • 大量或者调度点较分散的任务。例如大型应用的多个模块包含多个耗时任务,不方便使用8个Worker去做负载管理,推荐采用TaskPool。

TaskPool注意事项

  • 实现任务的函数需要使用装饰器@Concurrent标注,且仅支持在.ets文件中使用。

  • 实现任务的函数入参需满足序列化支持的类型,详情请参见数据传输对象

  • 由于不同线程中上下文对象是不同的,因此TaskPool工作线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用。

  • 序列化传输的数据量大小限制为16MB。

Worker注意事项

  • 创建Worker时,传入的Worker.ts路径在不同版本有不同的规则,详情请参见文件路径注意事项

  • Worker创建后需要手动管理生命周期,且最多同时运行的Worker子线程数量为8个,详情请参见生命周期注意事项

  • Ability类型的Module支持使用Worker,Library类型的Module不支持使用Worker。

  • 创建Worker不支持使用其他Module的Worker.ts文件,即不支持跨模块调用Worker。

  • 由于不同线程中上下文对象是不同的,因此Worker线程只能使用线程安全的库,例如UI相关的非线程安全库不能使用。

  • 序列化传输的数据量大小限制为16MB。

写在最后

  • 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
  • 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  • 关注博主,同时可以期待后续文章ing🚀,不定期分享原创知识。
  • 想要获取更多完整鸿蒙最新VIP学习资料,请关注猫哥公众号【猫青年】,回复“鸿蒙”获取

其他资源

鸿蒙开发笔记(十三): 线程模型,线程间通信,Emitter,Workder_鸿蒙 work 线程-优快云博客

【愚公系列】2023年12月 HarmonyOS教学课程 053-Stage模型(线程模型)-优快云博客

https://rtos_yuan.gitee.io/oh_app/#/custom_comon_event

文档中心--TaskPool和Worker

文档中心--@ohos.taskpool(使用任务池)

<think>好的,我现在需要帮助用户在HarmonyOS开发一个不阻碍触摸操作的光标应用,这个光标要能跟随触控位置移动,并且不阻塞其他触摸事件。首先,我得回忆一下HarmonyOS线程模型UI更新机制。根据引用[3],HarmonyOS提供了EmitterWorker来处理线程间通信,主线程负责UI,耗时任务应该放到WorkerTaskPool中,避免阻塞主线程。 用户的需求是光标实时跟随触摸位置,同时不阻碍其他触摸操作,比如点击滑动。触摸事件的处理通常在主线程,但如果处理逻辑复杂或者有耗时操作,会导致界面卡顿。因此,触摸事件的监听应该放在主线程,而光标的移动可能需要频繁的UI更新,这里需要确保UI更新不会延迟其他触摸事件。 首先,我需要考虑如何监听触摸事件。在HarmonyOS的Ability或AbilitySlice中,可以通过覆写onTouchEvent方法或者给组件设置触摸监听器来获取触摸位置。当用户触摸屏幕时,系统会触发这些事件,并传递MotionEvent对象,其中包含触控点的坐标信息。 接下来,光标的位置需要根据这些坐标实时更新。但是,如果直接在onTouchEvent中进行UI更新(比如修改光标组件的位置),这通常是在主线程执行的,如果UI更新操作本身不耗时,应该不会有问题。不过,如果用户界面复杂或者有其他操作,可能需要优化。 不过用户提到要“不阻碍触摸操作”,可能担心频繁的UI更新会影响其他触摸事件的响应。这时候,是否需要将光标的位置计算放在子线程中处理?根据引用[3],Worker用于执行耗时任务,而Emitter用于线程间的事件同步。但这里的光标移动可能只是简单的坐标更新,计算量不大,所以可能不需要放到Worker中。反而,如果UI更新必须在主线程进行,那么应该确保触摸事件处理不会因为其他任务而延迟。 因此,可能的步骤是: 1. 在AbilitySlice中监听触摸事件,获取触控坐标。 2. 将坐标传递给UI组件,更新光标位置。 3. 确保这些操作在主线程快速完成,避免阻塞。 另外,需要考虑光标的实现方式。比如,使用一个自定义组件作为光标,通过设置其位置属性(如layoutPosition)来移动。每次触摸事件触发时,获取新的坐标,并调用invalidate方法请求重绘,或者直接更新位置属性。 另一个考虑点是,如果应用有其他需要处理触摸操作的组件(比如按钮),需要确保光标的位置更新不会干扰这些组件的触摸事件。可能需要调整布局,确保光标层在视图层级中不会拦截触摸事件。例如,将光标所在的组件设置为不拦截触摸事件,或者调整触摸事件的分发逻辑。 例如,在HarmonyOS中,可以通过设置组件的touchable属性为false,使得该组件不会拦截触摸事件,但依然可以接收父容器传递的事件。或者,在触摸监听器中处理完光标移动后,继续将事件传递给子组件处理。 此外,引用[1]中提到的事件处理函数,类似计算器应用中的按钮点击处理,需要确保触摸事件的分发正确,避免被某个组件独占。因此,在光标移动的同时,其他触摸操作(如点击按钮)应该仍然能够被正常触发。 可能的代码结构: - 在布局文件中,有一个自定义的CursorView用于显示光标,可能是一个ImageView或者自定义绘制的组件。 - 在AbilitySlice中,设置根布局的触摸监听器,当触摸移动时,更新CursorView的位置。 - 同时,其他交互组件(如按钮)需要正常响应触摸事件,所以需要确保触摸事件能够传递到它们。 关于线程模型,由于触摸事件处理本身在主线程,而更新UI也需要在主线程完成,所以不需要额外使用Worker。但如果在处理触摸事件时有其他耗时操作(比如网络请求或复杂计算),才需要将这些操作放到Worker中,通过Emitter或事件总线通知主线程更新UI。 例如,如果需要在触摸事件中进行数据处理,可以这样做: 1. 在主线程中监听触摸事件,获取坐标。 2. 将坐标数据通过Emitter发送到Worker线程处理。 3. Worker处理完成后,通过Emitter将结果发送回主线程更新UI。 但根据用户的需求,可能不需要这样的复杂处理,只需实时更新UI即可。因此,关键点在于正确设置触摸监听,并高效更新光标位置,同时不影响其他触摸事件的处理。 总结步骤: 1. 创建自定义光标组件,并添加到布局中。 2. 在AbilitySlice中监听屏幕的触摸事件(如onTouchEvent)。 3. 在触摸事件回调中获取触控点的坐标(如event.getPointerPosition(0))。 4. 更新光标组件的位置属性,使用主线程的UI线程进行快速更新。 5. 确保其他UI组件仍能接收触摸事件,可能需要调整组件层级或触摸事件传递逻辑。 可能的代码示例: 在布局XML中添加一个ImageView作为光标: ```xml <DirectionalLayout ...> <Image id="$+id:cursor" width="24vp" height="24vp" ... /> <!-- 其他UI组件 --> </DirectionalLayout> ``` 在AbilitySlice中: ```java public class MainAbilitySlice extends AbilitySlice { private Image cursor; private DirectionalLayout rootLayout; @Override public void onStart(Intent intent) { super.onStart(intent); // 加载布局 setUIContent(ResourceTable.Layout_main_layout); cursor = (Image) findComponentById(ResourceTable.Id_cursor); rootLayout = (DirectionalLayout) findComponentById(ResourceTable.Id_root_layout); // 设置触摸监听 rootLayout.setTouchEventListener(new Component.TouchEventListener() { @Override public boolean onTouchEvent(Component component, MotionEvent event) { float x = event.getPointerPosition(0).getX(); float y = event.getPointerPosition(0).getY(); // 更新光标位置 cursor.setPosition((int)x - cursor.getWidth()/2, (int)y - cursor.getHeight()/2); // 返回false表示不消费事件,允许传递到其他组件 return false; } }); } } ``` 这里的关键是onTouchEvent返回false,允许触摸事件继续传递给子组件,这样按钮等组件仍能接收到事件。同时,更新光标的位置,确保其跟随触摸点。 另外,关于线程,如果仅仅是设置位置,这属于UI操作,必须在主线程执行。上述代码中的触摸事件处理在主线程,因此无需额外线程处理。但如果有其他耗时操作,比如记录坐标到文件或网络传输,就需要使用Worker。 因此,最终方案是在主线程处理触摸事件,快速更新UI,并且不阻止事件传递,确保其他组件的触摸操作正常响应。</think>在HarmonyOS中实现跟随触控位置移动的非阻塞光标,需结合触摸事件监听、UI线程优化组件层级管理。以下是分步实现方案: --- ### **1. 触摸事件监听与坐标获取** 在`AbilitySlice`中,通过根布局的触摸监听器获取触控坐标: ```java rootLayout.setTouchEventListener((component, event) -> { float touchX = event.getPointerPosition(0).getX(); float touchY = event.getPointerPosition(0).getY(); updateCursorPosition(touchX, touchY); // 更新光标位置 return false; // 返回false允许事件传递给子组件 }); ``` - **关键点**:`return false`确保触摸事件继续传递,避免阻塞按钮点击等操作[^1]。 --- ### **2. 非阻塞UI更新** 在`updateCursorPosition`方法中,直接通过主线程更新光标组件位置: ```java private void updateCursorPosition(float x, float y) { cursor.setPosition((int)x - cursor.getWidth()/2, (int)y - cursor.getHeight()/2); } ``` - **优化逻辑**:所有UI操作默认在主线程执行,无需额外线程,确保低延迟更新[^3]。 --- ### **3. 组件层级与触摸穿透** 在XML布局中设置光标组件为**不可触摸**(`touchable="false"`),避免拦截事件: ```xml <Image id="$+id:cursor" touchable="false" ... /> ``` - **作用**:光标层不消费触摸事件,下方按钮等组件仍可正常响应[^1]。 --- ### **4. 性能优化(可选)** 若需处理复杂逻辑(如坐标记录),使用`Worker`线程执行耗时任务: ```java // 创建Worker线程 Worker worker = new Worker.Builder().build(); worker.postTask(() -> { // 耗时操作(如存储坐标) saveCoordinatesToFile(x, y); }); ``` - **注意**:通过`Emitter`将结果传回主线程更新UI[^3]。 --- ### **完整代码示例** **布局文件(`resources/base/layout/main_layout.xml`)**: ```xml <DirectionalLayout id="$+id:root_layout" width="match_parent" height="match_parent" background_element="#FFFFFF"> <Image id="$+id:cursor" width="24vp" height="24vp" touchable="false" image_src="graphic/cursor_icon.png"/> <Button id="$+id:test_button" width="100vp" height="50vp" text="点击测试"/> </DirectionalLayout> ``` **逻辑代码(`MainAbilitySlice.java`)**: ```java public class MainAbilitySlice extends AbilitySlice { private Image cursor; private DirectionalLayout rootLayout; @Override public void onStart(Intent intent) { super.onStart(intent); setUIContent(ResourceTable.Layout_main_layout); cursor = (Image) findComponentById(ResourceTable.Id_cursor); rootLayout = (DirectionalLayout) findComponentById(ResourceTable.Id_root_layout); rootLayout.setTouchEventListener((component, event) -> { float touchX = event.getPointerPosition(0).getX(); float touchY = event.getPointerPosition(0).getY(); updateCursorPosition(touchX, touchY); return false; // 事件继续传递 }); } private void updateCursorPosition(float x, float y) { getUITaskDispatcher().asyncDispatch(() -> { cursor.setPosition((int)x - 12, (int)y - 12); // 24x24光标居中 }); } } ``` --- ### **效果验证** 1. 光标实时跟随触控位置移动。 2. 按钮等组件可正常响应点击事件。 3. 滑动操作流畅无卡顿。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

特立独行的猫a

您的鼓励是我的创作动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值