华为鸿蒙开发高级篇04-多线程与并发:TaskPool/Worker 实战
2026/9/16 9:22:50 网站建设 项目流程

华为鸿蒙开发高级篇04-多线程与并发:TaskPool/Worker 实战(图片批量压缩案例)

高级系列第 4 篇 · 案例:相册批量压缩工具——一次选择 50 张图,压缩、保存、进度展示,UI 全程不卡顿。 目标:搞清楚 ArkTS 并发模型(TaskPool vs Worker),掌握耗时任务下放子线程、线程间数据传递与进度回传的完整套路。

一、案例背景

图片压缩、大文件解析、模型推理这类 CPU 密集任务如果在主线程(UI 线程)执行,结果就是"点击后白屏、滑动掉帧、甚至 ANR"。正确的做法是:UI 线程只负责交互与渲染,重活交给并发线程,结果回传刷新

ArkTS 提供两套并发方案:

方案适用特点
TaskPool一次性/短任务、可复用线程池自动调度、任务级并发、推荐首选
Worker常驻长任务、需要独立生命周期手动创建/销毁、适合后台持续作业

图片批量压缩是典型的"短任务队列",用 TaskPool 最合适;文末给 Worker 的对照实现。

二、核心原理:TaskPool 工作方式

主线程 ── execute(任务函数, 参数) ──> TaskPool(系统线程池) <── onReceiveData(进度) ─── 子线程回调 <── Promise<结果> ───────── 任务完成

要点:

  • 任务函数必须是独立函数或独立 @Concurrent 函数,不能依赖闭包捕获主线程对象。
  • 传递的参数和返回值必须可序列化(基本类型、数组、普通对象;不能传自定义类实例/方法)。
  • 进度回传用sendData/onReceiveData,或直接返回聚合结果分批展示。

三、实际开发代码:图片批量压缩

3.1 并发任务函数(@Concurrent)

// concurrency/compressTask.ets import { taskpool } from '@kit.ArkTS' // @Concurrent:标记为可在线程池执行的函数 // 入参/返回值必须可序列化:这里传图片路径 + 压缩参数 @Concurrent function compressOne(arg: CompressArg): CompressResult { const { src, quality, index } = arg // 真实场景:读取 image source → 解码 → 压缩编码 → 写回文件 // 这里用伪实现模拟耗时与结果,替换为实际图像编解码代码即可 const start = Date.now() // 模拟压缩耗时 50~120ms while (Date.now() - start < 60 + (index % 60)) { // busy wait 模拟 CPU 密集 } const outPath = src.replace('.jpg', `_q${quality}.jpg`) return { index, outPath, savedBytes: 1024 * (quality < 60 ? 320 : 180) // 模拟压缩后大小 } } export interface CompressArg { src: string quality: number index: number } export interface CompressResult { index: number outPath: string savedBytes: number }

3.2 批量调度:TaskPool + 并发限制 + 进度

// service/CompressService.ets import { taskpool } from '@kit.ArkTS' import { compressOne, CompressArg, CompressResult } from '../concurrency/compressTask' export class CompressService { // 一次最多并发 3 个任务,避免资源打满 private readonly maxConcurrent = 3 // 返回进度回调:onProgress(index, total) async compressAll( sources: string[], quality: number, onProgress: (done: number, total: number) => void ): Promise<CompressResult[]> { const results: CompressResult[] = [] let done = 0 // 任务队列:用 Promise 手动控制并发数(简单版信号量) const queue = sources.map((src, index) => { const arg: CompressArg = { src, quality, index } return taskpool.execute(compressOne, arg).then((r) => r as CompressResult) }) // 分批 await,控制同时执行的个数 for (let i = 0; i < queue.length; i += this.maxConcurrent) { const batch = queue.slice(i, i + this.maxConcurrent) const batchResults = await Promise.all(batch) results.push(...batchResults) done += batchResults.length onProgress(done, sources.length) // 进度回传 UI } return results } }

说明:上述"分批 Promise"是最易读的并发限制写法;追求极致吞吐可改用计数信号量(start 一批后立即补充)。真实压缩代码请使用@kit.ImageKit(ImagePacker/ImageSource)完成解码与编码。

3.3 UI 层:进度展示 + 不卡顿

