前言

你有没有遇到过这种情况:列表页加载 5000 条数据,排序逻辑一跑,整个页面直接冻住?用户疯狂点击屏幕没反应,心想这 App 怎么跟死机了一样。

A hand-drawn doodle illustration on pure white pap

根本原因就是耗时任务占了主线程,UI 没机会刷新。 解决方案很简单——把耗时任务扔到子线程去跑,Worker 就是干这个的。

JavaScript 天生是单线程的,ArkTS 也没逃掉这个基因。但 HarmonyOS7 给了两个多线程方案:Worker 和 TaskPool。TaskPool 用起来更简单,但 Worker 更灵活、更适合长任务。

说实话,如果你只是想跑个简单计算,TaskPool 够用了。 但如果你的任务要跑几十分钟、需要维护状态、要跟主线程频繁通信——Worker 才是正解。今天重点聊 Worker。

为什么需要多线程

主线程只有一个职责:刷新 UI。所有用户能看到的东西,都在主线程上渲染。

A hand-drawn doodle illustration on pure white pap

一旦你在主线程干重活——大量计算、文件读写、JSON 解析大对象——UI 就卡了。而且 HarmonyOS 对主线程有超时检测,阻塞超过一定时间直接 ANR(应用无响应)。

所以规则很简单:跟 UI 无关的活,统统扔到子线程。

Worker vs TaskPool

很多人分不清这俩,先来张对比表:

维度 Worker TaskPool
线程生命周期 开发者手动管理 系统自动管理
任务粒度 整个线程级别 单个任务级别
长任务支持 ✅ 可以跑几小时 ❌ 超过 3 分钟会被系统回收
状态维护 ✅ 线程内可以维护持久状态 ❌ 任务结束状态就没了
优先级控制 ✅ API 18+ 支持 ✅ 支持
任务取消 需自己实现 ✅ 原生支持 cancel()
负载均衡 ❌ 手动管理 ✅ 自动扩缩容
创建开销 较大,需要新建线程 小,复用线程池
通信方式 postMessage / onmessage 任务返回值

一句话总结:短任务用 TaskPool,长任务用 Worker。 不确定就用 TaskPool,省心。

Worker 创建与通信

A hand-drawn doodle illustration on pure white pap

Worker 的通信模型就是经典的 Actor 模型——主线程和 Worker 线程内存完全隔离,只能通过 postMessage 互发消息。

先看主线程怎么创建 Worker 并收发消息:

import { worker, MessageEvents } from '@kit.ArkTS';

@Entry
@Component
struct WorkerDemo {
  @State result: string = '等待计算...'
  private workerInstance: worker.ThreadWorker | null = null

  aboutToAppear() {
    this.workerInstance = new worker.ThreadWorker('entry/ets/workers/ComputeWorker.ts')
    
    this.workerInstance.onmessage = (e: MessageEvents) => {
      this.result = `计算结果: ${e.data.result}`
    }
    
    this.workerInstance.onerror = (e: ErrorEvent) => {
      console.error(`Worker 出错: ${e.message}`)
    }
  }

  startCompute() {
    if (this.workerInstance) {
      this.workerInstance.postMessage({ action: 'compute', data: 1000 })
    }
  }

  aboutToDisappear() {
    this.workerInstance?.terminate()
    this.workerInstance = null
  }

  build() {
    Column() {
      Text(this.result).fontSize(20)
      Button('开始计算').onClick(() => this.startCompute())
    }
  }
}

再看 Worker 线程那边的代码(entry/ets/workers/ComputeWorker.ts):

import { worker, ThreadWorkerGlobalScope, MessageEvents } from '@kit.ArkTS';

let workerPort: ThreadWorkerGlobalScope = worker.workerPort;

workerPort.onmessage = (e: MessageEvents) => {
  if (e.data.action === 'compute') {
    let num: number = e.data.data as number
    let result: number = 0
    for (let i = 0; i < num; i++) {
      result += i
    }
    workerPort.postMessage({ result: result })
  }
}

