《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(必须由主线程发起绘制)。
  • 线程同步开销不能超过节省的计算时间,否则反而更慢。
  • 必须处理好生命周期:页面退出时工作线程需要安全停止。

环境说明

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_NativeWindowRequestBufferOH_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 被调用,但此时工作线程还在处理旧任务,访问了已经释放的 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 渲染的性能瓶颈,可以试试这套分块+线程池的架构。核心原则是:计算放线程,绘制留主线程。处理好线程生命周期和锁的范围,多线程渲染就能稳定提速。