《HarmonyOS技术精讲-UI开发 (基于NDK构建UI)》第5篇:多线程渲染与线程安全同步
2026/6/25 13:45:09 网站建设 项目流程

任务队列 + 条件变量:实现 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 的组件

步骤 1:NDK 侧 – 线程池与任务队列

TileRenderer.h

#ifndefTILERENDERER_H#defineTILERENDERER_H#include<cstdint>#include<atomic>#include<condition_variable>#include<functional>#include<mutex>#include<queue>#include<thread>#include<vector>// 一个渲染任务:绘制一块区域,结果写入 pixelDatastructTileTask{uint32_tstartX;// 区域左边界uint32_tstartY;// 区域上边界uint32_twidth;// 区域宽度uint32_theight;// 区域高度void*pixelData;// 目标像素(由调用方分配)std::function<void(void)>fillFunction;// 实际绘制函数};classTileRenderer{public:TileRenderer(uint32_ttileCountX,uint32_ttileCountY,uint32_tnumThreads);~TileRenderer();// 提交一组任务,等待所有任务完成(同步阻塞调用)voidSubmitAndWait(std::vector<TileTask>tasks);// 停止所有工作线程voidStop();private:voidWorkerLoop(uint32_tthreadIndex);std::vector<std::thread>workers_;std::mutex queueMutex_;std::condition_variable cv_;std::queue<TileTask>taskQueue_;std::atomic<int>pendingTasks_{0};boolstop_=false;};#endif

TileRenderer.cpp

#include"TileRenderer.h"#include<cassert>TileRenderer::TileRenderer(uint32_ttileCountX,uint32_ttileCountY,uint32_tnumThreads){for(uint32_ti=0;i<numThreads;++i){workers_.emplace_back(&TileRenderer::WorkerLoop,this,i);}}TileRenderer::~TileRenderer(){Stop();}voidTileRenderer::Stop(){{std::lock_guard<std::mutex>lock(queueMutex_);stop_=true;}cv_.notify_all();for(auto&t:workers_){if(t.joinable())t.join();}}voidTileRenderer::SubmitAndWait(std::vector<TileTask>tasks){{std::lock_guard<std::mutex>lock(queueMutex_);pendingTasks_.store(static_cast<int>(tasks.size()));for(auto&task:tasks){taskQueue_.push(std::move(task));}}cv_.notify_all();// 等待所有任务完成(忙等待+休眠,简单场景可用)std::unique_lock<std::mutex>lock(queueMutex_);cv_.wait(lock,[this](){returnpendingTasks_.load()==0;});}voidTileRenderer::WorkerLoop(uint32_tthreadIndex){while(true){TileTask task;{std::unique_lock<std::mutex>lock(queueMutex_);cv_.wait(lock,[this](){returnstop_||!taskQueue_.empty();});if(stop_)return;task=std::move(taskQueue_.front());taskQueue_.pop();}// 执行具体绘制if(task.fillFunction){task.fillFunction();}// 减少待办计数,通知主线程if(--pendingTasks_==0){cv_.notify_one();}}}

说明

  • SubmitAndWait是同步接口,主线程调用后阻塞直到所有子线程完成。
  • 工作线程通过condition_variable等待新任务,避免忙等。
  • pendingTasks_用原子变量记录,减少锁粒度。
  • 页面退出时调用Stop()安全终止所有线程。

步骤 2:NDK 侧 – XComponent 回调与绘制

native_bridge.cpp(关键片段)

#include<cinttypes>#include"TileRenderer.h"#include<native_window/external_window.h>#include<native_buffer/native_buffer.h>#include<native_window/oh_buffer_context.h>staticTileRenderer*g_renderer=nullptr;staticconstintTILE_COUNT_X=4;staticconstintTILE_COUNT_Y=4;// XComponent 表面创建时调用voidOnSurfaceCreated(OH_NativeXComponent*component,void*window){OHNativeWindow*nativeWindow=reinterpret_cast<OHNativeWindow*>(window);// 初始化渲染器(4×4 分块,4 个工作线程)g_renderer=newTileRenderer(TILE_COUNT_X,TILE_COUNT_Y,4);// 示例:生成一张纯色分形图(实际项目中替换为真实计算)uint32_twidth=800;// 应与 XComponent 的一致uint32_theight=600;// 分配像素缓冲区size_t bufferSize=width*height*4;void*pixelData=malloc(bufferSize);// 构造任务列表:每块区域 fillFunction 负责填充对应像素uint32_ttileW=width/TILE_COUNT_X;uint32_ttileH=height/TILE_COUNT_Y;std::vector<TileTask>tasks;for(uint32_tty=0;ty<TILE_COUNT_Y;++ty){for(uint32_ttx=0;tx<TILE_COUNT_X;++tx){TileTask task;task.startX=tx*tileW;task.startY=ty*tileH;task.width=tileW;task.height=tileH;task.pixelData=pixelData;task.fillFunction=[startX,startY,w=tileW,h=tileH,pd=pixelData,totalW=width](){// 模拟耗时计算:写入不同颜色块for(uint32_ty=startY;y<startY+h;++y){uint8_t*row=static_cast<uint8_t*>(pd)+(y*totalW+startX)*4;for(uint32_tx=startX;x<startX+w;++x){row[0]=static_cast<uint8_t>((x*256)/totalW);// Rrow[1]=static_cast<uint8_t>((y*256)/600);// Grow[2]=128;// Brow[3]=255;// Arow+=4;}}};tasks.push_back(std::move(task));}}// 提交并等待完成g_renderer->SubmitAndWait(std::move(tasks));// 主线程执行实际显示(将 pixelData 写入 nativeWindow)DisplayPixelBuffer(nativeWindow,pixelData,width,height);free(pixelData);}voidOnSurfaceDestroyed(OH_NativeXComponent*component){if(g_renderer){g_renderer->Stop();deleteg_renderer;g_renderer=nullptr;}}

