《HarmonyOS技术精讲-UI开发 (基于NDK构建UI)》第1篇:初探NDK与ArkUI集成
为什么需要NDK构建UIHarmonyOS 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足矣需要频繁动态修改的UIArkUI的响应式机制更成熟环境说明在开始之前确保你的开发环境已准备就绪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#ifndefMY_NATIVE_BUTTON_H#defineMY_NATIVE_BUTTON_H#includeace/xcomponent/native_interface_xcomponent.h#includenative_window/external_window.h#includeEGL/egl.h#includeEGL/eglext.h#includeGLES3/gl3.h#includestringclassMyNativeButton{public:MyNativeButton()default;~MyNativeButton();// 初始化EGL和OpenGL上下文boolInitialize(OH_NativeXComponent*component,intwidth,intheight);// 执行渲染voidRender();// 销毁资源voidDestroy();// 处理触摸事件voidOnTouchEvent(OH_NativeXComponent*component,TouchEventType type,floatx,floaty);private:voidCreateEGLContext();voidDestroyEGLContext();OH_NativeXComponent*nativeComponent_nullptr;EGLDisplay eglDisplay_EGL_NO_DISPLAY;EGLContext eglContext_EGL_NO_CONTEXT;EGLSurface eglSurface_EGL_NO_SURFACE;intwidth_0;intheight_0;};#endif// MY_NATIVE_BUTTON_H文件entry/src/main/cpp/MyNativeButton.cpp#includeMyNativeButton.h#includehilog/log.h#undefLOG_DOMAIN#undefLOG_TAG#defineLOG_DOMAIN0x0000#defineLOG_TAGMyNativeButtonMyNativeButton::~MyNativeButton(){Destroy();}boolMyNativeButton::Initialize(OH_NativeXComponent*component,intwidth,intheight){nativeComponent_component;width_width;height_height;// 获取NativeWindowif(nativeComponent_nullptr){OH_LOG_Print(LOG_APP,LOG_ERROR,LOG_DOMAIN,LOG_TAG,nativeComponent_ is null);returnfalse;}uint64_twindowId0;int32_tretOH_NativeXComponent_GetXComponentId(nativeComponent_,windowId);if(ret!OH_NATIVEXCOMPONENT_RESULT_SUCCESS){OH_LOG_Print(LOG_APP,LOG_ERROR,LOG_DOMAIN,LOG_TAG,GetXComponentId failed);returnfalse;}// 获取NativeWindowNativeWindow*nativeWindownullptr;retOH_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);returnfalse;}if(nativeWindownullptr){OH_LOG_Print(LOG_APP,LOG_ERROR,LOG_DOMAIN,LOG_TAG,nativeWindow is null);returnfalse;}// 创建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);returnfalse;}// 绑定上下文if(!eglMakeCurrent(eglDisplay_,eglSurface_,eglSurface_,eglContext_)){OH_LOG_Print(LOG_APP,LOG_ERROR,LOG_DOMAIN,LOG_TAG,eglMakeCurrent failed);returnfalse;}// 设置视口glViewport(0,0,width_,height_);OH_LOG_Print(LOG_APP,LOG_INFO,LOG_DOMAIN,LOG_TAG,Initialize success, width: %d, height: %d,width_,height_);returntrue;}voidMyNativeButton::CreateEGLContext(){// 获取默认EGL DisplayeglDisplay_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属性constEGLint 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}voidMyNativeButton::Render(){// 清屏glClearColor(0.0f,0.0f,0.0f,0.0f);glClear(GL_COLOR_BUFFER_BIT|GL_DEPTH_BUFFER_BIT);// 绘制一个圆形按钮简单实现// 实际项目中可以使用更复杂的渲染逻辑floatradius50.0f;glm::vec2center(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);}}voidMyNativeButton::Destroy(){DestroyEGLContext();}voidMyNativeButton::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;}voidMyNativeButton::OnTouchEvent(OH_NativeXComponent*component,TouchEventType type,floatx,floaty){if(typeTOUCH_EVENT_TYPE_DOWN){OH_LOG_Print(LOG_APP,LOG_INFO,LOG_DOMAIN,LOG_TAG,Touch down at (%f, %f),x,y);// 可以在这里触发UI状态变化通过回调反馈给ArkTS}elseif(typeTOUCH_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.txtcmake_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#includenapi/native_api.h#includeMyNativeButton.h#includehilog/log.h// 全局实例映射std::unordered_mapstd::string,MyNativeButton*g_buttonMap;// 初始化组件staticnapi_valueInitComponent(napi_env env,napi_callback_info info){size_t argc2;napi_value args[2]{nullptr};napi_get_cb_info(env,info,argc,args,nullptr,nullptr);// 获取组件ID字符串charcomponentId[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*buttonnewMyNativeButton();if(!button-Initialize(nativeComponent,200,200)){// 默认宽高deletebutton;returnnullptr;}g_buttonMap[std::string(componentId)]button;returnnullptr;}// 渲染组件staticnapi_valueRenderComponent(napi_env env,napi_callback_info info){size_t argc1;napi_value args[1]{nullptr};napi_get_cb_info(env,info,argc,args,nullptr,nullptr);charcomponentId[256];size_t length;napi_get_value_string_utf8(env,args[0],componentId,sizeof(componentId),length);autoitg_buttonMap.find(std::string(componentId));if(it!g_buttonMap.end()){it-second-Render();}returnnullptr;}// 注册模块EXTERN_C_STARTstaticnapi_valueInit(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);returnexports;}EXTERN_C_ENDstaticnapi_module demoModule{.nm_version1,.nm_flags0,.nm_filenamenullptr,.nm_register_funcInit,.nm_modnamemy_native_button,.nm_privnullptr,.reserved{0},};externC__attribute__((constructor))voidRegisterModule(){napi_module_register(demoModule);}第五步在ArkUI中加载Native组件文件entry/src/main/ets/pages/Index.etsimport{XComponent,NodeContent}fromkit.ArkUI;importnativeButtonfromlibmy_native_button.so;EntryComponentstruct Index{StatebuttonWidth:number200StatebuttonHeight:number200privatexcomponentController:XComponentControllernewXComponentController()build(){Column(){Text(HarmonyOS NDK UI 示例).fontSize(20).fontWeight(FontWeight.Bold).margin({bottom:20})// 使用XComponent承载Native UIXComponent({id:myNativeButton,type:XComponentType.SURFACE,controller:this.xcomponentController}).width(this.buttonWidth).height(this.buttonHeight).onLoad((){// 获取Native XComponent实例并传给C层letnativeXComponentthis.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销毁事件。EntryComponentstruct Index{aboutToDisappear(){nativeButton.destroyComponent(myNativeButton);}}问题2CMakeLists.txt中缺少必要的库导致链接失败现象编译时出现undefined reference错误例如eglMakeCurrent。原因HarmonyOS的NDK开发环境与标准Android有一定区别部分库的全路径不自动包含。解决方案手动添加EGL、GLESv3、native_window依赖。注意顺序先写自己写的cpp再写依赖库。target_link_libraries(my_native_button ace_napi.z # HarmonyOS特有的N-API库 hilog_ndk.z # 日志库 native_window # 窗口管理 EGL # EGL库 GLESv3 # OpenGL ES库 z # 压缩库 )问题3XComponent onLoad回调中获取NativeWindow失败现象OH_NativeXComponent_GetNativeWindow返回失败非零。原因XComponent的Surface创建是异步的onLoad回调表示组件加载完成但Surface可能还未完全可用。解决方案在onLoad后加入一个微任务延迟或者使用setTimeout等待一帧再操作。.onLoad((){setTimeout((){letnativeXComponentthis.xcomponentController.getNativeXComponent();// 现在安全了},100);})最佳实践不要在Render()中频繁申请/释放资源EGL上下文切换和资源创建是重量级操作。应该在Initialize阶段做好所有准备工作Render只处理绘制逻辑。使用Hilog进行调试而非printfOH_LOG_Print是HarmonyOS NDK推荐的日志接口支持错误级别和标签分类在DevEco Studio的Logcat中能清晰过滤。触摸事件回调频率远高于ArkUI的gestureNative侧的触摸事件回调是原始事件流频率可能高达120Hz。不建议在回调中直接做重渲染操作应该累积坐标变化后在下一帧Render时统一处理。这篇文章里展示的只是一个最简Demo实际项目中需要处理更多细节资源池管理、多组件并发渲染、ArkTS侧状态同步。如果你在实际开发中遇到类似问题可以重点检查生命周期和资源释放逻辑。