HarmonyOS 鸿蒙Next Worker 线程反复创建几轮就崩了?Worker 生命周期管理与内存回收实战
HarmonyOS 鸿蒙Next Worker 线程反复创建几轮就崩了?Worker 生命周期管理与内存回收实战 有个需求是用户每选一张图片就开一个 Worker 做缩放+压缩,用完 terminate 掉。测试的时候发现:
- 连续处理 15-20 张图之后,DevEco Profiler 里 native heap 持续往上涨,Worker terminate 了但内存不降
- 有时候处理到第 10 张左右直接闪退,hilog 里看到 OOM相关日志
- 试过 Worker 复用,但发现 Worker 里用过的变量第二次进 onmessage 时还残留着上次的值,导致结果不对
建议不要每张图片都新建并销毁一个 Worker,改成固定数量 Worker 或单个 Worker 复用,并把每次任务设计成无状态。Worker 被 terminate 后退出和内存回收不是同步保证;频繁处理大图时,native heap 持续上涨通常和大对象拷贝、主线程/Worker 仍持有引用、并发解码压缩过多有关。
处理建议:
-
主线程只创建少量 ThreadWorker,给每个任务带 requestId;Worker 中不要用模块级变量保存上一次图片/结果,或在 finally 中置空。
-
大的 ArrayBuffer 用 postMessage(message, { transfer: [buffer] }) 转移所有权,减少重复拷贝;适合 sendable 的场景再考虑 postMessageWithSharedSendable。
-
onmessage/onerror/onexit 只注册一次,不要每次任务叠加监听;任务完成后清掉主线程 jobMap、回调、图片 buffer 引用。
-
Worker 复用时变量还在是正常现象,因为 Worker 线程上下文是长生命周期的;每次 onmessage 开头都要按 requestId 初始化本次局部状态,不能依赖全局变量自动重置。
-
如果确实需要销毁,主线程调用 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"
]
}
}
}
你当前观测到的三类现象成因如下:
- 调用
terminate()后原生堆内存未立即回落:属于正常表现。线程持续运行、图像编解码产生的原生对象、内存分配器缓存均会造成内存延迟回收;高频新建 Worker 会进一步加剧该现象。 - 加载至第 10 张图前后触发 OOM:核心诱因多为并发 Worker 数量超限、原图数据存在冗余拷贝、
PixelMap/ImageSource/Packer资源未主动释放,或是事件回调闭包持续持有巨型内存对象。 - 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。
解决办法:
- 不要反复创建,改用固定 Worker 实例,通过
postMessage传入不同图片任务。 - 复用 Worker 时变量残留:因为 Worker 的顶层/模块作用域是持久的,上次任务的数据仍存在。每次
onmessage开头重置所有状态,或把临时数据都放进message参数中的局部变量,避免使用全局缓存。 - 尽量手动释放大对象:在 Worker 内处理完图片后,显式将
ArrayBuffer/ArrayBufferView置空并调用close()(若用TaskPool则用taskpool.execute后自动回收)。 - 监听退出:
workerInstance.onexit = () => { workerInstance = null; },确保引用释放。 - 使用
TaskPool替代 Worker,它由系统统一管理线程与内存,更适合短任务场景。