说明

  • OnSurfaceCreated中构造所有 TileTask,fillFunction是纯计算,不涉及任何图形 API,因此可以在子线程安全执行。
  • 计算完成后,主线程(还是在OnSurfaceCreated的上下文中)调用DisplayPixelBuffer将像素数据写入 NativeWindow。
  • DisplayPixelBuffer使用OH_NativeWindow_NativeWindowRequestBufferOH_NativeWindow_NativeWindowFlushBuffer,属于标准流程,这里不再展开。

步骤 3:ArkTS 侧 – XComponent 绑定

Index.ets

import{RenderComponent}from'../utils/RenderComponent';@Entry@Componentstruct Index{build(){Column(){Text('多线程分块渲染').fontSize(20).margin(10)// 自定义组件封装了 XComponentRenderComponent()}.width('100%').height('100%').backgroundColor('#F5F5F5')}}

RenderComponent.ets

@Componentexportstruct RenderComponent{privatexComponentId:string='tile_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被调用,但此时工作线程还在处理旧任务,访问了已经释放的pixelDataTileRenderer自身。

解法Stop()必须等待所有线程退出后再释放资源。上面的~TileRenderer调用了Stop(),且Stop()内部先设置stop_=truenotify_all,然后join所有线程。注意SubmitAndWaitpendingTasks_的等待不能漏掉,否则线程可能卡在wait上无法退出。


最佳实践

  1. 任务队列使用std::deque而不是std::queue
    为了支持优先级或更灵活的调度(例如未来需要推送到队首),但当前队列只需 FIFO,std::queue足够。如果计算量不均衡,可以改用 thread-local 工作窃取,但初学者先从简单队列开始。

  2. 只使用原子变量管理“任务完成数”,避免频繁加锁
    pendingTasks_std::atomic<int>,每个工作线程完成后--pendingTasks_并检查是否为零,零时才通知主线程。主线程使用cv_.wait而不是忙等,CPU 负担低。

  3. 始终在主线程完成OH_NativeXComponent相关操作
    包括获取窗口、申请缓冲、刷新缓冲等。子线程越界访问窗口句柄会导致不可预知的崩溃,而且不同设备上的表现可能不一样。


FAQ

Q:为什么我按此实现后,分块渲染反而比单线程慢?
A:检查每个 Tile 的计算量是否足够大。线程创建、同步、条件变量唤醒都有开销。如果单块计算时间小于 0.1ms,多线程的效益会被抵消。建议将分块数减少(例如 2×2 或 3×3),或者合并小任务。

Q:Condition_variable 的wait为什么会引起死锁?
A:常见的死锁原因是notify在锁释放之后发出,而wait因为丢失了信号永远睡下去。确保notify_allnotify_one在锁的作用域外调用(当前代码在queueMutex_作用域外通知),或者使用std::notify_all_at_thread_exit

Q:页面跳动时出现渲染残留(上一帧图像留了一部分)?
A:问题通常出在DisplayPixelBuffer没有清空整个 surface。每次绘制前使用OH_NativeWindow_NativeWindowSetBufferGeometry配合OH_NativeWindow_NativeWindowFlushBuffer覆盖全区域,或者在任务开始前用memset清空像素缓冲区。


如果你也在 HarmonyOS NDK 开发中遇到 UI 渲染的性能瓶颈,可以试试这套分块+线程池的架构。核心原则是:计算放线程,绘制留主线程。处理好线程生命周期和锁的范围,多线程渲染就能稳定提速。

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

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

立即咨询