当前位置: 首页 > news >正文

《HarmonyOS技术精讲-UI开发 (基于NDK构建UI)》第6篇:集成第三方C++图形库——以Skia为例

HarmonyOS NEXT的Native UI开发中,一种常见的需求

HarmonyOS NEXT的ArkUI框架提供了丰富的Canvas API,能满足大部分2D图形绘制需求。但遇到对性能要求较高的复杂图形渲染场景——比如实时地理信息系统(GIS)地图渲染、复杂的数据可视化图表(如大量节点的拓扑图)、高精度矢量字体排版时,ArkUI的Canvas在某些场景下会成为瓶颈。

这个问题在HarmonyOS开发里比较常见。很多人第一次尝试在Native层绘制复杂图形时,会优先考虑OpenGL ES或Vulkan。但对于大多数2D图形渲染任务,Skia是一个更合适的选择——它提供了完整的2D图形管线、跨平台一致性、丰富的文字排版和路径操作能力,而且不需要像OpenGL那样管理复杂的着色器。

这篇实战教程会走通一个完整流程:将Skia引入HarmonyOS NDK项目、在C++层完成图形渲染、通过OHOS Native组件将渲染结果显示到ArkUI页面上。过程中会涉及库的编译配置、头文件路径设置、渲染上下文的桥接,以及一些实际项目中需要注意的性能问题。

它解决什么问题

适用场景

场景ArkUI CanvasSkia + NDK
简单2D图形(矩形、圆形、直线)简单直接大材小用
复杂矢量图形(贝塞尔曲线、路径裁剪)性能受限,支持有限原生支持,性能可控
大量文字排版(多语言、复杂排版)功能有限完整排版引擎
实时动画/交互式绘图有性能瓶颈利用CPU/GPU渲染
跨平台代码复用仅限鸿蒙可复用其他平台

为什么不直接用OpenGL ES

OpenGL ES确实能提供最高的渲染性能,但它需要开发者自己处理更多的底层逻辑:顶点缓冲、着色器编译、帧缓冲区管理等。Skia对这些细节做了封装,对于2D图形渲染而言,用Skia的开发效率远高于OpenGL ES,且效果不差。如果你的目标是绘制2D图形而不是3D场景,Skia通常是更务实的选择。

为什么不直接用ArkUI Canvas

ArkUI Canvas在普通场景下完全够用。但当你需要渲染数千个独立的矢量元素时,ArkUI的组件化架构反而成了负担——每个路径都是一次UI组件更新。而在Skia的渲染流程里,所有图形操作都转换为绘制指令,最终一次性提交到GPU,性能差异很明显。

环境说明

DevEco Studio 版本:DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本:HarmonyOS 6.1.0(23) 及以上 目标设备:手机、平板

核心实现:集成Skia到NDK项目

第一步:准备Skia库

需要做两件事:编译Skia库、配置NDK项目。

编译Skia(简要流程,实际工程中会在CI里执行):

# 使用HarmonyOS NDK toolchain编译gitclone https://skia.googlesource.com/skiacdskia patch-p1<<你的HarmonyOS编译补丁>python3 tools/git-sync-deps bin/gn gen out/ohos--args="target_cpu=\"arm64\"is_official_build=true skia_use_egl=false skia_use_gl=true"ninja-Cout/ohos skia

编译后得到libskia.a和头文件目录。

配置NDK项目:创建native/子目录,CMakeLists.txt配置如下:

cmake_minimum_required(VERSION 3.4.0) project("skia_demo") set(CMAKE_CXX_STANDARD 17) # 导入Skia add_library(skia STATIC IMPORTED) set_target_properties(skia PROPERTIES IMPORTED_LOCATION ${CMAKE_CURRENT_SOURCE_DIR}/../skia/out/ohos/libskia.a) # 设置头文件路径 target_include_directories(skia_demo PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/../skia/include ${CMAKE_CURRENT_SOURCE_DIR}/../skia/include/core ${CMAKE_CURRENT_SOURCE_DIR}/../skia/include/effects ${CMAKE_CURRENT_SOURCE_DIR}/../skia/include/utils ) target_link_libraries(skia_demo PUBLIC skia)

第二步:创建OHOS Native组件

在ArkUI端,通过XComponent创建Native渲染区域:

