HarmonyOS 鸿蒙Next Worker 线程反复创建几轮就崩了?Worker 生命周期管理与内存回收实战

HarmonyOS 鸿蒙Next Worker 线程反复创建几轮就崩了?Worker 生命周期管理与内存回收实战 有个需求是用户每选一张图片就开一个 Worker 做缩放+压缩,用完 terminate 掉。测试的时候发现:

  1. 连续处理 15-20 张图之后,DevEco Profiler 里 native heap 持续往上涨,Worker terminate 了但内存不降
  2. 有时候处理到第 10 张左右直接闪退,hilog 里看到 OOM相关日志
  3. 试过 Worker 复用,但发现 Worker 里用过的变量第二次进 onmessage 时还残留着上次的值,导致结果不对
10 回复

建议不要每张图片都新建并销毁一个 Worker,改成固定数量 Worker 或单个 Worker 复用,并把每次任务设计成无状态。Worker 被 terminate 后退出和内存回收不是同步保证;频繁处理大图时,native heap 持续上涨通常和大对象拷贝、主线程/Worker 仍持有引用、并发解码压缩过多有关。

处理建议:

  1. 主线程只创建少量 ThreadWorker,给每个任务带 requestId;Worker 中不要用模块级变量保存上一次图片/结果,或在 finally 中置空。

  2. 大的 ArrayBuffer 用 postMessage(message, { transfer: [buffer] }) 转移所有权,减少重复拷贝;适合 sendable 的场景再考虑 postMessageWithSharedSendable。

  3. onmessage/onerror/onexit 只注册一次,不要每次任务叠加监听;任务完成后清掉主线程 jobMap、回调、图片 buffer 引用。

  4. Worker 复用时变量还在是正常现象,因为 Worker 线程上下文是长生命周期的;每次 onmessage 开头都要按 requestId 初始化本次局部状态,不能依赖全局变量自动重置。

  5. 如果确实需要销毁,主线程调用 worker.terminate();Worker 内主动结束可调用 workerPort.close()。同时限制图片处理并发,避免多个大图同时解码/压缩导致 OOM。

更多关于HarmonyOS 鸿蒙Next Worker 线程反复创建几轮就崩了?Worker 生命周期管理与内存回收实战的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


建议不要“每张图创建一个 Worker,用完就 terminate”。图片缩放/压缩会碰到 native 内存、编解码缓存、线程运行时回收延迟,频繁建销毁很容易把 native heap 顶上去。

正确做法是 固定少量 Worker + 队列 + 每次任务清空 Worker 内部上下文

主线程用 worker.ThreadWorker 创建 Worker,通过 postMessage 通信,任务完成后可 terminate();Worker 是隔离线程,通过消息传递数据。相关文档:[@ohos.worker](https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-worker)。

推荐结构

页面
  -> ImageWorkerPool
      -> 固定 1~2 个 Worker
      -> 任务排队
      -> 每个 Worker 同一时间只处理 1 个任务
      -> 页面销毁时统一 terminate

Index.ets

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

class ImageJob {
  id: number = 0;
  buffer: ArrayBuffer = new ArrayBuffer(0);

  constructor(id: number, buffer: ArrayBuffer) {
    this.id = id;
    this.buffer = buffer;
  }
}

class WorkerResult {
  id: number = 0;
  size: number = 0;
  success: boolean = false;
  message: string = '';
}

@Entry
@Component
struct Index {
  @State private log: string = '等待处理';
  private workerInstance: worker.ThreadWorker | null = null;
  private nextId: number = 1;
  private isBusy: boolean = false;
  private queue: ImageJob[] = [];

  aboutToDisappear(): void {
    this.releaseWorker();
  }

  private ensureWorker(): void {
    if (this.workerInstance !== null) {
      return;
    }

    let instance = new worker.ThreadWorker('entry/ets/workers/ImageWorker.ets');

    instance.onmessage = (event: MessageEvents): void => {
      let result = event.data as WorkerResult;
      this.isBusy = false;
      this.log = `任务 ${result.id} 完成,压缩后大小:${result.size}`;
      this.runNext();
    };

    instance.onerror = (err: ErrorEvent): void => {
      this.isBusy = false;
      this.log = `Worker 错误:${err.message}`;
      this.runNext();
    };

    instance.onexit = (code: number): void => {
      console.info(`Worker exit: ${code}`);
    };

    this.workerInstance = instance;
  }

