ArkUI 绘制体系:Canvas 与 XComponent 对比

封面信息图

> 掌握 Canvas 与 XComponent 的核心差异,选对绘制方案,避免踩上"用了接口但渲染完全不对"的天坑。

> 适用版本:HarmonyOS NEXT / API 12+ | 阅读时长:约 18 分钟

---

一、从真实需求说起

假设你在开发一款运动健康 App,需要实现两个功能:

1. 一个心率折线图,用户每秒刷新一次数据,图表需要流畅动画。

2. 一个 OpenGL 渲染的 3D 运动轨迹地图,接入第三方 Native 地图 SDK。

直觉上,两者都是"在界面上画图",但如果你把这两个需求都交给 Canvas 或都交给 XComponent,你会碰到截然不同的麻烦:Canvas 无法接入 Native Surface;XComponent 的 ArkTS 绘制 API 极为有限。

这篇文章的目的就是搞清楚:Canvas 和 XComponent 各自是什么,适合什么场景,底层机制为何不同,以及实践中的关键坑点。

---

二、核心原理:两套截然不同的渲染管线

2.1 ArkUI 渲染体系概览

ArkUI 渲染体系

├── ArkTS 侧渲染(托管式)

│ ├── 内置组件(Text/Image/Button…)

│ ├── Canvas 组件 ← ArkTS 2D 绘制 API

│ └── Shape 组件

└── Native Surface 渲染(自持式)

└── XComponent ← 挂载 Native Surface / EGL / OpenGL / Camera

Canvas 属于托管式渲染:ArkUI 框架接管合成、栅格化、VSync 同步等全部流程,你只需调用 ArkTS 绘图 API,框架决定何时提交帧缓冲。

XComponent 属于自持式渲染:框架仅提供一块 OHNativeWindow(即 ANativeWindow),后续的 EGL Surface 初始化、帧提交、双缓冲管理全部由 Native 侧代码自行完成。

2.2 Canvas 的内部调用链

ArkTS: ctx.fillRect(x, y, w, h)

└── CanvasRenderingContext2D (ArkTS binding)

└── CanvasRenderer (C++ Skia 封装层)

└── SkCanvas.drawRect

└── GPU Backend (Vulkan / OpenGL ES)

└── Swap Buffer (由 ArkUI RenderThread 触发)

关键点:你的 ArkTS 绘图指令是批量提交的,在下一次 VSync 信号到来时,RenderThread 才统一提交。这意味着:

- 即使你连续调用 100 次 fillRect,也只产生一次 GPU 提交。

- 你无法控制帧提交时机,也无法在 Canvas 上使用 EGL。

2.3 XComponent 的内部调用链

ArkTS: XComponent { type: SURFACE, onLoad: … }

└── NativeXComponent 回调 (C++ NAPI)

└── OH_NativeXComponent_GetNativeWindow()

└── OHNativeWindow (= ANativeWindow)

├── eglCreateWindowSurface(eglDisplay, config, window, nullptr)

└── 自主 RenderThread → eglSwapBuffers → 提交帧

关键点:你拿到的是一个 Native Surface 句柄,之后的渲染完全脱离 ArkUI 框架。你可以:

- 使用 OpenGL ES / Vulkan 直接渲染。

- 挂载三方 Native SDK(地图、视频、游戏引擎等)。

- 精确控制帧率(如 120fps 独立渲染线程)。

---

三、API 对比速查

| 维度 | Canvas | XComponent |

|------|--------|------------|

| 使用语言 | ArkTS | C++ (NAPI) + ArkTS 触发 |

| 绘制 API | CanvasRenderingContext2D(类 Web Canvas) | OpenGL ES / Vulkan / EGL(自行管理) |

| Surface 控制 | 框架托管,不可干预 | 完全自主(OHNativeWindow) |

| 帧率控制 | 随 ArkUI VSync,通常 60fps | 自行控制,可超 60fps |

| 与 Native SDK 集成 | 不支持 | 原生支持 |

| 性能上限 | 中(受 ArkTS/Skia 层开销) | 高(直连 GPU) |

| 开发复杂度 | 低 | 高(需写 C++) |

| 典型场景 | 图表、手写板、小游戏逻辑 | 地图、视频、AR/VR、游戏引擎 |

| XComponent 类型 | 不适用 | SURFACE(渲染)/ TEXTURE(合成)/ COMPONENT(嵌入 View) |

---

四、Canvas 实战:心率折线图

4.1 完整可运行示例

// HeartRateChart.ets

@Component