// 入口页面 Index.etsimport{XComponentContext}from'@ohos.multimedia.xcomponent';importnativeRenderfrom'libskia_render.so';@Entry@Componentstruct SkiaDemoPage{privatexcomponentContext:XComponentContext|null=null;@StaterenderWidth:number=0;@StaterenderHeight:number=0;build(){Column(){XComponent({id:'skia_render',type:XComponentType.SURFACE,libraryName:'skia_render'}).onLoad((xcomponentContext:XComponentContext)=>{this.xcomponentContext=xcomponentContext;// 获取渲染区域尺寸letrect=xcomponentContext.getXComponentSurfaceRect();this.renderWidth=rect.width;this.renderHeight=rect.height;// 调用Native初始化nativeRender.initWithSurface(xcomponentContext);}).width('100%').height('100%').backgroundColor(Color.White)}.width('100%').height('100%').padding(10)}}

这里的关键是libraryName: 'skia_render',它会让系统加载libskia_render.soonLoad回调里拿到XComponentContext后,传给Native层初始化渲染上下文。

第三步:Native层实现Skia渲染

这是最核心的部分,需要完成:接收XComponent的surface、创建Skia渲染目标、执行绘制。

// native_render.cpp#include<string>#include<cmath>#include<napi/native_api.h>#include<multimedia/xcomponent/xcomponent_native.h>#include<native_window/xcomponent/xcomponent_nativewindow.h>#include<multimedia/player/player_xcomponent.h>#include"include/core/SkSurface.h"#include"include/core/SkCanvas.h"#include"include/core/SkPaint.h"#include"include/core/SkPath.h"#include"include/core/SkFont.h"#include"include/core/SkTypeface.h"#include"include/core/SkTextBlob.h"#include"window.h"// 全局变量:保存XComponent实例和Skia surfacestaticOH_NativeXComponent*g_nativeXComponent=nullptr;staticSkSurface*g_skSurface=nullptr;staticint32_tg_surfaceWidth=0;staticint32_tg_surfaceHeight=0;// 初始化渲染上下文napi_valueInitWithSurface(napi_env env,napi_callback_info info){size_t argc=1;napi_value argv[1];napi_get_cb_info(env,info,&argc,argv,nullptr,nullptr);// 从ArkTS传入的XComponentContext获取native组件napi_valuetype valuetype;napi_typeof(env,argv[0],&valuetype);// 通过NAPI获取OH_NativeXComponent实例// 实际工程中更推荐从XComponent的onLoad回调直接传递native实例OH_NativeXComponent*nativeXComponent=nullptr;napi_get_native_xcomponent(env,argv[0],&nativeXComponent);if(nativeXComponent==nullptr){// 如果获取失败,尝试从XComponent ID获取// 这里简化处理,假设直接拿到}g_nativeXComponent=nativeXComponent;// 获取surface宽高OH_NativeXComponent_GetXComponentSize(nativeXComponent,nullptr,&g_surfaceWidth,&g_surfaceHeight);// 获取native windowvoid*nativeWindow=nullptr;OH_NativeXComponent_GetNativeWindow(nativeXComponent,&nativeWindow);OHNativeWindow*window=reinterpret_cast<OHNativeWindow*>(nativeWindow);// 创建Skia surface绑定到native windowg_skSurface=SkSurface::MakeFromOHNativeWindow(window,SkSurface::Origin::kTopLeft_Origin,nullptr).release();// 首次绘制DrawScene();returnnullptr;}// 绘制函数voidDrawScene(){if(g_skSurface==nullptr)return;SkCanvas*canvas=g_skSurface->getCanvas();canvas->clear(SK_ColorWHITE);// ---- 绘制矢量图形 ----SkPaint paint;paint.setAntiAlias(true);// 绘制贝塞尔曲线路径SkPath path;path.moveTo(50,150);path.cubicTo(100,50,200,250,250,150);paint.setColor(SK_ColorBLUE);paint.setStyle(SkPaint::kStroke_Style);paint.setStrokeWidth(4);canvas->drawPath(path,paint);// 绘制渐变圆SkPaint gradientPaint;SkPoint points[]={{50,50},{150,150}};SkColor colors[]={SK_ColorRED,SK_ColorYELLOW};autogradient=SkGradientShader::MakeLinear(points,colors,nullptr,2,SkTileMode::kClamp);gradientPaint.setShader(gradient);canvas->drawCircle(150,100,80,gradientPaint);// ---- 文字排版 ----// 加载字体文件(需放置到resources/rawfile目录)// 实际工程中需通过资源文件路径加载SkFont font;font.setSize(36);// 设置字体样式(粗体)font.setEmbolden(true);// 创建文字块SkPaint textPaint;textPaint.setColor(SK_ColorBLACK);textPaint.setAntiAlias(true);constchar*text="HarmonyOS NDK & Skia";canvas->drawString(text,50,300,font,textPaint);// 绘制多行文字SkPaint subtitlePaint;subtitlePaint.setColor(SK_ColorGRAY);subtitlePaint.setAntiAlias(true);SkFont subtitleFont;subtitleFont.setSize(18);canvas->drawString("复杂矢量图形和文字排版支持",50,340,subtitleFont,subtitlePaint);// ---- 提交渲染结果 ----g_skSurface->flushAndSubmit();}// NAPI注册staticnapi_valueInit(napi_env env,napi_value exports){napi_property_descriptor desc[]={{"initWithSurface",nullptr,InitWithSurface,nullptr,nullptr,nullptr,napi_default,nullptr}};napi_define_properties(env,exports,sizeof(desc)/sizeof(desc[0]),desc);returnexports;}NAPI_MODULE(skia_render,Init)

