XComponent 原生渲染避坑:Surface 生命周期回调的幂等设计

前言

在 HarmonyOS 中,如果业务需要 OpenGL / EGL 渲染、相机预览、视频硬解、3D 场景绘制等原生能力,通常会使用 XComponent 组件把一块 Native surfaces 暴露给 NDK。XComponent 通过 OH_NativeXComponent 向 C/C++ 层提供 OnSurfaceCreatedOnSurfaceChangedOnSurfaceDestroyed 三个回调。

这三个回调的顺序和触发次数,直接决定了渲染能否正确初始化和资源能否安全释放。很多开发者默认 “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

总结

避坑要点

  1. OnSurfaceChanged 不是只触发一次:首次尺寸确定和后续旋转/分屏/折叠都会触发。
  2. 尺寸过滤是刚需w <= 0 || h <= 0 必须直接返回,避免 GL 异常。
  3. changed 必须幂等:尺寸未变不重建资源,只更新尺寸相关对象。
  4. 回调线程不做 GL 渲染:启动独立 Native 渲染线程,避免阻塞 UI。
  5. Destroyed 后清空所有句柄:包括 surfaceId、EGLSurface、FBO,防止回前台复用旧资源。

后续预防方案

  • 日志断言:开发阶段在三个回调里打印 tid、尺寸、时间戳,固化期望顺序。
  • 旋转/分屏自动化测试:覆盖竖屏 → 横屏 → 分屏 → 折叠展开 → 回前台 → 退出,检查内存和画面比例。
  • 资源泄漏检测:每次 Destroyed 后用 adb shell dumpsys meminfo <pkg> 或 Profiler 观察 GPU 内存是否回落。

XComponent Native 渲染的稳定性,核心就是尊重 Surface 生命周期、坚持幂等设计、把 GL 操作隔离到渲染线程。只要守住这三条,黑屏、拉伸、泄漏三大类问题基本可以避免。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