鸿蒙百篇系列第 66 篇 · 高级
第 61 篇我们钻进了 RenderNode 层的渲染管线。这一篇把镜头拉到另一条赛道:XComponent + EGL 自绘。它们的"渲染权"在 Native 一侧,能直接持有 NativeWindow 上屏。游戏引擎、视频编辑、相机预览、自定义图表动效都用得上。本文从鸿蒙图形栈的分层开始,到一个能跑通的 EGL 三角形 Demo 结束。
一、鸿蒙图形栈的分层
鸿蒙图形栈按"渲染逻辑到硬件"分四层,每层各司其职:
应用普通 UI 走"应用层 → 渲染层 → 驱动层",自绘时绕过应用层,直接进驱动层。XComponent 就是打开这条直通通道的"门票"。
二、XComponent 的两种类型
XComponent 不是一个普通组件。它有 type 字段:
代码上看起来差不多:
import { XComponent, XComponentContext } from '@ohos.arkui.advanced';
// SURFACE 类型
XComponent({
type: 'surface',
controller: this.xComponentController,
// onLoad 是 Native 侧拿到 XComponent 句柄的回调
onLoad: () => {}
})
// TEXTURE 类型
XComponent({
type: 'texture',
controller: this.xComponentController,
onLoad: () => {}
})
XComponentContext 是 Native 拿到 component 句柄后的上下文,包含 NativeWindow、nativeRenderContext,是后续 EGL 初始化的入口。
三、NativeWindow:拿到 surface 的钥匙
EGL 要 EGLSurface + EGLContext,Native 一侧通过 NativeWindow 把 XComponent 的 surface 转给 EGL:
#include <ace/xcomponent/native_interface_xcomponent.h>
#include <EGL/egl.h>
#include <GLES3/gl3.h>
struct XCompContext {
EGLSurface eglSurface;
EGLContext eglContext;
EGLDisplay eglDisplay;
NativeWindow nativeWindow;
};
bool InitEGL(XCompContext &ctx, OHNativeWindow *window) {
ctx.eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY);
if (ctx.eglDisplay == EGL_NO_DISPLAY) return false;
EGLint major, minor;
if (!eglInitialize(ctx.eglDisplay, &major, &minor)) return false;
const EGLint configAttribs[] = {
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT,
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, 24,
EGL_STENCIL_SIZE, 8,
EGL_NONE
};
EGLConfig config;
EGLint numConfigs;
if (!eglChooseConfig(ctx.eglDisplay, configAttribs, &config, 1, &numConfigs)) return false;
EGLint nativeVisualId;
eglGetConfigAttrib(ctx.eglDisplay, config, EGL_NATIVE_VISUAL_ID, &nativeVisualId);
OH_NativeWindow_SetNativeWindowColorSpace(window, OH_NativeWindow_GetColorSpaceFromVisualId(nativeVisualId));
const EGLint contextAttribs[] = {
EGL_CONTEXT_CLIENT_VERSION, 3,
EGL_NONE
};
ctx.eglContext = eglCreateContext(ctx.eglDisplay, config, EGL_NO_CONTEXT, contextAttribs);
if (ctx.eglContext == EGL_NO_CONTEXT) return false;
ctx.eglSurface = eglCreateWindowSurface(ctx.eglDisplay, config, window, nullptr);
if (ctx.eglSurface == EGL_NO_SURFACE) return false;
return eglMakeCurrent(ctx.eglDisplay, ctx.eglSurface, ctx.eglSurface, ctx.eglContext) == EGL_TRUE;
}
每一个 NativeWindow 都对应一个 EGL Surface。把 XComponent 的 surface_id 喂给 OH_NativeWindow_CreateFromSurfaceId 即可创建 nativeWindow:
OHNativeWindow *window = OH_NativeWindow_CreateFromSurfaceId(surfaceId, OH_SURFACE_TYPE_TEXTURE);
接着持有这个窗口去做 EGL 初始化,后面就能直接调 OpenGL ES API 画东西了。
四、画一个三角形
完成初始化后,每帧渲染流程是:清屏 → 画 → 提交:
void DrawTriangle() {
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
const char *vertexShader = R"(
#version 300 es
in vec2 aPos;
in vec3 aColor;
out vec3 vColor;
void main() {
gl_Position = vec4(aPos, 0.0, 1.0);
vColor = aColor;
}
)";
const char *fragmentShader = R"(
#version 300 es
precision mediump float;
in vec3 vColor;
out vec4 FragColor;
void main() {
FragColor = vec4(vColor, 1.0);
}
)";
// 编译、链接、绑定、着色器绘制过程略
// 提交 Surface
eglSwapBuffers(eglDisplay, eglSurface);
}
完整示例工程可以参考华为官方的 NativeWindow & EGL 指导,我把它简化成最关键的几步:着色器编译、VAO 绑定、uniform 变量设置、glDrawArrays 调用、eglSwapBuffers。
五、ArkTS 侧绑定 XComponent
class XCompController extends XComponentController {
// 被 Native 通过 OH_COMPONENT 接口回调过来,绑定 surfaceId
private surfaceId: string = '';
getSurfaceId(): string {
return this.surfaceId;
}
}
@Entry
@Component
struct EGLCover {
private controller: XCompController = new XCompController();
build() {
Column() {
XComponent({
id: 'egl',
type: 'surface',
controller: this.controller
})
.width('100%')
.height(400)
.onLoad(() => {
// 注册 Native 回调
// Native 会拿到 controller.xcomponent_
})
}
}
}
Native 侧收到回调后通过 OH_XComponent_GetXComponentContext(component) 拿到上下文,调用 OH_NativeWindow_CreateFromSurfaceId(context->surfaceId, ...)。
六、Native XComponent 与 ArkTS XComponent 的边界
这两个很容易混:
完整声明:
XComponent({
type: 'surface',
controller: this.controller,
xcomponent: { // 用于 ArkTS 侧接回调
onLoad: (context) => {
// context.surfaceId
}
},
xcomponent_native: { // Native 侧接回调
onSurfaceCreated: (context) => { /* C++ 回调 */ },
onSurfaceDestroyed: () => { /* ... */ }
}
})
ArkTS 的 onLoad 用于直接调 canvas.fillRect,不能拿 EGL Surface;真正走 EGL 自绘要写到 xcomponent_native 的回调里。
七、合成类型:TEXTURE 的特殊价值
TEXTURE 类型适合做"叠加"。比如直播场景:把摄像头(OPENGL 推流)当背景,画面上加一层 UI 控件(点赞、礼物)。TEXTURE 渲染的内容会跟 XComponent 兄弟节点一起合成:
XComponent({
type: 'texture', // ← 关键
controller: this.controller
})
Native 侧把每一帧画到 GLES texture,再用 OH_NativeWindow_SetSurfacePixelMapRotation 或其相关接口合上去。具体合成逻辑见 Render Service 的文档,正常情况下框架自动处理。
八、性能与坑
帧率控制。自绘默认是 vsync 60FPS 锁定,要解锁 90/120Hz 要申请高刷权限
EGL Context 不能跨线程共享。多线程渲染需要各自创建 Context 并显式
eglMakeCurrentNativeWindow 释放对顺序敏感。先
destroy资源,再OH_NativeWindow_DestroyNativeWindow,否则会出现 leak 警告deprecated 的 API 别用。
GSNativeWindow已被 OH_NativeWindow 系列取代OpenGL ES 版本选用 ES3.0+。ES2.0 缺少 uniform block、compute 等特性,画图表明显吃亏
重要资源清理放 onSurfaceDestroyed。否则 framebuffer、texture leak 等到应用崩溃才发现
九、几个工程模式
十、和前几篇的衔接
第 61 篇讲 RenderNode。本篇是 RenderNode 的"放大版"——XComponent 直接拥有整个 EGL Surface
第 14 篇讲音视频,那里的视频渲染底层就是 XComponent + Surface
第 64 篇的 NDK 是 XComponent EGL 自绘的工程基础,没看过可以先翻一遍
十一、小结
XComponent 是 ArkUI 体系里的"Native 渲染通行证"。一旦你的需求超出"声明式 UI"的范畴——做极致流畅的自绘、做视频编辑器、做实时滤镜——它就是必经之路。掌握 NativeWindow + EGL 初始化、shader 编译、draw/swap 节奏之后,就可以放心把 OpenGL ES 生态的东西搬过来。
下一篇转向电源管理,讨论 RunningLock、Thermal、后台保活的取舍。