这段代码里有几个关键点:

  1. 创建Skia Surface:通过SkSurface::MakeFromOHNativeWindow将Skia的渲染目标绑定到HarmonyOS的Native Window上。这是ArkUI Native组件和Skia渲染管线之间的桥梁。

  2. 绘制流程:和标准Skia用法一致——获取Canvas、设置画笔、绘制路径和文字、最后调用flushAndSubmit提交渲染指令。

  3. 资源管理:Skia的Surface在组件销毁时需要释放,但这里仅做演示。

第四步:处理组件生命周期

ArkUI的XComponent有自己的生命周期回调,需要在Native层注册处理函数:

// 在初始化时注册回调staticvoidOnSurfaceCreated(OH_NativeXComponent*component,void*window){// surface已创建,可以开始渲染initSkiaSurface(window);}staticvoidOnSurfaceChanged(OH_NativeXComponent*component,void*window){// 窗口大小变化,重新创建surfacedeleteg_skSurface;initSkiaSurface(window);DrawScene();}staticvoidOnSurfaceDestroyed(OH_NativeXComponent*component,void*window){// 销毁前释放资源deleteg_skSurface;g_skSurface=nullptr;}// 注册回调OH_NativeXComponent_Callback callback;callback.OnSurfaceCreated=OnSurfaceCreated;callback.OnSurfaceChanged=OnSurfaceChanged;callback.OnSurfaceDestroyed=OnSurfaceDestroyed;OH_NativeXComponent_RegisterCallback(g_nativeXComponent,&callback);

不注册生命周期回调的后果是:当页面返回或切换时,渲染资源不会释放,可能导致内存泄漏或后续渲染错乱。

踩坑记录

坑1:Skia渲染性能下降——像素缓冲区问题

现象:首次启动时渲染流畅,但连续调用flushAndSubmit后出现明显卡顿,帧率下降到个位数。

原因:Skia在创建Surface时默认使用单缓冲模式。每次flushAndSubmit后,Skia会等待GPU完成渲染再返回,导致CPU和GPU无法并行工作。如果渲染内容复杂,每帧的等待时间会累积。

解决方案:使用双缓冲模式:

// 创建Surface时指定双缓冲SkSurfaceProps props;props.setBufferMode(SkSurfaceProps::BufferMode::kDouble);g_skSurface=SkSurface::MakeFromOHNativeWindow(window,SkSurface::Origin::kTopLeft_Origin,&props).release();

双缓冲模式下,Skia会维护两个缓冲区:一个用于GPU渲染,一个用于显示。CPU提交后立即返回,GPU继续渲染,利用率大幅提升。

坑2:字体文件加载失败

现象canvas->drawString无任何文字输出,但矢量图形正常。

原因:Skia默认使用系统字体,但在HarmonyOS环境下,系统字体路径与Android/Linux不同。直接使用默认字体时,Skia可能找不到可用的字体文件。

解决方案:手动指定字体文件路径或使用SkTypeface::MakeFromName指定字体族名称:

// 方式1:指定字体文件路径(需将字体文件放置到rawfile中,运行时读取)// 这里假设字体文件已解压到/data/storage/el2/base/haps/entry/files/目录sk_sp<SkTypeface>typeface=SkTypeface::MakeFromFile("/data/storage/el2/base/haps/entry/files/Roboto-Regular.ttf");if(typeface){font.setTypeface(typeface);}// 方式2:使用系统字体族名称(取决于HarmonyOS支持的字体)font.setTypeface(SkTypeface::MakeFromName("HarmonyOS Sans",SkFontStyle::Normal()));

更稳定的做法是将字体文件打包到resources/rawfile下,在Native层通过NAPI接口读取文件内容后再加载。