workerPort.onerror = (e: ErrorEvent) => {
  console.error(`Worker 内部错误: ${e.message}`)
}

关键代码讲解:

  • new worker.ThreadWorker('entry/ets/workers/ComputeWorker.ts') —— 主线程创建 Worker,参数是 Worker 脚本的相对路径,路径写错直接报错
  • worker.workerPort —— Worker 线程侧的通信端口,必须定义在顶层作用域,不能用 const
  • postMessage() —— 发送消息,数据会被序列化后传递
  • onmessage —— 接收消息的回调,e.data 就是对方发过来的数据
  • terminate() —— 主线程主动销毁 Worker,组件销毁时一定要调,否则线程泄漏

耗时计算实战

来个真实的场景:图片灰度处理。一张 1920×1080 的图片有 200 万像素,主线程处理直接卡死。

import { worker, MessageEvents } from '@kit.ArkTS';
import { image } from '@kit.ImageKit';

@Entry
@Component
struct ImageProcessPage {
  @State srcPixelMap: PixelMap | null = null
  @State processedPixelMap: PixelMap | null = null
  private workerInstance: worker.ThreadWorker | null = null

  aboutToAppear() {
    this.workerInstance = new worker.ThreadWorker('entry/ets/workers/GrayWorker.ts')
    
    this.workerInstance.onmessage = (e: MessageEvents) => {
      if (e.data.type === 'progress') {
        console.info(`处理进度: ${e.data.percent}%`)
      }
      if (e.data.type === 'done') {
        let buffer = e.data.buffer as ArrayBuffer
        image.createPixelMap(buffer, {
          size: { width: e.data.width, height: e.data.height },
          pixelFormat: 4,
          alphaType: 0
        }).then((pixelMap: PixelMap) => {
          this.processedPixelMap = pixelMap
        })
      }
    }
  }

  async processImage() {
    if (!this.srcPixelMap || !this.workerInstance) return

    let imageInfo = await this.srcPixelMap.getImageInfo()
    let width = imageInfo.size.width
    let height = imageInfo.size.height
    let buffer = new ArrayBuffer(width * height * 4)
    await this.srcPixelMap.readPixelsToBuffer(buffer)

    this.workerInstance.postMessage({
      type: 'gray',
      buffer: buffer,
      width: width,
      height: height
    })
  }

  aboutToDisappear() {
    this.workerInstance?.terminate()
    this.workerInstance = null
  }

  build() {
    Column() {
      Row() {
        Image(this.srcPixelMap).width('45%')
        Image(this.processedPixelMap).width('45%')
      }
      Button('灰度处理').onClick(() => this.processImage())
    }
  }
}

Worker 端灰度处理逻辑:

import { worker, ThreadWorkerGlobalScope, MessageEvents } from '@kit.ArkTS';

let workerPort: ThreadWorkerGlobalScope = worker.workerPort;

workerPort.onmessage = (e: MessageEvents) => {
  if (e.data.type === 'gray') {
    let buffer = e.data.buffer as ArrayBuffer
    let data = new Uint8Array(buffer)
    let width = e.data.width as number
    let height = e.data.height as number
    let total = width * height

    for (let i = 0; i < total; i++) {
      let offset = i * 4
      let r = data[offset]
      let g = data[offset + 1]
      let b = data[offset + 2]
      let gray = Math.floor(0.299 * r + 0.587 * g + 0.114 * b)
      data[offset] = gray
      data[offset + 1] = gray
      data[offset + 2] = gray

      if (i % 50000 === 0) {
        workerPort.postMessage({
          type: 'progress',
          percent: Math.floor((i / total) * 100)
        })
      }
    }

    workerPort.postMessage({
      type: 'done',
      buffer: buffer,
      width: width,
      height: height
    })
  }
}