// pages/CompressPage.ets import { CompressService } from '../service/CompressService' @Entry @Component struct CompressPage { private service: CompressService = new CompressService() // 模拟 50 张图片路径 private sources: string[] = Array.from({ length: 50 }, (_, i) => `/data/img_${i + 1}.jpg`) @State progress: number = 0 @State totalText: string = '' @State running: boolean = false private async startCompress() { if (this.running) return this.running = true this.progress = 0 this.totalText = '开始压缩...' try { const results = await this.service.compressAll(this.sources, 60, (done, total) => { // 进度回调:更新状态,UI 自动刷新;主线程空闲,滑动依旧流畅 this.progress = Math.round((done / total) * 100) this.totalText = `已压缩 ${done}/${total} 张` }) this.totalText = `完成!共 ${results.length} 张,本批省下约 ${this.sumBytes(results)} KB` } catch (e) { this.totalText = `压缩失败:${(e as Error).message}` } finally { this.running = false } } private sumBytes(results: { savedBytes: number }[]): number { return Math.round(results.reduce((s, r) => s + r.savedBytes, 0) / 1024) } build() { Column({ space: 24 }) { Text('相册批量压缩').fontSize(24).fontWeight(FontWeight.Bold) // 进度环(复用第 1 篇的自绘组件思路,这里用系统组件) Progress({ value: this.progress, total: 100 }) .width('100%').color('#007DFF') Text(this.totalText).fontSize(14).fontColor('#86909C') Button(this.running ? '压缩中...' : '开始压缩 50 张', { type: ButtonType.Capsule }) .width('100%') .enabled(!this.running) .onClick(() => this.startCompress()) // 说明:压缩期间可以来回拖动这个列表,验证 UI 不卡 List() { ForEach(Array.from({ length: 20 }, (_, i) => `滑动测试项 ${i + 1}`), (item: string) => { ListItem() { Text(item).fontSize(14).padding(10) } }, (item: string) => item) } .layoutWeight(1) .width('100%') } .padding(24) } }

3.4 Worker 对照(常驻任务场景)

TaskPool 之外的场景(长驻后台、独立生命周期)用 Worker:

// workers/workerDemo.ets —— 子线程入口 import { worker, ThreadWorkerGlobalScope } from '@kit.ArkTS' const workerPort = worker.workerPort as ThreadWorkerGlobalScope workerPort.onmessage = (e) => { const { src, quality } = e.data // 执行压缩... const result = { src, quality, ok: true } // 回传结果 workerPort.postMessage(result) } workerPort.onmessageerror = () => { workerPort.close() }
// 主线程使用 import { worker } from '@kit.ArkTS' const wk = new worker.ThreadWorker('entry/ets/workers/workerDemo.ets') wk.onmessage = (e) => { /* 接收子线程结果 */ } wk.postMessage({ src: '/data/a.jpg', quality: 60 }) // 用完释放 // wk.terminate()

选型建议:默认 TaskPool(自动管理、代码简洁);需要长期存活、手动控制生命周期的后台任务才上 Worker。

四、优化与踩坑

问题处理
报"taskpool execute 参数不可序列化"传基本类型/可序列化对象;自定义类实例不能直接传
任务函数报 undefined并发函数必须独立定义(或 @Concurrent),不能引用外部闭包变量
进度回调不刷新回调里更新@State;若频率过高可节流(如每 5 条一次)
并发开太大反而变慢压测找最优并发数(本例 3~5);压缩任务本身也吃 CPU/IO
Worker 消息丢失postMessage 是异步的,业务侧做好回调幂等;异常走 onmessageerror 收尾
真机内存暴涨分批处理 + 及时释放 ImageSource;避免一次性解码全部大图

五、小结与延伸

  • 并发选型:短任务队列 → TaskPool;长驻后台 → Worker。
  • 三要素:@Concurrent 独立任务函数、可序列化参数、进度回传(回调/Promise)。
  • 案例沉淀:CompressService(并发受限的批量任务调度器)+ 进度 UI,可直接套用于文件转换、批量下载、数据迁移等场景。
  • 延伸:任务取消(检查取消标志)、任务优先级、与分布式(第 8 篇)结合做跨端任务下发。

下一篇预告:网络层架构实战——统一请求层设计(拦截器/缓存/重试/取消,新闻客户端案例)。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询