export struct HeartRateChart {

// 状态数据:最近 60 秒心率值

@State private dataPoints: number[] = Array(60).fill(75)

private ctx: CanvasRenderingContext2D = new CanvasRenderingContext2D(

new RenderingContextSettings(true) // true = 开启抗锯齿

)

private animTimer: number = -1

// 模拟心率数据推入

aboutToAppear() {

this.animTimer = setInterval(() => {

const newVal = 60 + Math.floor(Math.random() * 60) // 60~120

this.dataPoints = [...this.dataPoints.slice(1), newVal]

}, 1000)

}

aboutToDisappear() {

clearInterval(this.animTimer)

}

// 核心绘制逻辑

private drawChart(ctx: CanvasRenderingContext2D, w: number, h: number) {

ctx.clearRect(0, 0, w, h)

// 背景

ctx.fillStyle = '#1A1A2E'

ctx.fillRect(0, 0, w, h)

// 网格线

ctx.strokeStyle = 'rgba(255,255,255,0.1)'

ctx.lineWidth = 1

for (let i = 1; i < 4; i++) {

const y = (h / 4) * i

ctx.beginPath()

ctx.moveTo(0, y)

ctx.lineTo(w, y)

ctx.stroke()

}

// 心率折线(渐变)

const grad = ctx.createLinearGradient(0, 0, 0, h)

grad.addColorStop(0, '#FF6B6B')

grad.addColorStop(1, '#FF6B6B00')

ctx.strokeStyle = '#FF6B6B'

ctx.lineWidth = 2.5

ctx.beginPath()

const max = 160, min = 40

this.dataPoints.forEach((val, i) => {

const x = (i / (this.dataPoints.length - 1)) * w

const y = h - ((val - min) / (max - min)) * h

i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y)

})

ctx.stroke()

// 最新心率文字

const latest = this.dataPoints[this.dataPoints.length - 1]

ctx.fillStyle = '#FF6B6B'

ctx.font = 'bold 32px sans-serif'

ctx.fillText(${latest} bpm, 12, 44)

}

build() {

Canvas(this.ctx)

.width('100%')

.height(200)

// onReady 仅在 Canvas 首次就绪时触发,不要在此做数据驱动更新

.onReady(() => {

this.drawChart(this.ctx, 375, 200) // 初始绘制

})

// 数据变化时通过 @State 触发 build,在 build 完成后重绘

.onChange(() => {

const size = this.ctx.canvas

this.drawChart(this.ctx, size.width, size.height)

})

}

}

4.2 错误写法 → 问题 → 正确写法

场景:在 onReady 里动态监听数据变化并触发重绘
// 错误写法:在 onReady 里 setInterval 直接调用绘制

.onReady(() => {

setInterval(() => {

this.ctx.clearRect(0, 0, 375, 200)

// ... 绘制逻辑

}, 1000)

})

问题:onReady 只触发一次,此时 Canvas 尺寸可能尚未最终确定(尤其在动态布局中),且 setInterval 里直接操作 ctx 绕过了 ArkUI 渲染调度,可能导致绘制结果在下一帧被覆盖或 ctx 状态不一致。

// 正确写法:用 @State 驱动 build,在 onChange 里重绘

@State private dataPoints: number[] = []

// 数据变更 → @State 更新 → build 重新执行 → onChange 触发 → 重绘

.onChange(() => {

const { width, height } = this.ctx.canvas

this.drawChart(this.ctx, width, height)

})

---

五、XComponent 实战:接入 OpenGL ES 渲染

5.1 ArkTS 侧声明

// GLSurface.ets

@Component

export struct GLSurface {

private xComponentController = new XComponentController()

build() {

XComponent({

id: 'glSurface',

type: XComponentType.SURFACE, // 关键:SURFACE 类型

controller: this.xComponentController,

libraryname: 'gl_renderer' // Native .so 名称(不含 lib 前缀)

})

.width('100%')

.height('100%')

.onLoad((context) => {

// 此时 Surface 已准备好,C++ 侧可安全调用 eglCreateWindowSurface

console.info('XComponent loaded')

})

.onDestroy(() => {

// 必须在此销毁 EGL context,避免 Surface 泄漏

console.info('XComponent destroyed')

})

}

}

5.2 Native 侧关键步骤(C++)

// gl_renderer.cpp — 关键路径

#include

#include

#include

static EGLDisplay g_display = EGL_NO_DISPLAY;

static EGLSurface g_surface = EGL_NO_SURFACE;

static EGLContext g_context = EGL_NO_CONTEXT;

