《HarmonyOS技术精讲-UI开发 (基于NDK构建UI)》第1篇:初探NDK与ArkUI集成

为什么需要NDK构建UI?

HarmonyOS NEXT的ArkUI框架功能已经很完善了,为什么还要用NDK来构建UI?这个问题我自己也困惑过一段时间。

简单来说,ArkTS在UI描述和业务逻辑上非常高效,但涉及到高性能计算、复杂图形渲染、或者需要复用现有C/C++库的场景时,JS/Native的边界就成了性能瓶颈。

举个例子:一个实时视频滤镜应用,如果每一帧的像素处理都通过ArkTS调用,即使使用N-API,频繁的跨语言调用开销也会让帧率直接腰斩。再比如音视频编解码、3D渲染引擎、游戏引擎内部的UI逻辑,这些场景天然就在C++侧运行。

NDK构建UI的定位不是替代ArkUI,而是在需要高性能或复用C/C++能力的特定UI组件上,提供一个Native渲染的入口。

适用场景:

  • 高性能图形渲染UI(如游戏HUD、实时特效控制面板)
  • 复用现有C/C++图形库的UI组件
  • 需要精细控制渲染管线的自定义组件

不适合场景:

  • 简单列表、表单、布局切换(ArkUI足矣)
  • 需要频繁动态修改的UI(ArkUI的响应式机制更成熟)

环境说明

在开始之前,确保你的开发环境已准备就绪:

text 复制代码
DevEco Studio 版本:DevEco Studio 6.1.0 及以上
HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上
目标设备:手机(API 23+)

核心实现:从零搭建NDK UI组件

整个流程分为三步:创建NDK工程 -> 编写C++组件 -> 注册并调用。

第一步:创建支持NDK的工程

在DevEco Studio中创建新项目,选择**Native C++**模板。这个模板会帮你初始化CMakeLists.txt和cpp目录。

创建完成后,项目结构如下:

复制代码
MyNativeApp/
├── entry/
│   ├── src/
│   │   ├── main/
│   │   │   ├── cpp/
│   │   │   │   ├── CMakeLists.txt
│   │   │   │   ├── napi_init.cpp
│   │   │   │   └── (你的C++组件文件)
│   │   │   ├── ets/
│   │   │   │   └── (ArkTS页面)
│   │   │   └── resources/
│   │   └── module.json5
│   └── build-profile.json5

第二步:编写C++ UI组件

我们先创建一个非常简单的Native UI组件------一个自定义绘制的圆形按钮。

文件:entry/src/main/cpp/MyNativeButton.h

cpp 复制代码
#ifndef MY_NATIVE_BUTTON_H
#define MY_NATIVE_BUTTON_H

#include <ace/xcomponent/native_interface_xcomponent.h>
#include <native_window/external_window.h>
#include <EGL/egl.h>
#include <EGL/eglext.h>
#include <GLES3/gl3.h>
#include <string>

class MyNativeButton {
public:
    MyNativeButton() = default;
    ~MyNativeButton();

    // 初始化EGL和OpenGL上下文
    bool Initialize(OH_NativeXComponent* component, int width, int height);
    // 执行渲染
    void Render();
    // 销毁资源
    void Destroy();

    // 处理触摸事件
    void OnTouchEvent(OH_NativeXComponent* component, TouchEventType type, float x, float y);

private:
    void CreateEGLContext();
    void DestroyEGLContext();

    OH_NativeXComponent* nativeComponent_ = nullptr;
    EGLDisplay eglDisplay_ = EGL_NO_DISPLAY;
    EGLContext eglContext_ = EGL_NO_CONTEXT;
    EGLSurface eglSurface_ = EGL_NO_SURFACE;
    int width_ = 0;
    int height_ = 0;
};

#endif // MY_NATIVE_BUTTON_H

文件:entry/src/main/cpp/MyNativeButton.cpp

cpp 复制代码
#include "MyNativeButton.h"
#include <hilog/log.h>

