《HarmonyOS技术精讲-UI开发 (基于NDK构建UI)》第5篇:多线程渲染与线程安全同步

发布时间:2026/6/25 13:45:16

《HarmonyOS技术精讲-UI开发 (基于NDK构建UI)》第5篇:多线程渲染与线程安全同步 任务队列 条件变量实现 HarmomyOS 原生 UI 多线程渲染HarmonyOS 的 NDK 环境下UI 绘制默认在主线程完成。当需要渲染大量几何图形或执行复杂计算时主线程被阻塞帧率直接跳水。很多人尝试用std::thread开子线程绘制却发现子线程根本没有合法的渲染上下文 —— 调用OH_NativeXComponent_GetNativeWindow返回空指针或者绘制指令被丢弃。分块渲染是一个比较成熟的思路将画布切成若干网格每个网格由一个工作线程独立渲染最后在主线程合并结果。难点在于如何安全高效地提交任务、回收结果并保证主线程的绘制不被打断。这篇文章会用一个完整的 NDK 项目把整个流程串起来包括线程池、互斥锁、条件变量的正确用法。分块渲染解决什么问题场景快速显示一张经过大量计算生成的位图例如分形、图像滤波、模拟噪声。单线程方案在主线程串行计算所有像素帧率 1 / (计算耗时 绘制耗时)当耗时超过 16ms 时画面明显卡顿。多线程方案将画布分成 4 块4 个线程并行计算主线程只负责最后的合成与显示。理想情况下计算耗时缩短到 1/4。限制每个线程不能直接调用 OH_NativeXComponent 系列 API必须由主线程发起绘制。线程同步开销不能超过节省的计算时间否则反而更慢。必须处理好生命周期页面退出时工作线程需要安全停止。环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机 / 平板ARM64核心实现项目结构entry/src/main/ ├── cpp/ │ ├── CMakeLists.txt │ ├── include/ │ │ └── TileRenderer.h // 分块渲染器定义 │ └── src/ │ ├── TileRenderer.cpp // 分块渲染器实现 │ └── native_bridge.cpp // NAPI 注册 XComponent 回调 ├── ets/ │ ├── entryability/ │ │ └── EntryAbility.ets │ ├── pages/ │ │ └── Index.ets // 主页面包含 XComponent │ └── utils/ │ └── RenderComponent.ets // 封装 XComponent 的组件步骤 1NDK 侧 – 线程池与任务队列TileRenderer.h#ifndefTILERENDERER_H#defineTILERENDERER_H#includecstdint#includeatomic#includecondition_variable#includefunctional#includemutex#includequeue#includethread#includevector// 一个渲染任务绘制一块区域结果写入 pixelDatastructTileTask{uint32_tstartX;// 区域左边界uint32_tstartY;// 区域上边界uint32_twidth;// 区域宽度uint32_theight;// 区域高度void*pixelData;// 目标像素由调用方分配std::functionvoid(void)fillFunction;// 实际绘制函数};classTileRenderer{public:TileRenderer(uint32_ttileCountX,uint32_ttileCountY,uint32_tnumThreads);~TileRenderer();// 提交一组任务等待所有任务完成同步阻塞调用voidSubmitAndWait(std::vectorTileTasktasks);// 停止所有工作线程voidStop();private:voidWorkerLoop(uint32_tthreadIndex);std::vectorstd::threadworkers_;std::mutex queueMutex_;std::condition_variable cv_;std::queueTileTasktaskQueue_;std::atomicintpendingTasks_{0};boolstop_false;};#endifTileRenderer.cpp#includeTileRenderer.h#includecassertTileRenderer::TileRenderer(uint32_ttileCountX,uint32_ttileCountY,uint32_tnumThreads){for(uint32_ti0;inumThreads;i){workers_.emplace_back(TileRenderer::WorkerLoop,this,i);}}TileRenderer::~TileRenderer(){Stop();}voidTileRenderer::Stop(){{std::lock_guardstd::mutexlock(queueMutex_);stop_true;}cv_.notify_all();for(autot:workers_){if(t.joinable())t.join();}}voidTileRenderer::SubmitAndWait(std::vectorTileTasktasks){{std::lock_guardstd::mutexlock(queueMutex_);pendingTasks_.store(static_castint(tasks.size()));for(autotask:tasks){taskQueue_.push(std::move(task));}}cv_.notify_all();// 等待所有任务完成忙等待休眠简单场景可用std::unique_lockstd::mutexlock(queueMutex_);cv_.wait(lock,[this](){returnpendingTasks_.load()0;});}voidTileRenderer::WorkerLoop(uint32_tthreadIndex){while(true){TileTask task;{std::unique_lockstd::mutexlock(queueMutex_);cv_.wait(lock,[this](){returnstop_||!taskQueue_.empty();});if(stop_)return;taskstd::move(taskQueue_.front());taskQueue_.pop();}// 执行具体绘制if(task.fillFunction){task.fillFunction();}// 减少待办计数通知主线程if(--pendingTasks_0){cv_.notify_one();}}}说明SubmitAndWait是同步接口主线程调用后阻塞直到所有子线程完成。工作线程通过condition_variable等待新任务避免忙等。pendingTasks_用原子变量记录减少锁粒度。页面退出时调用Stop()安全终止所有线程。步骤 2NDK 侧 – XComponent 回调与绘制native_bridge.cpp关键片段#includecinttypes#includeTileRenderer.h#includenative_window/external_window.h#includenative_buffer/native_buffer.h#includenative_window/oh_buffer_context.hstaticTileRenderer*g_renderernullptr;staticconstintTILE_COUNT_X4;staticconstintTILE_COUNT_Y4;// XComponent 表面创建时调用voidOnSurfaceCreated(OH_NativeXComponent*component,void*window){OHNativeWindow*nativeWindowreinterpret_castOHNativeWindow*(window);// 初始化渲染器4×4 分块4 个工作线程g_renderernewTileRenderer(TILE_COUNT_X,TILE_COUNT_Y,4);// 示例生成一张纯色分形图实际项目中替换为真实计算uint32_twidth800;// 应与 XComponent 的一致uint32_theight600;// 分配像素缓冲区size_t bufferSizewidth*height*4;void*pixelDatamalloc(bufferSize);// 构造任务列表每块区域 fillFunction 负责填充对应像素uint32_ttileWwidth/TILE_COUNT_X;uint32_ttileHheight/TILE_COUNT_Y;std::vectorTileTasktasks;for(uint32_tty0;tyTILE_COUNT_Y;ty){for(uint32_ttx0;txTILE_COUNT_X;tx){TileTask task;task.startXtx*tileW;task.startYty*tileH;task.widthtileW;task.heighttileH;task.pixelDatapixelData;task.fillFunction[startX,startY,wtileW,htileH,pdpixelData,totalWwidth](){// 模拟耗时计算写入不同颜色块for(uint32_tystartY;ystartYh;y){uint8_t*rowstatic_castuint8_t*(pd)(y*totalWstartX)*4;for(uint32_txstartX;xstartXw;x){row[0]static_castuint8_t((x*256)/totalW);// Rrow[1]static_castuint8_t((y*256)/600);// Grow[2]128;// Brow[3]255;// Arow4;}}};tasks.push_back(std::move(task));}}// 提交并等待完成g_renderer-SubmitAndWait(std::move(tasks));// 主线程执行实际显示将 pixelData 写入 nativeWindowDisplayPixelBuffer(nativeWindow,pixelData,width,height);free(pixelData);}voidOnSurfaceDestroyed(OH_NativeXComponent*component){if(g_renderer){g_renderer-Stop();deleteg_renderer;g_renderernullptr;}}说明OnSurfaceCreated中构造所有 TileTaskfillFunction是纯计算不涉及任何图形 API因此可以在子线程安全执行。计算完成后主线程还是在OnSurfaceCreated的上下文中调用DisplayPixelBuffer将像素数据写入 NativeWindow。DisplayPixelBuffer使用OH_NativeWindow_NativeWindowRequestBuffer和OH_NativeWindow_NativeWindowFlushBuffer属于标准流程这里不再展开。步骤 3ArkTS 侧 – XComponent 绑定Index.etsimport{RenderComponent}from../utils/RenderComponent;EntryComponentstruct Index{build(){Column(){Text(多线程分块渲染).fontSize(20).margin(10)// 自定义组件封装了 XComponentRenderComponent()}.width(100%).height(100%).backgroundColor(#F5F5F5)}}RenderComponent.etsComponentexportstruct RenderComponent{privatexComponentId:stringtile_render;build(){XComponent({id:this.xComponentId,type:surface,libraryname:render_engine// 对应 CMakeLists 中的 lib}).onLoad((){console.log(XComponent loaded);}).width(800).height(600).margin(20)}}常见踩坑与解决方案坑 1子线程中调用 OH_NativeXComponent 函数崩溃现象在fillFunction中尝试调用OH_NativeXComponent_GetNativeWindow获取窗口句柄然后直接绘制结果闪退或报错“EGL_BAD_NATIVE_WINDOW”。原因OH_NativeXComponent 的回调全部在主线程ArkUI 渲染线程分发子线程没有有效的 EGL/GLES 上下文无法直接操作 surface。解法将绘制操作与计算分离。子线程只负责填充像素缓冲区纯内存操作主线程统一将缓冲区提交到窗口。上面代码的fillFunction只写内存DisplayPixelBuffer在主线程执行完美避开限制。坑 2页面切换时工作线程未停止导致野指针现象快速返回上一页再回来有时 App 崩溃崩溃栈指向TileRenderer::WorkerLoop。原因页面销毁时OnSurfaceDestroyed被调用但此时工作线程还在处理旧任务访问了已经释放的pixelData或TileRenderer自身。解法Stop()必须等待所有线程退出后再释放资源。上面的~TileRenderer调用了Stop()且Stop()内部先设置stop_true并notify_all然后join所有线程。注意SubmitAndWait中pendingTasks_的等待不能漏掉否则线程可能卡在wait上无法退出。最佳实践任务队列使用std::deque而不是std::queue为了支持优先级或更灵活的调度例如未来需要推送到队首但当前队列只需 FIFOstd::queue足够。如果计算量不均衡可以改用 thread-local 工作窃取但初学者先从简单队列开始。只使用原子变量管理“任务完成数”避免频繁加锁pendingTasks_用std::atomicint每个工作线程完成后--pendingTasks_并检查是否为零零时才通知主线程。主线程使用cv_.wait而不是忙等CPU 负担低。始终在主线程完成OH_NativeXComponent相关操作包括获取窗口、申请缓冲、刷新缓冲等。子线程越界访问窗口句柄会导致不可预知的崩溃而且不同设备上的表现可能不一样。FAQQ为什么我按此实现后分块渲染反而比单线程慢A检查每个 Tile 的计算量是否足够大。线程创建、同步、条件变量唤醒都有开销。如果单块计算时间小于 0.1ms多线程的效益会被抵消。建议将分块数减少例如 2×2 或 3×3或者合并小任务。QCondition_variable 的wait为什么会引起死锁A常见的死锁原因是notify在锁释放之后发出而wait因为丢失了信号永远睡下去。确保notify_all或notify_one在锁的作用域外调用当前代码在queueMutex_作用域外通知或者使用std::notify_all_at_thread_exit。Q页面跳动时出现渲染残留上一帧图像留了一部分A问题通常出在DisplayPixelBuffer没有清空整个 surface。每次绘制前使用OH_NativeWindow_NativeWindowSetBufferGeometry配合OH_NativeWindow_NativeWindowFlushBuffer覆盖全区域或者在任务开始前用memset清空像素缓冲区。如果你也在 HarmonyOS NDK 开发中遇到 UI 渲染的性能瓶颈可以试试这套分块线程池的架构。核心原则是计算放线程绘制留主线程。处理好线程生命周期和锁的范围多线程渲染就能稳定提速。

相关新闻