
任务队列 + 条件变量:实现 HarmomyOS 原生 UI 多线程渲染
HarmonyOS 的 NDK 环境下,UI 绘制默认在主线程完成。当需要渲染大量几何图形或执行复杂计算时,主线程被阻塞,帧率直接跳水。很多人尝试用 std::thread 开子线程绘制,却发现子线程根本没有合法的渲染上下文 ------ 调用 OH_NativeXComponent_GetNativeWindow 返回空指针,或者绘制指令被丢弃。
分块渲染是一个比较成熟的思路:将画布切成若干网格,每个网格由一个工作线程独立渲染,最后在主线程合并结果。难点在于如何安全高效地提交任务、回收结果,并保证主线程的绘制不被打断。这篇文章会用一个完整的 NDK 项目,把整个流程串起来,包括线程池、互斥锁、条件变量的正确用法。
分块渲染解决什么问题
场景 :快速显示一张经过大量计算生成的位图(例如分形、图像滤波、模拟噪声)。
单线程方案 :在主线程串行计算所有像素,帧率 = 1 / (计算耗时 + 绘制耗时),当耗时超过 16ms 时画面明显卡顿。
多线程方案:将画布分成 4 块,4 个线程并行计算,主线程只负责最后的合成与显示。理想情况下,计算耗时缩短到 1/4。
限制:
- 每个线程不能直接调用 OH_NativeXComponent 系列 API(必须由主线程发起绘制)。
- 线程同步开销不能超过节省的计算时间,否则反而更慢。
- 必须处理好生命周期:页面退出时工作线程需要安全停止。
环境说明
text
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
cpp
#ifndef TILERENDERER_H
#define TILERENDERER_H
#include <cstdint>
#include <atomic>
#include <condition_variable>
#include <functional>
#include <mutex>
#include <queue>
#include <thread>
#include <vector>
// 一个渲染任务:绘制一块区域,结果写入 pixelData
struct TileTask {
uint32_t startX; // 区域左边界
uint32_t startY; // 区域上边界
uint32_t width; // 区域宽度
uint32_t height; // 区域高度
void* pixelData; // 目标像素(由调用方分配)
std::function<void(void)> fillFunction; // 实际绘制函数
};
class TileRenderer {
public:
TileRenderer(uint32_t tileCountX, uint32_t tileCountY, uint32_t numThreads);
~TileRenderer();
// 提交一组任务,等待所有任务完成(同步阻塞调用)
void SubmitAndWait(std::vector<TileTask> tasks);
// 停止所有工作线程
void Stop();
private:
void WorkerLoop(uint32_t threadIndex);
std::vector<std::thread> workers_;
std::mutex queueMutex_;
std::condition_variable cv_;
std::queue<TileTask> taskQueue_;
std::atomic<int> pendingTasks_{0};
bool stop_ = false;
};
#endif
TileRenderer.cpp
cpp
#include "TileRenderer.h"
#include <cassert>
TileRenderer::TileRenderer(uint32_t tileCountX, uint32_t tileCountY, uint32_t numThreads) {
for (uint32_t i = 0; i < numThreads; ++i) {
workers_.emplace_back(&TileRenderer::WorkerLoop, this, i);
}
}
TileRenderer::~TileRenderer() {
Stop();
}
void TileRenderer::Stop() {
{
std::lock_guard<std::mutex> lock(queueMutex_);
stop_ = true;
}
cv_.notify_all();
for (auto& t : workers_) {
if (t.joinable()) t.join();
}
}
void TileRenderer::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]() { return pendingTasks_.load() == 0; });
}
void TileRenderer::WorkerLoop(uint32_t threadIndex) {
while (true) {
TileTask task;
{
std::unique_lock<std::mutex> lock(queueMutex_);
cv_.wait(lock, [this]() { return stop_ || !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(关键片段)
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>
static TileRenderer* g_renderer = nullptr;
static const int TILE_COUNT_X = 4;
static const int TILE_COUNT_Y = 4;
// XComponent 表面创建时调用
void OnSurfaceCreated(OH_NativeXComponent* component, void* window) {
OHNativeWindow* nativeWindow = reinterpret_cast<OHNativeWindow*>(window);
// 初始化渲染器(4×4 分块,4 个工作线程)
g_renderer = new TileRenderer(TILE_COUNT_X, TILE_COUNT_Y, 4);
// 示例:生成一张纯色分形图(实际项目中替换为真实计算)
uint32_t width = 800; // 应与 XComponent 的一致
uint32_t height = 600;
// 分配像素缓冲区
size_t bufferSize = width * height * 4;
void* pixelData = malloc(bufferSize);
// 构造任务列表:每块区域 fillFunction 负责填充对应像素
uint32_t tileW = width / TILE_COUNT_X;
uint32_t tileH = height / TILE_COUNT_Y;
std::vector<TileTask> tasks;
for (uint32_t ty = 0; ty < TILE_COUNT_Y; ++ty) {
for (uint32_t tx = 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_t y = startY; y < startY + h; ++y) {
uint8_t* row = static_cast<uint8_t*>(pd) + (y * totalW + startX) * 4;
for (uint32_t x = startX; x < startX + w; ++x) {
row[0] = static_cast<uint8_t>((x * 256) / totalW); // R
row[1] = static_cast<uint8_t>((y * 256) / 600); // G
row[2] = 128; // B
row[3] = 255; // A
row += 4;
}
}
};
tasks.push_back(std::move(task));
}
}
// 提交并等待完成
g_renderer->SubmitAndWait(std::move(tasks));
// 主线程执行实际显示(将 pixelData 写入 nativeWindow)
DisplayPixelBuffer(nativeWindow, pixelData, width, height);
free(pixelData);
}
void OnSurfaceDestroyed(OH_NativeXComponent* component) {
if (g_renderer) {
g_renderer->Stop();
delete g_renderer;
g_renderer = nullptr;
}
}
说明:
OnSurfaceCreated中构造所有 TileTask,fillFunction是纯计算,不涉及任何图形 API,因此可以在子线程安全执行。- 计算完成后,主线程(还是在
OnSurfaceCreated的上下文中)调用DisplayPixelBuffer将像素数据写入 NativeWindow。 DisplayPixelBuffer使用OH_NativeWindow_NativeWindowRequestBuffer和OH_NativeWindow_NativeWindowFlushBuffer,属于标准流程,这里不再展开。
步骤 3:ArkTS 侧 -- XComponent 绑定
Index.ets
typescript
import { RenderComponent } from '../utils/RenderComponent';
@Entry
@Component
struct Index {
build() {
Column() {
Text('多线程分块渲染')
.fontSize(20)
.margin(10)
// 自定义组件封装了 XComponent
RenderComponent()
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}
RenderComponent.ets
typescript
@Component
export struct RenderComponent {
private xComponentId: 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 被调用,但此时工作线程还在处理旧任务,访问了已经释放的 pixelData 或 TileRenderer 自身。
解法 :Stop() 必须等待所有线程退出后再释放资源。上面的 ~TileRenderer 调用了 Stop(),且 Stop() 内部先设置 stop_=true 并 notify_all,然后 join 所有线程。注意 SubmitAndWait 中 pendingTasks_ 的等待不能漏掉,否则线程可能卡在 wait 上无法退出。
最佳实践
-
任务队列使用
std::deque而不是std::queue为了支持优先级或更灵活的调度(例如未来需要推送到队首),但当前队列只需 FIFO,
std::queue足够。如果计算量不均衡,可以改用 thread-local 工作窃取,但初学者先从简单队列开始。 -
只使用原子变量管理"任务完成数",避免频繁加锁
pendingTasks_用std::atomic<int>,每个工作线程完成后--pendingTasks_并检查是否为零,零时才通知主线程。主线程使用cv_.wait而不是忙等,CPU 负担低。 -
始终在主线程完成
OH_NativeXComponent相关操作包括获取窗口、申请缓冲、刷新缓冲等。子线程越界访问窗口句柄会导致不可预知的崩溃,而且不同设备上的表现可能不一样。
FAQ
Q:为什么我按此实现后,分块渲染反而比单线程慢?
A:检查每个 Tile 的计算量是否足够大。线程创建、同步、条件变量唤醒都有开销。如果单块计算时间小于 0.1ms,多线程的效益会被抵消。建议将分块数减少(例如 2×2 或 3×3),或者合并小任务。
Q:Condition_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 渲染的性能瓶颈,可以试试这套分块+线程池的架构。核心原则是:计算放线程,绘制留主线程。处理好线程生命周期和锁的范围,多线程渲染就能稳定提速。