#undef LOG_DOMAIN
#undef LOG_TAG
#define LOG_DOMAIN 0x0000
#define LOG_TAG "MyNativeButton"

MyNativeButton::~MyNativeButton() {
    Destroy();
}

bool MyNativeButton::Initialize(OH_NativeXComponent* component, int width, int height) {
    nativeComponent_ = component;
    width_ = width;
    height_ = height;

    // 获取NativeWindow
    if (nativeComponent_ == nullptr) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "nativeComponent_ is null");
        return false;
    }

    uint64_t windowId = 0;
    int32_t ret = OH_NativeXComponent_GetXComponentId(nativeComponent_, &windowId);
    if (ret != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "GetXComponentId failed");
        return false;
    }

    // 获取NativeWindow
    NativeWindow* nativeWindow = nullptr;
    ret = OH_NativeXComponent_GetNativeWindow(nativeComponent_, &nativeWindow);
    if (ret != OH_NATIVEXCOMPONENT_RESULT_SUCCESS) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "GetNativeWindow failed: %d", ret);
        return false;
    }

    if (nativeWindow == nullptr) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "nativeWindow is null");
        return false;
    }

    // 创建EGL上下文
    CreateEGLContext();

    // 设置EGL窗口
    eglSurface_ = eglCreateWindowSurface(eglDisplay_, GetEGLConfig(), nativeWindow, nullptr);
    if (eglSurface_ == EGL_NO_SURFACE) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "eglCreateWindowSurface failed");
        return false;
    }

    // 绑定上下文
    if (!eglMakeCurrent(eglDisplay_, eglSurface_, eglSurface_, eglContext_)) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "eglMakeCurrent failed");
        return false;
    }

    // 设置视口
    glViewport(0, 0, width_, height_);

    OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, "Initialize success, width: %d, height: %d", width_, height_);
    return true;
}

void MyNativeButton::CreateEGLContext() {
    // 获取默认EGL Display
    eglDisplay_ = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    if (eglDisplay_ == EGL_NO_DISPLAY) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "eglGetDisplay failed");
        return;
    }

    EGLint major, minor;
    if (!eglInitialize(eglDisplay_, &major, &minor)) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "eglInitialize failed");
        return;
    }

    // 配置EGL属性
    const EGLint attribs[] = {
        EGL_SURFACE_TYPE, EGL_WINDOW_BIT,
        EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
        EGL_BLUE_SIZE, 8,
        EGL_GREEN_SIZE, 8,
        EGL_RED_SIZE, 8,
        EGL_ALPHA_SIZE, 8,
        EGL_DEPTH_SIZE, 16,
        EGL_STENCIL_SIZE, 8,
        EGL_NONE
    };

    EGLint numConfigs;
    if (!eglChooseConfig(eglDisplay_, attribs, nullptr, 0, &numConfigs)) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "eglChooseConfig failed");
        return;
    }

    EGLConfig config;
    if (!eglChooseConfig(eglDisplay_, attribs, &config, 1, &numConfigs)) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "eglChooseConfig failed 2");
        return;
    }

    // 创建EGL上下文
    EGLint contextAttribs[] = {EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE};
    eglContext_ = eglCreateContext(eglDisplay_, config, EGL_NO_CONTEXT, contextAttribs);
    if (eglContext_ == EGL_NO_CONTEXT) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "eglCreateContext failed");
        return;
    }

    eglConfig_ = config; // 记住config
}

void MyNativeButton::Render() {
    // 清屏
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

    // 绘制一个圆形按钮(简单实现)
    // 实际项目中可以使用更复杂的渲染逻辑
    float radius = 50.0f;
    glm::vec2 center(width_ / 2.0f, height_ / 2.0f);

    // 这里省略OpenGL绘制圆的代码,重点展示流程
    // ...

    // 交换缓冲区
    if (eglSwapBuffers(eglDisplay_, eglSurface_) != EGL_TRUE) {
        OH_LOG_Print(LOG_APP, LOG_ERROR, LOG_DOMAIN, LOG_TAG, "eglSwapBuffers failed");
    }
}