最佳实践

  1. 不要在ArkUI的build()中频繁调用Native渲染函数。每次build()都会触发渲染,但Skia的flushAndSubmit会提交GPU指令。如果build()在动画循环中被频繁调用(例如每16ms一次),GPU压力会非常大。建议在Native层用独立的定时器控制渲染频率,ArkUI只负责触发启动渲染循环。

  2. 渲染任务异步化。Skia的DrawScene()如果在ArkUI主线程执行,会阻塞UI更新。需要将flushAndSubmit放到单独的渲染线程中执行。但需要注意线程安全性——Skia的SkSurface不是线程安全的,同一个Surface的所有操作应在同一线程完成。

  3. 使用像素缓冲区提升连续渲染性能。如果需要频繁更新渲染内容(如实时数据图表),推荐使用SkPixelBufferSkColorSpace管理色彩空间,避免每次绘制都重新创建字体、路径等对象。这些对象的创建成本较高。

FAQ

Q:为什么真机渲染效果正常,模拟器上文字会显示乱码或消失?

A:模拟器的设备型号和真实设备在字体配置上存在差异。模拟器可能缺少某些系统字体文件。解决方案是在resources/rawfile中打包一份通用字体如Roboto-Regular.ttf,在Native层手动加载。

Q:页面返回后重新进入,Skia渲染区域变成黑屏?

A:这是生命周期管理问题。页面返回时,XComponent的Surface会被销毁,但Native层的Skia Surface对象没有及时释放。再次进入时,旧的Skia Surface指向一个已失效的Native Window。解决方案是在OnSurfaceDestroyed回调中清空Skia Surface对象,并在OnSurfaceCreated中重新创建。

Q:为什么第一次绘制很快,后续多次绘制后内存占用持续增长?

A:检查Skia版本的幻影图层(Overdraw)问题。某些版本的Skia在创建SkSurface时,如果没有指定合适的SkColorSpace,每次flushAndSubmit时会泄漏像素缓冲区对象。可尝试将Surface的makeRasterImage调用注释掉,或者改用SkSurface::MakeRenderTarget创建离屏渲染目标。

http://www.cnnetsun.cn/news/3002557.html

相关文章:

  • UVa 599 The Forrest for the Trees
  • 登报遗失声明收费标准是什么?登报遗失声明去哪办?流程+费用保姆级指南
  • 智人曾经这样灭绝猛犸象:AI入侵与行业灭绝
  • 如何用3分钟解锁15+加密音乐格式:浏览器中的音乐自由革命
  • 应届生为什么要先学技术再找工作?优选产品结构设计的就业优势
  • NewTab Redirect! 终极指南:5步轻松定制你的Chrome新标签页
  • 淘宝闪购 AI 应用研发二面,我笑了!!!
  • SkillNexus:开源 Skills 全生命周期创造平台
  • 3步快速掌握知网文献批量下载:学术研究效率提升的终极方案
  • 数值半群相对理想的联络理论:主联络与典范联络的构造与应用
  • 【数据分析】自动驾驶车辆控制的优化前馈补偿器的数据驱动方法matlab代码
  • 专业的厨房商用空调哪个公司强
  • 决策树实战指南:从可解释性到业务落地的完整工作流
  • 如何免费获取百度文库等30+平台文档:kill-doc终极指南
  • designmodel-中一维线体-梁单元绘制-和网格划分!!!
  • 放弃解决一类人的痛点,专注用AI解决一个又一个具体的问题,或许会有新的机会
  • 红外与可见光图像融合|主流 SOTA 模型数据集选取及预处理汇总(Part4)
  • MC9S08MP16 SPI模式故障与BDC调试模块实战解析
  • FanControl终极中文设置指南:3分钟让Windows风扇控制彻底汉化
  • 深度学习进阶(十五)通道注意力 SE
  • 在普通CPU上跑通Vicuna大模型的实战指南
  • Java8 到 Java21 核心新特性详解(附实战代码)2026后端面试必备
  • 早期停止聚合:贝叶斯模型选择与泛化误差控制实战
  • Codex CLI 安装与环境配置完整指南
  • 如何用免费工具快速下载哔咔漫画:打造个人离线图书馆的完整指南
  • 如何高效解决Windows热键冲突:Hotkey Detective实用指南
  • C# 与 C 类型对照速查表
  • 中文NLP的语义断层:3步解决全词掩码技术实践
  • 低压电工- 光电传感器(Photoelectric Sensor)
  • 用 Vercel Eve 的 Subagent 和 Skill 搭建 Agent Team