关键代码讲解:

  • readPixelsToBuffer(buffer) —— 把 PixelMap 的像素数据读到 ArrayBuffer,这个 buffer 可以跨线程传递
  • new Uint8Array(buffer) —— Worker 端用 Uint8Array 视图操作像素数据,每 4 字节一组(RGBA)
  • 灰度算法 0.299R + 0.587G + 0.114B —— 经典的加权灰度公式,符合人眼对绿色更敏感的特性
  • 中间穿插的 progress 消息 —— 让主线程知道处理进度,可以显示进度条
  • ArrayBuffer 是可转移对象,传给 Worker 后主线程就不能再访问了,Worker 处理完需要传回来

线程间数据传递(序列化)

Worker 通信基于 结构化克隆算法,不是所有数据都能传。来理清楚:

✅ 可以直接传递的:

  • 基本类型:string、number、boolean
  • ArrayBuffer、TypedArray
  • 普通对象(plain object)
  • 数组

❌ 不能直接传递的:

  • 函数
  • 带原型链的对象(class 实例)
  • DOM 对象
  • @State@Observed 修饰的复杂类型

如果你需要传 class 实例,手动序列化成普通对象再传:

interface UserData {
  name: string
  age: number
}

class User {
  name: string = ''
  age: number = 0

  toJSON(): UserData {
    return { name: this.name, age: this.age }
  }
}

let user = new User()
user.name = '鸿蒙'
user.age = 3

this.workerInstance.postMessage(user.toJSON())

线程池管理

单个 Worker 好搞,但如果你需要同时处理多个任务呢?比如图片列表要并发压缩,总不能每张图建一个 Worker 吧。

简单实现一个 Worker 池:

import { worker, MessageEvents } from '@kit.ArkTS';

class WorkerPool {
  private workers: worker.ThreadWorker[] = []
  private taskQueue: { action: string; data: Object; resolve: Function }[] = []
  private busySet: Set<number> = new Set()
  private maxSize: number = 4

  constructor() {
    for (let i = 0; i < this.maxSize; i++) {
      let w = new worker.ThreadWorker('entry/ets/workers/PoolWorker.ts')
      let index = i
      w.onmessage = (e: MessageEvents) => {
        this.busySet.delete(index)
        this.processQueue()
      }
      this.workers.push(w)
    }
  }

  execute(action: string, data: Object): Promise<Object> {
    return new Promise((resolve) => {
      this.taskQueue.push({ action, data, resolve })
      this.processQueue()
    })
  }

  private processQueue() {
    while (this.taskQueue.length > 0) {
      let idleIndex = this.findIdleWorker()
      if (idleIndex === -1) break

      let task = this.taskQueue.shift()!
      this.busySet.add(idleIndex)
      this.workers[idleIndex].postMessage({ action: task.action, data: task.data })
    }
  }

  private findIdleWorker(): number {
    for (let i = 0; i < this.workers.length; i++) {
      if (!this.busySet.has(i)) return i
    }
    return -1
  }

  destroy() {
    this.workers.forEach(w => w.terminate())
    this.workers = []
  }
}

关键代码讲解:

  • maxSize: 4 —— 最多 4 个 Worker 并发,别开太多,线程本身有内存开销
  • taskQueue —— 任务队列,Worker 忙不过来时先排队
  • busySet —— 记录哪些 Worker 在忙,processQueue 每次找空闲 Worker 分配任务
  • destroy() —— 用完一定要销毁所有 Worker

实际项目中建议用 TaskPool 代替手动线程池,除非你的任务确实需要 Worker 的长驻特性。

写在最后

多线程不是银弹,但主线程卡顿的问题必须解决。记住两个原则:UI 只管渲染,计算扔给子线程;短任务 TaskPool,长任务 Worker。

Worker 用完要 terminate(),数据传递注意序列化限制,线程池别开太多——这三点做到,多线程就不会给你惹麻烦。下一篇聊聊 UIAbility 生命周期,跟 Worker 的销毁时机密切相关,建议一起看。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