void MyNativeButton::Destroy() {
    DestroyEGLContext();
}

void MyNativeButton::DestroyEGLContext() {
    if (eglDisplay_ == EGL_NO_DISPLAY) return;

    eglMakeCurrent(eglDisplay_, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);

    if (eglContext_ != EGL_NO_CONTEXT) {
        eglDestroyContext(eglDisplay_, eglContext_);
        eglContext_ = EGL_NO_CONTEXT;
    }

    if (eglSurface_ != EGL_NO_SURFACE) {
        eglDestroySurface(eglDisplay_, eglSurface_);
        eglSurface_ = EGL_NO_SURFACE;
    }

    eglTerminate(eglDisplay_);
    eglDisplay_ = EGL_NO_DISPLAY;
}

void MyNativeButton::OnTouchEvent(OH_NativeXComponent* component, TouchEventType type, float x, float y) {
    if (type == TOUCH_EVENT_TYPE_DOWN) {
        OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, "Touch down at (%f, %f)", x, y);
        // 可以在这里触发UI状态变化,通过回调反馈给ArkTS
    } else if (type == TOUCH_EVENT_TYPE_UP) {
        OH_LOG_Print(LOG_APP, LOG_INFO, LOG_DOMAIN, LOG_TAG, "Touch up at (%f, %f)", x, y);
    }
}

第三步:配置CMakeLists.txt

文件:entry/src/main/cpp/CMakeLists.txt

cmake 复制代码
cmake_minimum_required(VERSION 3.18)
project("my_native_app")

# 设置C++标准
set(CMAKE_CXX_STANDARD 17)

# 添加共享库
add_library(my_native_button SHARED
    MyNativeButton.cpp
    napi_init.cpp
)

# 链接必要的库
target_link_libraries(my_native_button
    ace_napi.z
    hilog_ndk.z
    native_window
    EGL
    GLESv3
    z
)

第四步:编写N-API接口

文件:entry/src/main/cpp/napi_init.cpp

cpp 复制代码
#include "napi/native_api.h"
#include "MyNativeButton.h"
#include <hilog/log.h>

// 全局实例映射
std::unordered_map<std::string, MyNativeButton*> g_buttonMap;

// 初始化组件
static napi_value InitComponent(napi_env env, napi_callback_info info) {
    size_t argc = 2;
    napi_value args[2] = {nullptr};

    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    // 获取组件ID(字符串)
    char componentId[256];
    size_t length;
    napi_get_value_string_utf8(env, args[0], componentId, sizeof(componentId), &length);

    // 获取Native XComponent句柄
    OH_NativeXComponent* nativeComponent;
    napi_get_native_pointer(env, args[1], (void**)&nativeComponent);

    // 创建并初始化组件
    auto* button = new MyNativeButton();
    if (!button->Initialize(nativeComponent, 200, 200)) { // 默认宽高
        delete button;
        return nullptr;
    }

    g_buttonMap[std::string(componentId)] = button;

    return nullptr;
}

// 渲染组件
static napi_value RenderComponent(napi_env env, napi_callback_info info) {
    size_t argc = 1;
    napi_value args[1] = {nullptr};

    napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);

    char componentId[256];
    size_t length;
    napi_get_value_string_utf8(env, args[0], componentId, sizeof(componentId), &length);

    auto it = g_buttonMap.find(std::string(componentId));
    if (it != g_buttonMap.end()) {
        it->second->Render();
    }

    return nullptr;
}

// 注册模块
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        { "initComponent", nullptr, InitComponent, nullptr, nullptr, nullptr, napi_default, nullptr },
        { "renderComponent", nullptr, RenderComponent, nullptr, nullptr, nullptr, napi_default, nullptr },
    };
    napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
    return exports;
}
EXTERN_C_END

static napi_module demoModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "my_native_button",
    .nm_priv = nullptr,
    .reserved = { 0 },
};

extern "C" __attribute__((constructor)) void RegisterModule() {
    napi_module_register(&demoModule);
}

第五步:在ArkUI中加载Native组件