  private addMockImage(): void {
    let id = this.nextId;
    this.nextId += 1;

    // 示例用 ArrayBuffer 模拟图片原始数据。真实业务里可传图片路径或 URI,避免大对象频繁复制。
    let buffer = new ArrayBuffer(1024 * 1024 * 4);
    this.queue.push(new ImageJob(id, buffer));
    this.log = `已加入任务 ${id},队列长度:${this.queue.length}`;
    this.runNext();
  }

  private runNext(): void {
    if (this.isBusy) {
      return;
    }

    if (this.queue.length === 0) {
      return;
    }

    this.ensureWorker();

    let instance = this.workerInstance;
    if (instance === null) {
      return;
    }

    let job = this.queue.shift();
    if (job === undefined) {
      return;
    }

    this.isBusy = true;
    this.log = `正在处理任务 ${job.id}`;

    // 大 ArrayBuffer 建议转移所有权,减少主线程和 Worker 间复制。
    instance.postMessage(job, [job.buffer]);
  }

  private releaseWorker(): void {
    if (this.workerInstance !== null) {
      this.workerInstance.terminate();
      this.workerInstance = null;
    }

    this.queue.splice(0, this.queue.length);
    this.isBusy = false;
  }

  build() {
    Column({ space: 16 }) {
      Text(this.log)
        .fontSize(16)

      Button('模拟选择一张图片')
        .onClick(() => {
          this.addMockImage();
        })

      Button('释放 Worker')
        .onClick(() => {
          this.releaseWorker();
          this.log = 'Worker 已释放';
        })
    }
    .width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

ImageWorker.ets

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

class ImageJob {
  id: number = 0;
  buffer: ArrayBuffer = new ArrayBuffer(0);
}

class WorkerResult {
  id: number = 0;
  size: number = 0;
  success: boolean = false;
  message: string = '';
}

const workerPort: ThreadWorkerGlobalScope = worker.workerPort;

workerPort.onmessage = (event: MessageEvents): void => {
  let job = event.data as ImageJob;

  // 关键:每次任务都创建新的局部上下文,不复用上一次的临时变量。
  let input: Uint8Array = new Uint8Array(job.buffer);
  let outputSize: number = Math.floor(input.byteLength / 2);

  let result = new WorkerResult();
  result.id = job.id;
  result.size = outputSize;
  result.success = true;
  result.message = 'ok';

  workerPort.postMessage(result);

  // 真实图片处理时,这里要释放 imageSource、pixelMap、packer 等 native 资源。
  // 不要把大数组、PixelMap、ImageSource 放到模块级变量里缓存。
};

workerPort.onerror = (err: ErrorEvent): void => {
  console.error(`ImageWorker error: ${err.message}`);
};

build-profile.json5

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

你当前观测到的三类现象成因如下:

  1. 调用 terminate() 后原生堆内存未立即回落:属于正常表现。线程持续运行、图像编解码产生的原生对象、内存分配器缓存均会造成内存延迟回收;高频新建 Worker 会进一步加剧该现象。
  2. 加载至第 10 张图前后触发 OOM:核心诱因多为并发 Worker 数量超限、原图数据存在冗余拷贝、PixelMap/ImageSource/Packer 资源未主动释放,或是事件回调闭包持续持有巨型内存对象。
  3. Worker 复用后存在变量残留:根源是任务状态存储在 Worker 文件的模块级全局变量中。Worker 复用场景下进程不会重启,模块级变量会持续保留上一轮数据;业务任务数据建议仅存于 onmessage 局部作用域,或每次执行任务前手动重置状态。

这种场景不建议每张图片都新建一个 Worker 再 terminate。图片缩放/压缩会涉及解码缓存、native buffer 和线程运行时,terminate 只结束 Worker,本轮 native 内存不一定立刻回落。

更稳的做法是固定 1 到 2 个 Worker 做队列消费:每个任务结束后把 PixelMap、ArrayBuffer、大数组、临时状态全部置空,并给每个任务分配独立 taskId,避免复用 Worker 时读到上一轮变量。主线程侧也要在收到结果后及时释放输入和输出引用。

如果单次处理是短 CPU 任务,可以优先评估 TaskPool,让系统管理线程复用;如果必须使用 Worker,就把它当长期工作线程,而不是频繁创建销毁的临时对象。

这个问题其实很多图片处理、音视频处理场景都会遇到,主要不是 terminate() 没调用,而是 Worker 的资源释放机制和大对象生命周期没有处理好

当前现象:

Worker创建 → 图片压缩 → terminate → 内存一直涨

第一个,terminate 后 native heap 不马上降

这个是正常现象。

Worker 里面如果处理:

  • PixelMap
  • ArrayBuffer
  • 大 Uint8Array
  • 图片缓存

这些很多是在 native 层申请的。

worker.terminate() 只是结束线程,不代表 native 内存马上归还给系统。

所以 Profiler 看到:

Worker没了,但是 heap还高

不一定代表泄漏。

重点看:

多轮执行后是否持续增长。

如果一直涨,说明有引用没释放。

第二个,处理十几张直接 OOM

你的方案:

一张图 → 创建 Worker → 压缩 → terminate

其实压力比较大。

因为每次:

  • 创建线程
  • 加载 worker上下文
  • 初始化运行环境
  • 申请图片内存
  • 释放

反复几十次,很容易产生内存碎片。

图片压缩建议:

不要一个图片一个 Worker。

改用以下方案:

  • 创建 Worker池:
    • 启动2~3个Worker
    • 图片任务排队:
      • 图片1 → Worker1
      • 图片2 → Worker2
    • 完成后继续复用。

第三个,Worker复用变量残留

这个也正常。

Worker不是函数调用。

它是一个长期运行环境。

比如:

let result = []

onmessage = () => {
   result.push(...)
}

第二次执行:

result还存在。

所以每次任务开始:

主动清理状态。

比如:

onmessage = (msg) => {
   let temp = []
   //处理
   temp = null
}

不要把任务数据挂在全局。

图片场景还要注意:

  • 不要直接传大对象。
  • 比如:PixelMap / ArrayBuffer
  • 尽量:
    • 传路径
    • Worker内部读取
    • 或者用 Transferable 转移所有权。
  • 否则:
    • 主线程一份
    • Worker一份
    • 内存直接翻倍。

比较稳定的架构:

  • 主线程
    • → Worker池(2~3个)
    • → 单任务处理
    • → 返回结果
    • → 清理临时变量

不要:

每张图片创建Worker。

目前你的情况大概率是:

频繁创建Worker + 图片native内存释放滞后 + 大对象复制

三个叠加导致的。

先改Worker复用,通常 OOM 就会明显改善。

希望可以帮到你~~~

短期耗时任务,可以用taskpool线程,线程自动管理,自动复用,能满足大部分场景下的工作。长期耗时任务再考虑用Woker,需要自己管理约束线程。

学习了。

问题一思路:使用 image 等组件申请 pixelmap,需严格关注对象的生命周期,在使用结束后直接使用 release 或destroy等API 接口释放,而非依赖于 GC 回收。

问题二思路:限制并发 Worker 数量 不要用"每选一张图就开一个Worker"的模式,改用Worker池并任务队列

问题三思路:Worker 复用时,必须确保每次 onmessage 都是"无状态"的,

崩溃根因:Worker线程未正确销毁,导致句柄或内存耗尽。每次new Worker后,任务结束必须调用terminate()并置空引用;Worker内部也可调用close()自行退出。频繁创建时复用单例或线程池,避免循环中反复新建。同时监听error事件,异常时释放资源。

频繁创建 Worker 的典型问题:terminate() 是异步销毁,资源并非立即回收;且 Worker 线程内的 native 内存(图片解码、压缩缓冲)需等真正退出才释放。连续创建导致旧线程未销毁、新线程又申请,叠加后 OOM。

解决办法

  1. 不要反复创建,改用固定 Worker 实例,通过 postMessage 传入不同图片任务。
  2. 复用 Worker 时变量残留:因为 Worker 的顶层/模块作用域是持久的,上次任务的数据仍存在。每次 onmessage 开头重置所有状态,或把临时数据都放进 message 参数中的局部变量,避免使用全局缓存。
  3. 尽量手动释放大对象:在 Worker 内处理完图片后,显式将 ArrayBuffer/ArrayBufferView 置空并调用 close()(若用 TaskPool 则用 taskpool.execute 后自动回收)。
  4. 监听退出workerInstance.onexit = () => { workerInstance = null; },确保引用释放。
  5. 使用 TaskPool 替代 Worker,它由系统统一管理线程与内存,更适合短任务场景。
回到顶部