void OnSurfaceCreated(OH_NativeXComponent* component, void* window) {

OHNativeWindow* nativeWindow = static_cast (window);

g_display = eglGetDisplay(EGL_DEFAULT_DISPLAY);

eglInitialize(g_display, nullptr, nullptr);

EGLint attribs[] = {

EGL_SURFACE_TYPE, EGL_WINDOW_BIT,

EGL_RED_SIZE, 8, EGL_GREEN_SIZE, 8,

EGL_BLUE_SIZE, 8, EGL_ALPHA_SIZE, 8,

EGL_DEPTH_SIZE, 16,

EGL_NONE

};

EGLConfig config;

EGLint numConfigs;

eglChooseConfig(g_display, attribs, &config, 1, &numConfigs);

// 注意:第三个参数是 OHNativeWindow*

g_surface = eglCreateWindowSurface(g_display, config,

(EGLNativeWindowType)nativeWindow, nullptr);

EGLint ctxAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE };

g_context = eglCreateContext(g_display, config, EGL_NO_CONTEXT, ctxAttribs);

eglMakeCurrent(g_display, g_surface, g_surface, g_context);

glClearColor(0.1f, 0.1f, 0.3f, 1.0f);

}

void RenderLoop() {

while (g_running) {

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

// ... 绘制调用 ...

eglSwapBuffers(g_display, g_surface); // 提交帧

}

}

void OnSurfaceDestroyed(OH_NativeXComponent* component, void* window) {

eglDestroySurface(g_display, g_surface);

eglDestroyContext(g_display, g_context);

eglTerminate(g_display);

g_surface = EGL_NO_SURFACE;

g_context = EGL_NO_CONTEXT;

}

5.3 错误写法 → 问题 → 正确写法

场景:在 ArkTS 的 onLoad 之前就尝试获取 Surface 尺寸
// 错误写法:在 aboutToAppear 中直接调用 getXComponentSurfaceSize

aboutToAppear() {

const size = this.xComponentController.getXComponentSurfaceSize()

console.info(size: ${size.surfaceWidth}x${size.surfaceHeight}) // 输出 0x0

}

问题:onLoad 触发前,Native Surface 尚未创建,所有尺寸查询返回 0,在 Native 侧也无法安全调用 EGL API。

// 正确写法:在 onLoad 回调中操作

.onLoad(() => {

const size = this.xComponentController.getXComponentSurfaceSize()

// 此时 size 有效,Native 侧也已触发 OnSurfaceCreated

})

---

六、TEXTURE 类型:Canvas 与 XComponent 的"混用"方案

XComponent 还支持 TEXTURE 类型,这是一个容易被忽视但很有用的模式:

XComponent(TEXTURE 类型)

└── 渲染结果通过 OHNativeImage 暴露为 SurfaceTexture

└── 可被 ArkUI 合成器作为普通纹理合成到 UI 层

└── 支持在其上叠加 ArkUI 组件(如弹窗、浮层)

与 SURFACE 类型的核心区别:

- SURFACE:独立 Layer,始终在 ArkUI UI 层之下,无法在其上叠加 ArkUI 组件。

- TEXTURE:作为 ArkUI 合成流程的一个纹理节点,可以被 ArkUI 组件遮盖、混合。

XComponent({

id: 'textureSurface',

type: XComponentType.TEXTURE,

libraryname: 'my_renderer'

})

选择原则:需要在渲染内容上叠加 ArkUI 组件(如弹幕、控制按钮)→ 用 TEXTURE;纯渲染、追求最高性能 → 用 SURFACE。

---

七、最佳实践

7.1 Canvas:用 offscreen 隔离复杂计算

做法:将耗时的图形计算放到 OffscreenCanvas 上完成,再通过 drawImage 一次性贴到主 Canvas。

原因:OffscreenCanvas 的绘制不在 UI 线程,不阻塞主渲染管线。

不这样做会怎样:主 Canvas 上的复杂计算直接占用 UI 线程,超过 16ms 就丢帧,导致整个界面卡顿。

const offscreen = new OffscreenCanvas(width, height)

const offCtx = offscreen.getContext('2d')

// 在 offCtx 上做耗时绘制

this.ctx.drawImage(offscreen, 0, 0)

7.2 XComponent:必须实现完整的 Surface 生命周期回调

做法:完整实现 OnSurfaceCreatedOnSurfaceChanged(屏幕旋转/尺寸变化)、OnSurfaceDestroyed 三个回调,每个回调都要正确管理 EGL 资源。

