XComponent 原生渲染避坑:Surface 生命周期回调的幂等设计
XComponent 原生渲染避坑:Surface 生命周期回调的幂等设计
前言
在 HarmonyOS 中,如果业务需要 OpenGL / EGL 渲染、相机预览、视频硬解、3D 场景绘制等原生能力,通常会使用 XComponent 组件把一块 Native surfaces 暴露给 NDK。XComponent 通过 OH_NativeXComponent 向 C/C++ 层提供 OnSurfaceCreated、OnSurfaceChanged、OnSurfaceDestroyed 三个回调。
这三个回调的顺序和触发次数,直接决定了渲染能否正确初始化和资源能否安全释放。很多开发者默认 “Created 后就能画,Changed 只在尺寸变化时触发一次,Destroyed 释放即可”。这个直觉往往会导致黑屏、EGL 上下文泄漏、旋转/折叠后画面拉伸、重复创建 FBO 引发崩溃。本文从一个真实 Native 渲染 Bug 出发,给出可直接落地的回调骨架和验证方法。
问题描述
故障现象
某 3D 预览应用使用 XComponent + OpenGL ES 渲染:
- 首次进入页面概率性黑屏 1~2 秒,随后才出现画面;
- 手机横竖屏切换后,画面被拉伸或只显示一半;
- 折叠屏展开时 App 偶发崩溃,堆栈停在
eglMakeCurrent或 FBO 创建处; - 多次进入/退出同一页面后,GPU 内存持续上涨。
错误代码(可复现问题)
// ❌ 错误示范:OnSurfaceChanged 里无条件重建所有资源
void OnSurfaceChanged(OH_NativeXComponent* component, void* window) {
int32_t w = 0, h = 0;
OH_NativeXComponent_GetSurfaceSize(component, &w, &h);
// 每次 changed 都创建新的 EGLSurface / FBO,旧资源泄漏
eglMakeCurrent(g_display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if (g_surface != EGL_NO_SURFACE) eglDestroySurface(g_display, g_surface);
g_surface = eglCreateWindowSurface(g_display, g_config, (EGLNativeWindowType)window, nullptr);
eglMakeCurrent(g_display, g_surface, g_surface, g_context);
// 每次都新建 FBO
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glGenFramebuffers(1, &g_fbo);
glBindFramebuffer(GL_FRAMEBUFFER, g_fbo);
// ... 创建 color/depth attachment
glViewport(0, 0, w, h);
}
复现条件
- XComponent 在页面首次布局完成前被创建,首次
OnSurfaceChanged可能拿到 0×0 或错误尺寸; - 系统横竖屏切换、分屏、折叠展开都会再次触发
OnSurfaceChanged; - 应用切后台后 Native 层资源被回收,回到前台会重新走
Created → Changed; - 没有过滤无效尺寸和重复尺寸。
细节解析
1. 回调语义与触发顺序
XComponent 的三个回调语义如下:
| 回调 | 触发时机 | 职责 |
|---|---|---|
OnSurfaceCreated |
Surface 首次创建 | 创建与尺寸无关的渲染上下文、初始化渲染线程 |
OnSurfaceChanged |
Surface 尺寸/格式从“不确定”变为“确定”,或后续变化 | 按真实尺寸更新 viewport、FBO、投影矩阵等尺寸相关资源 |
OnSurfaceDestroyed |
Surface 销毁或页面退出 | 停止渲染循环,释放仅属于该 Surface 的资源 |
首次进入页面时,常见顺序是 Created → Changed(首次有效尺寸)。也就是说 Changed 第一次触发并不代表“后续变化”,而是 Surface 真正可用的时间点。把 Changed 当成“只触发一次”是造成黑屏和泄漏的根因之一。
2. 为什么需要幂等?
OnSurfaceChanged 必须设计成幂等函数:
- 同一尺寸重复触发时直接返回;
- 无效尺寸(
w <= 0 || h <= 0)直接过滤; - 只重建“尺寸相关”资源,不要动 EGLContext、渲染线程、纹理缓存等尺寸无关对象。
3. 渲染线程与 EGL 上下文
推荐把渲染逻辑放在独立 Native 线程:
OnSurfaceCreated中启动渲染线程;- 线程循环中等待最新尺寸;
OnSurfaceChanged把新尺寸写入原子变量;OnSurfaceDestroyed设置退出标志并 join 线程。
避免在回调线程中直接做 GL 操作,回调线程通常是主线程或 Binder 线程,直接 GL 调用容易阻塞 UI。
4. 无效尺寸保护
在页面动画、分屏拖动、折叠过渡期间,可能收到 w=0/h=0 或极大值。必须做范围校验,否则 glViewport(0, 0, 0, 0) 或分配超大纹理会触发 GL 错误甚至崩溃。
示例代码
修复后的 NDK 骨架(幂等 + 尺寸过滤 + 独立渲染线程)
#include <EGL/egl.h>
#include <GLES3/gl3.h>
#include "native_xcomponent/native_xcomponent.h"
#include <thread>
#include <atomic>
#include <mutex>
struct RenderState {
std::atomic_bool created{false};
std::atomic_bool shouldExit{false};
std::atomic_int32_t pendingW{0};
std::atomic_int32_t pendingH{0};
int32_t currentW = 0;
int32_t currentH = 0;
EGLDisplay display = EGL_NO_DISPLAY;
EGLSurface surface = EGL_NO_SURFACE;
EGLContext context = EGL_NO_CONTEXT;
GLuint fbo = 0;
GLuint colorTex = 0;
std::thread renderThread;
std::mutex mutex;
} g_state;
// OnSurfaceCreated:只创建与尺寸无关的上下文/线程
void OnSurfaceCreated(OH_NativeXComponent* component, void* window) {
g_state.created.store(true);
g_state.shouldExit.store(false);
g_state.renderThread = std::thread(RenderLoop, (EGLNativeWindowType)window);
}
// OnSurfaceChanged:仅更新待渲染尺寸,不直接做 GL 操作
void OnSurfaceChanged(OH_NativeXComponent* component, void* window) {
int32_t w = 0, h = 0;
OH_NativeXComponent_GetSurfaceSize(component, &w, &h);
if (w <= 0 || h <= 0) return;
g_state.pendingW.store(w);
g_state.pendingH.store(h);
}
// OnSurfaceDestroyed:停止渲染线程并清理 Surface 资源
void OnSurfaceDestroyed(OH_NativeXComponent* component, void* window) {
g_state.shouldExit.store(true);
if (g_state.renderThread.joinable()) g_state.renderThread.join();
g_state.created.store(false);
}
static void RenderLoop(EGLNativeWindowType window) {
// 1. 初始化 EGLDisplay/Context(尺寸无关)
g_state.display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
eglInitialize(g_state.display, nullptr, nullptr);
EGLConfig config;
EGLint numConfigs;
EGLint attribs[] = { EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_NONE };
eglChooseConfig(g_state.display, attribs, &config, 1, &numConfigs);
EGLint ctxAttribs[] = { EGL_CONTEXT_CLIENT_VERSION, 3, EGL_NONE };
g_state.context = eglCreateContext(g_state.display, config, EGL_NO_CONTEXT, ctxAttribs);
// 2. 创建 EGLSurface
g_state.surface = eglCreateWindowSurface(g_state.display, config, window, nullptr);
eglMakeCurrent(g_state.display, g_state.surface, g_state.surface, g_state.context);
while (!g_state.shouldExit.load()) {
int32_t w = g_state.pendingW.load();
int32_t h = g_state.pendingH.load();
// 幂等:尺寸未变不重建
if (w > 0 && h > 0 && (w != g_state.currentW || h != g_state.currentH)) {
std::lock_guard<std::mutex> lock(g_state.mutex);
ReleaseSizeDependentResources();
g_state.currentW = w;
g_state.currentH = h;
CreateSizeDependentResources(w, h);
glViewport(0, 0, w, h);
}
if (g_state.currentW > 0) {
DrawFrame();
eglSwapBuffers(g_state.display, g_state.surface);
}
}
// 3. 清理 Surface 相关资源
ReleaseSizeDependentResources();
eglMakeCurrent(g_state.display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);
if (g_state.surface != EGL_NO_SURFACE) eglDestroySurface(g_state.display, g_state.surface);
if (g_state.context != EGL_NO_CONTEXT) eglDestroyContext(g_state.display, g_state.context);
eglTerminate(g_state.display);
}
static void CreateSizeDependentResources(int32_t w, int32_t h) {
glGenFramebuffers(1, &g_state.fbo);
glBindFramebuffer(GL_FRAMEBUFFER, g_state.fbo);
glGenTextures(1, &g_state.colorTex);
glBindTexture(GL_TEXTURE_2D, g_state.colorTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, w, h, 0, GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, g_state.colorTex, 0);
}
static void ReleaseSizeDependentResources() {
if (g_state.fbo) { glDeleteFramebuffers(1, &g_state.fbo); g_state.fbo = 0; }
if (g_state.colorTex) { glDeleteTextures(1, &g_state.colorTex); g_state.colorTex = 0; }
}
static void DrawFrame() {
glClearColor(0.1f, 0.2f, 0.3f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
}
ArkTS 侧 XComponent 配置
@Entry
@Component
struct NativeRenderPage {
private xComponentContext: object | undefined
build() {
Column() {
XComponent({
id: 'xcomponent_native',
type: XComponentType.SURFACE,
libraryname: 'nativerender' // 对应 NDK 动态库名
})
.width('100%')
.height('100%')
.backgroundColor('#000000')
.onLoad((ctx) => { this.xComponentContext = ctx })
.onDestroy(() => { /* NDK 侧 OnSurfaceDestroyed 会自动触发 */ })
}
}
}
回调顺序验证日志
在 NDK 回调和渲染线程里都加上日志:
#include "hilog/log.h"
#define LOGI(...) OH_LOG_Print(LOG_APP, LOG_INFO, 0xFF00, "XComponent", __VA_ARGS__)
void OnSurfaceCreated(...) {
LOGI("Created, tid=%{public}d", gettid());
}
void OnSurfaceChanged(...) {
int32_t w=0,h=0;
OH_NativeXComponent_GetSurfaceSize(component, &w, &h);
LOGI("Changed, w=%{public}d h=%{public}d tid=%{public}d", w, h, gettid());
}
void OnSurfaceDestroyed(...) {
LOGI("Destroyed, tid=%{public}d", gettid());
}
预期日志:
Created, tid=1234
Changed, w=1080 h=2340 tid=1234
Changed, w=2340 h=1080 tid=1234 // 旋转
Changed, w=0 h=0 tid=1234 // 可能收到,应被过滤
Destroyed, tid=1234
总结
避坑要点
OnSurfaceChanged不是只触发一次:首次尺寸确定和后续旋转/分屏/折叠都会触发。- 尺寸过滤是刚需:
w <= 0 || h <= 0必须直接返回,避免 GL 异常。 - changed 必须幂等:尺寸未变不重建资源,只更新尺寸相关对象。
- 回调线程不做 GL 渲染:启动独立 Native 渲染线程,避免阻塞 UI。
Destroyed后清空所有句柄:包括 surfaceId、EGLSurface、FBO,防止回前台复用旧资源。
后续预防方案
- 日志断言:开发阶段在三个回调里打印 tid、尺寸、时间戳,固化期望顺序。
- 旋转/分屏自动化测试:覆盖竖屏 → 横屏 → 分屏 → 折叠展开 → 回前台 → 退出,检查内存和画面比例。
- 资源泄漏检测:每次
Destroyed后用adb shell dumpsys meminfo <pkg>或 Profiler 观察 GPU 内存是否回落。
XComponent Native 渲染的稳定性,核心就是尊重 Surface 生命周期、坚持幂等设计、把 GL 操作隔离到渲染线程。只要守住这三条,黑屏、拉伸、泄漏三大类问题基本可以避免。
更多推荐

所有评论(0)