【鸿蒙】ArkUI 绘制体系:Canvas 与 XComponent 对比
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 生命周期回调
做法:完整实现 OnSurfaceCreated、OnSurfaceChanged(屏幕旋转/尺寸变化)、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.canvas 在 onReady 之前为 null
- 现象:调用 this.ctx.canvas.width 得到 undefined 或报错
- 原因:CanvasRenderingContext2D 在未与 Canvas 组件绑定时,canvas 属性尚未初始化
- 复现:在 aboutToAppear 或组件构造函数中访问 ctx.canvas
- 解决:所有依赖 Canvas 尺寸的操作必须在 onReady 或 onChange 之后执行
坑点 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 自主",边界清晰,不存在谁好谁坏。---
参考资料
- EGL/OpenGL ES 开发指南(OpenHarmony)
- OpenHarmony 源码:foundation/graphic/graphic_2d/rosen/modules/render_service_client/core/
- OpenHarmony 源码:foundation/graphic/graphic_surface/surface/
更多推荐




所有评论(0)