《HarmonyOS技术精讲-ArkWeb》并行能力:Web Worker与多线程
《HarmonyOS技术精讲-ArkWeb》并行能力Web Worker与多线程页面卡顿是前端开发的老问题HarmonyOS NEXT 开发里Web 页面加载大图片并做实时处理时UI 卡顿非常明显。很多人第一次尝试用 JavaScript 直接处理像素数据会发现界面滑动都变得不流畅。原因是图像处理这种计算密集型任务默认跑在主线程上而 ArkWeb 渲染和用户交互也需要主线程。主线程一旦被占住页面立马卡死。如果把 500 万像素的图片灰度处理放在主线程里跑用户能直观感受到 2-3 秒的冻结。Web Worker 解决的就是这个问题——把计算任务放到后台线程主线程只负责 UI 渲染和交互。这个方案在浏览器端已经成熟在 HarmonyOS 的 ArkWeb 组件里也完整支持。它解决什么问题Web Worker 本质是给 Web 页面提供多线程能力。在 ArkWeb 里它跟浏览器环境的 Worker 行为基本一致维度主线程Web Worker 线程职责UI 渲染、用户交互计算密集型任务能否操作 DOM可以不可以能否访问 window 对象可以不可以能否发网络请求可以可以通过 fetch 发请求生命周期随页面生存手动管理适合用 Worker 的场景图像处理、数据压缩、加密解密、大数组排序、JSON 解析。不适合的场景需要操作 DOM、需要频繁更新页面状态、短时间小计算创建 Worker 本身有开销。跟 Service Worker 的区别Service Worker 是网络代理拦截请求做缓存离线Web Worker 纯粹用来做后台计算两者不冲突。环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机核心实现图像灰度处理的完整流程目标在一个 ArkWeb 页面里点击按钮后把图片转成灰度图灰度处理在 Worker 线程里完成。步骤 1创建 Web Worker 脚本在项目entry/src/main/resources/rawfile目录下创建worker.js// 监听主线程发来的消息self.onmessagefunction(e){constimageDatae.data.imageData;// 灰度处理取 RGB 平均值constdataimageData.data;for(leti0;idata.length;i4){constrdata[i];constgdata[i1];constbdata[i2];constgray(rgb)/3;data[i]gray;// Rdata[i1]gray;// Gdata[i2]gray;// B// data[i 3] alpha 保持不变}// 把处理结果发回主线程self.postMessage({imageData:imageData});};// 错误处理self.onerrorfunction(e){self.postMessage({error:e.message});};这段代码接收主线程传来的图像像素数据遍历每个像素的 RGB 通道取平均值再赋值回去。注意onerror这个回调Worker 内部出异常时如果没处理主线程完全感知不到所以必须加上错误上报。步骤 2前端 HTML 页面在entry/src/main/resources/rawfile目录下创建index.html!DOCTYPEhtmlhtmlheadmetacharsetutf-8titleWeb Worker 图像处理/title/headbodyimgidsourceImagesrctest.jpgwidth300height200alt原始图片canvasidcanvasstyledisplay:none;/canvasbuttonidprocessBtn灰度处理/buttonimgidresultImagewidth300height200alt处理后结果scriptletworkernull;document.getElementById(processBtn).addEventListener(click,function(){constimgdocument.getElementById(sourceImage);constcanvasdocument.getElementById(canvas);constctxcanvas.getContext(2d);// 设置 canvas 尺寸与图片一致canvas.widthimg.naturalWidth;canvas.heightimg.naturalHeight;// 把图片绘制到 canvas 上ctx.drawImage(img,0,0);constimageDatactx.getImageData(0,0,canvas.width,canvas.height);// 创建 Workerif(worker){worker.terminate();workernull;}workernewWorker(worker.js);// 发送数据到 Workerworker.postMessage({imageData:imageData});// 接收结果worker.onmessagefunction(e){if(e.data.error){console.error(Worker error:,e.data.error);return;}// 把处理后的数据放回 canvasctx.putImageData(e.data.imageData,0,0);// 给 resultImage 显示document.getElementById(resultImage).srccanvas.toDataURL(image/jpeg);// 终止 Worker释放资源worker.terminate();workernull;};worker.onerrorfunction(e){console.error(Worker error event:,e.message);worker.terminate();workernull;};});/script/body/htmlHTML 页面逻辑点击按钮后先把图片绘制到 canvas 上获取像素数据ImageData然后创建 Worker 把数据扔过去。Worker 处理完返回后再把结果放进另一个 img 标签展示。这里有个容易忽略的点每次点击按钮都重新创建 Worker用完就终止。不这么做的话多次点击会累积多个 Worker 实例内存占用越来越高。步骤 3ArkTS 页面加载 ArkWeb 并触发处理// pages/WebWorkerDemo.etsEntryComponentstruct WebWorkerDemo{Statemessage:string图片灰度处理示例privatecontroller:web_webview.WebviewControllernewweb_webview.WebviewController()build(){Column(){// 显示状态信息Text(this.message).fontSize(16).fontWeight(FontWeight.Bold).margin({bottom:10})// ArkWeb 组件Web({src:$rawfile(index.html),controller:this.controller}).width(100%).height(90%).javaScriptAccess(true).fileAccess(true).onPageBegin((event){// 页面开始加载时可以做一些初始化console.info(WebWorkerDemo onPageBegin)}).onErrorReceive((event){console.error(WebWorkerDemo onError:,event.errorMsg)})}.width(100%).height(100%).padding(10)}}ArkTS 页面很简单就是加载一个包含 Worker 逻辑的 HTML 页面。注意必须开启javaScriptAccess(true)否则 Worker 创建不成功。实际项目里图片可能来自相册选择或者网络下载这时候需要通过 ArkWeb 的 JSBridge 把图片数据传进去。下面补充一个通过 JSBridge 触发 Worker 处理的完整示例。步骤 4JSBridge 方式触发 Worker在 ArkTS 侧通过controller.runJavaScript调用 HTML 中暴露的函数// 在 ArkTS 中调用asyncfunctiontriggerImageProcessing():Promisevoid{// 假设已经在 index.html 中定义了一个全局函数// function processImageFromNative(imageUrl: string) { ... }constresultawaitthis.controller.runJavaScript(processImageFromNative(resource://rawfile/test.jpg))console.info(JSBridge call result:,result)}HTML 中对应的函数// 在 index.html 中window.processImageFromNativefunction(imageUrl){constimgnewImage();img.onloadfunction(){constcanvasdocument.createElement(canvas);constctxcanvas.getContext(2d);canvas.widthimg.naturalWidth;canvas.heightimg.naturalHeight;ctx.drawImage(img,0,0);constimageDatactx.getImageData(0,0,canvas.width,canvas.height);// 创建 Worker 处理constworkernewWorker(worker.js);worker.postMessage({imageData:imageData});worker.onmessagefunction(e){if(e.data.error){console.error(Worker error:,e.data.error);return;}ctx.putImageData(e.data.imageData,0,0);constresultDataUrlcanvas.toDataURL(image/jpeg);// 把结果传回 ArkTS// 这里可以用 JSBridge 把数据传回去或者直接更新 DOMconstresultImgdocument.getElementById(resultImage);resultImg.srcresultDataUrl;worker.terminate();};};img.srcimageUrl;};ArkWeb 的runJavaScript返回的是一个 Promise可以拿到 HTML 中函数的返回值。但 HTML 函数里如果有异步回调返回值可能不是最终结果这时需要把结果通过某种全局回调机制传回来。简单场景下直接在 HTML 内更新 DOM 就够了不一定非要传给 ArkTS。常见问题 1Worker 脚本加载失败现象worker.js放在rawfile目录下在 HTML 中引用new Worker(worker.js)时抛异常Failed to construct Worker。原因ArkWeb 默认不允许从$rawfile路径直接加载 Worker 脚本。Worker 脚本必须从网络服务器加载或者通过web_webview.WebviewController的loadData方式内联。解法把worker.js放到云端服务器HTML 中引用完整 URL。或者把 Worker 代码内联到 HTML 中// index.html 中直接包含 Worker 代码constworkerCodeself.onmessage function(e) { const data e.data.imageData.data; for (let i 0; i data.length; i 4) { const gray (data[i] data[i1] data[i2]) / 3; data[i] gray; data[i1] gray; data[i2] gray; } self.postMessage({imageData: e.data.imageData}); };;constblobnewBlob([workerCode],{type:application/javascript});constworkerUrlURL.createObjectURL(blob);constworkernewWorker(workerUrl);在本地调试时推荐用第二种方式避免网络环境依赖。常见问题 2Worker 内存泄漏现象频繁点击灰度按钮内存持续增长页面最终卡死。原因每次创建 Worker 时旧 Worker 没有被正确终止。点击按钮后立即创建新 Worker旧 Worker 可能还在处理中的数据占用内存。解法letcurrentWorkernull;functionstartProcess(){// 终止旧的 Workerif(currentWorker){currentWorker.terminate();currentWorkernull;}// 创建新 WorkercurrentWorkernewWorker(workerUrl);currentWorker.postMessage({...});currentWorker.onmessagefunction(e){// 处理结果currentWorker.terminate();currentWorkernull;};}在页面卸载时也必须做清理不过这属于 HTML 页面的生命周期管理了。常见问题 3postMessage 传递大对象性能问题现象处理 2000x2000 图片时postMessage这一步耗时非常长。原因默认情况下postMessage传递的数据会被序列化和反序列化结构化克隆。大的 ImageData 对象包含几十 MB 的像素数据要拷贝一遍耗时明显。解法使用可转移对象Transferable来避免拷贝// 发送方worker.postMessage({imageData:imageData},[imageData.data.buffer]// 转移所有权);这样数据不会被拷贝直接从主线程转移到 Worker然后映射到 Worker 的 ArrayBuffer 上。注意转移后主线程不能再访问这个数据否则会报错。在 Worker 处理完返回时同样可以用转移方式传回来。最佳实践不要在 Worker 创建后频繁发消息。每次postMessage都有序列化开销多个小消息比一个大消息的开销大得多。尽量把数据攒一批再发。Worker 尽量做纯计算不要依赖全局变量。Worker 线程没有访问 DOM、window、document 的能力如果需要这些信息必须通过postMessage从主线程传进来。错误处理必须双保险。Worker 内部的onerror和try/catch都要加否则 Worker 内抛异常时主线程完全不知道。另外主线程监听worker.onerror时事件对象的 message 属性在不同实现里可能不一样建议统一用postMessage把错误信息传出来。FAQQ真机上 Worker 创建成功模拟器上报Script error.怎么办A模拟器对 Blob URL 的支持不完全URL.createObjectURL创建的内联 Worker 脚本可能在模拟器上无法正常工作。建议把 Worker 脚本放到远程服务器上用完整 URL 加载。Q页面返回后 Worker 还在运行吗A页面返回时 ArkWeb 会销毁Worker 随之结束。但在单页应用SPA里路由切换时Worker 会继续运行必须手动terminate否则会在后台空转。QWorker 里能使用 HarmonyOS 的 JSBridge 吗A不能。Worker 线程没有window对象无法触发 ArkWeb 的 JSBridge 注册方法。所有与 HarmonyOS 原生侧的通信都必须通过主线程转发。Q多个 Worker 并行处理同一张大图能加速吗A可以但要小心内存。每个 Worker 会持有数据的一份拷贝或者共享一个 ArrayBuffer如果数据太大并行 Worker 数不宜过多。2-4 个 Worker 通常是最优的。示例代码地址项目地址