原因:系统在息屏、转屏、后台时会销毁并重建 Surface。如果只实现了 OnSurfaceCreated,转屏后 EGL Surface 变为无效。

不这样做会怎样:转屏/息屏/回前台后黑屏,极难复现,通常在 QA 阶段才暴露。

7.3 Canvas:onReady 只做初始化,数据驱动用 @State

做法:onReady 只调用一次初始绘制;之后所有数据变更通过修改 @State 变量来触发 onChange 重绘。

原因:onReady 不会随数据变化重复触发,直接在其中用 Timer 驱动重绘会绕过 ArkUI 渲染调度。

不这样做会怎样:脏渲染、画面撕裂,或 clearRect 后界面空白。

7.4 XComponent:渲染线程与 EGL Context 一一对应

做法:为 EGL Context 绑定一个独立的渲染线程,确保所有 EGL 调用都在同一线程执行。

原因:EGL Context 有线程亲和性(thread affinity)。跨线程调用 eglMakeCurrent 会导致 EGL_BAD_ACCESS 或未定义行为。

不这样做会怎样:随机花屏、偶现崩溃,在多核设备上复现率更高。

---

八、常见坑点

坑点 1:Canvas ctx.canvasonReady 之前为 null

- 现象:调用 this.ctx.canvas.width 得到 undefined 或报错

- 原因:CanvasRenderingContext2D 在未与 Canvas 组件绑定时,canvas 属性尚未初始化

- 复现:在 aboutToAppear 或组件构造函数中访问 ctx.canvas

- 解决:所有依赖 Canvas 尺寸的操作必须在 onReadyonChange 之后执行

坑点 2:XComponent SURFACE 类型上叠加的 ArkUI 组件点击无响应

- 现象:在 XComponent 上方放置 Button,按钮显示正常但点击无效

- 原因:SURFACE 类型是独立 Layer,触摸事件被 Native Surface 截获,ArkUI 组件收不到事件

- 复现:使用 SURFACE 类型 + 在其上叠加可点击组件

- 解决:改用 TEXTURE 类型,或通过事件拦截机制处理

坑点 3:Canvas 渐变坐标系在缩放后错位

- 现象:createLinearGradient 定义的渐变方向在 Canvas 缩放后出现偏移

- 原因:渐变坐标在 Canvas 的变换矩阵中定义,scale 之后再定义渐变坐标会被变换

- 复现:先调用 ctx.scale(2, 2),再调用 createLinearGradient(0, 0, 0, height) — 实际渐变终点变为 height*2

- 解决:在 scale 之前 ctx.save(),定义好渐变后 ctx.restore()

坑点 4:XComponent libraryname 大小写错误导致 onLoad 不触发

- 现象:onLoad 回调永远不被调用,Native 侧注册的 NAPI 函数无效

- 原因:libraryname 必须与 .so 文件名(去掉 lib 前缀和 .so 后缀)完全一致,大小写敏感

- 复现:libraryname: 'MyRenderer' 但 so 文件为 libmyrenderer.so

- 解决:检查 CMakeLists.txt 中的 add_library 名称,确保与 libraryname 一致(全小写)

---

九、总结

1. Canvas 是托管式 2D 绘制:ArkTS 侧调用 Web Canvas 兼容 API,框架负责 GPU 提交,开发简单但无法接入 Native Surface。

2. XComponent 是 Native Surface 挂载点:提供 OHNativeWindow,后续渲染完全由 C++ 代码控制,适合高性能/三方 SDK 场景。

3. SURFACE vs TEXTURE:SURFACE 独占 Layer 性能最高但不能被 ArkUI 组件遮盖;TEXTURE 融入 ArkUI 合成流程,支持 UI 叠加。

4. Canvas 数据驱动核心:用 @State 变更触发 onChange 重绘,不要在 Timer 里绕过 ArkUI 渲染调度。

5. XComponent 生命周期铁律:必须完整实现三个 Surface 回调,EGL 资源严格绑定渲染线程。

核心结论:选 Canvas 还是 XComponent,本质是选"框架托管"还是"Native 自主",边界清晰,不存在谁好谁坏。

---

参考资料

- ArkUI Canvas 组件官方文档

- XComponent 组件官方文档

- NativeXComponent API 参考

- EGL/OpenGL ES 开发指南(OpenHarmony)

- OpenHarmony 源码:foundation/graphic/graphic_2d/rosen/modules/render_service_client/core/

- OpenHarmony 源码:foundation/graphic/graphic_surface/surface/

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