文件:entry/src/main/ets/pages/Index.ets

typescript 复制代码
import { XComponent, NodeContent } from '@kit.ArkUI';
import nativeButton from 'libmy_native_button.so';

@Entry
@Component
struct Index {
  @State buttonWidth: number = 200
  @State buttonHeight: number = 200
  private xcomponentController: XComponentController = new XComponentController()

  build() {
    Column() {
      Text("HarmonyOS NDK UI 示例")
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
        .margin({ bottom: 20 })

      // 使用XComponent承载Native UI
      XComponent({
        id: 'myNativeButton',
        type: XComponentType.SURFACE,
        controller: this.xcomponentController
      })
        .width(this.buttonWidth)
        .height(this.buttonHeight)
        .onLoad(() => {
          // 获取Native XComponent实例并传给C++层
          let nativeXComponent = this.xcomponentController.getNativeXComponent();
          if (nativeXComponent) {
            nativeButton.initComponent('myNativeButton', nativeXComponent);
            // 主动触发渲染
            nativeButton.renderComponent('myNativeButton');
          }
        })

      Button("点击触发渲染")
        .margin({ top: 20 })
        .onClick(() => {
          nativeButton.renderComponent('myNativeButton');
        })
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }
}

常见问题

问题1:页面返回后Native组件崩溃

现象:从页面A跳转到页面B,再返回页面A时,Native渲染组件直接闪退。

原因:XComponent销毁时,C++侧的EGL上下文和资源没有被正确释放。页面返回后,系统尝试重建XComponent,但C++层仍在拿着旧的资源句柄。

解决方案:在ArkTS页面onPageHide或页面销毁时,主动调用C++销毁接口,并在ArkUI中监听XComponent销毁事件。

typescript 复制代码
@Entry
@Component
struct Index {
  aboutToDisappear() {
    nativeButton.destroyComponent('myNativeButton');
  }
}

问题2:CMakeLists.txt中缺少必要的库导致链接失败

现象 :编译时出现undefined reference错误,例如eglMakeCurrent

原因:HarmonyOS的NDK开发环境与标准Android有一定区别,部分库的全路径不自动包含。

解决方案 :手动添加EGLGLESv3native_window依赖。注意顺序:先写自己写的cpp,再写依赖库。

cmake 复制代码
target_link_libraries(my_native_button
    ace_napi.z    # HarmonyOS特有的N-API库
    hilog_ndk.z   # 日志库
    native_window # 窗口管理
    EGL           # EGL库
    GLESv3        # OpenGL ES库
    z             # 压缩库
)

问题3:XComponent onLoad回调中获取NativeWindow失败

现象OH_NativeXComponent_GetNativeWindow返回失败(非零)。

原因:XComponent的Surface创建是异步的,onLoad回调表示组件加载完成,但Surface可能还未完全可用。

解决方案 :在onLoad后加入一个微任务延迟,或者使用setTimeout等待一帧再操作。

typescript 复制代码
.onLoad(() => {
  setTimeout(() => {
    let nativeXComponent = this.xcomponentController.getNativeXComponent();
    // 现在安全了
  }, 100);
})

最佳实践

  1. 不要在Render()中频繁申请/释放资源:EGL上下文切换和资源创建是重量级操作。应该在Initialize阶段做好所有准备工作,Render只处理绘制逻辑。

  2. 使用Hilog进行调试而非printfOH_LOG_Print是HarmonyOS NDK推荐的日志接口,支持错误级别和标签分类,在DevEco Studio的Logcat中能清晰过滤。

  3. 触摸事件回调频率远高于ArkUI的gesture:Native侧的触摸事件回调是原始事件流,频率可能高达120Hz。不建议在回调中直接做重渲染操作,应该累积坐标变化后,在下一帧Render时统一处理。

这篇文章里展示的只是一个最简Demo,实际项目中需要处理更多细节:资源池管理、多组件并发渲染、ArkTS侧状态同步。如果你在实际开发中遇到类似问题,可以重点检查生命周期和资源释放逻辑。