XComponent 与 GLES:鸿蒙原生查看器的生命周期

0 阅读6分钟

磁刻技术实录 · 第 22 篇 · 工程实践 · 技术基线 2026-10-31

我们正在做的「磁刻(Cike)」,是一款鸿蒙原生的生活记录与个性化创作应用。用户可以把照片做成数字冰箱贴,在洞洞板上自由摆放、组合自己的生活片段,也可以把喜欢的内容做成桌面卡片,用来回看、互动和分享。

在空间记录上,我们引入了 3D Gaussian Splatting(3DGS,三维高斯泼溅),把拍摄的视频重建为可以转动、靠近观察的三维场景。为此,我们重构了视频重建链路,实现自己的 C++ 训练核心,并结合鸿蒙原生的媒体处理、模型查看和分享能力,希望让用户保存一段经历时,也能留下当时的空间关系。

磁刻需要在原生页面里持续显示三维模型,也需要支持用户反复进入、退出和切换采集方式。这篇分享 XComponent 与 GLES 的资源生命周期,说明模型查看怎样与鸿蒙页面、渲染线程和其他原生图形能力协作。

在鸿蒙页面中放置一个 XComponent,只解决了原生绘制入口。真正的查看器还需要管理窗口、EGL 上下文、GPU 资源和持续渲染线程。磁刻把这些生命周期集中在 GsScanRenderer 中,使 ArkUI 负责页面与交互,原生层负责绘制。

以下代码按当前实现整理,保留关键运算与控制顺序;省略导入、日志和完整类型,部分辅助函数采用说明性命名,片段需结合上下文阅读。

最重要的约束是:图形资源必须在正确线程和正确上下文中使用。页面退出时,不能只把某个指针设为空,也不能让 UI 线程随意删除仍被渲染循环使用的缓冲对象。创建、运行和释放必须形成闭合顺序。

原生查看器的生命周期

图 1:图形资源在渲染线程创建、使用和释放

Surface 创建是异步边界

ArkUI 构建组件、原生窗口可用以及 EGL 初始化完成,并不发生在同一瞬间。当前查看器启动独立线程后返回,真正的就绪状态由后续检查提供。因此,调用 Init 成功不能直接被理解为已经能显示模型。

类似地,模型解析与 GPU 上传也有自己的状态。页面可以分别展示准备画面、加载模型与进入浏览,避免把所有等待合成一个不透明的黑屏。错误日志应注明 EGL、GL 或模型阶段。

尺寸有逻辑单位与像素单位

ArkUI 页面布局通常以逻辑单位表达,EGL Surface 的可绘制尺寸则是像素。若直接把页面宽高用于 viewport 或投影,可能造成比例、清晰度和触摸映射不一致。当前绘制路径读取 EGL drawable 尺寸,用统一的实际像素建立渲染范围。

窗口变化时,需要更新渲染尺寸和相机投影。触摸输入仍应保持明确的页面坐标语义,再在边界转换,不能让显示缩放比例隐式混入相机控制。

上下文负责资源身份

初始化阶段选择 EGL 配置,创建上下文与窗口 Surface,并使上下文在渲染线程当前化。随后创建着色器、顶点数组和实例缓冲。绘制时上传必要变化、设置 uniform、发起实例化调用,再交换缓冲。

释放时先结束绘制循环,确保没有继续访问资源,再删除 GL 对象并释放本查看器的 EGL 资源。磁刻代码特别避免对共享默认显示执行会影响其他使用者的全局终止,因为 AR 预览也使用相关显示环境。

Surface 回调负责通知,GL 操作集中到渲染线程

void OnSurfaceCreated(NativeWindow* window) {
    QueueRenderCommand([window] { CreateEglContextAndSurface(window); });
}
void RenderLoop() {
    while (!stopping) {
        DrainRenderCommands();
        if (!surface_ready || !context_ready) continue;
        ApplyPendingModel();
        DrawFrame();
        eglSwapBuffers(display, surface);
    }
}

这是线程职责示意,QueueRenderCommand 代表把状态交给渲染线程的机制,完整循环还需要等待或调度,避免空转。Surface 存在只证明系统提供了绘制目标,并不代表 EGL 已绑定,也不代表模型已经完成上传。三个状态混在一个 ready 变量中,会让时序问题难以排查。

OpenGL 状态依赖当前上下文。业务回调直接调用 GL,即使某次碰巧正常,也不能保证下一次发生在正确线程。把上下文创建、资源上传和绘制收敛到同一所有者,可以把“偶发黑屏”转成可解释的状态转换。

三个不同的就绪条件

图 2:页面存在不代表下一层已经准备完成

页面与渲染线程怎样交换状态

ArkTS 可以更新相机角度、缩放和模型偏移,原生侧在自己的节奏读取这些目标。模型数据通过待上传状态交给渲染线程,避免解析线程直接操作 GL。哪些数据需要锁、哪些可以使用原子状态,应按实际读写关系决定。

这类设计不能仅靠“很少同时发生”保证安全。快速切页、连续打开模型、旋转屏幕和应用前后台切换,会把平时分散的事件挤到一起。生命周期回归需要主动覆盖这些组合。

观察完整退出行为

一个查看器能首次显示,并不代表生命周期可靠。验证应反复进入退出,检查线程是否结束、内存是否回落、旧窗口是否被继续使用,以及再次打开时是否重建正确上下文。发生 GL 或交换缓冲错误时,还要保证循环能退出并留下阶段信息。

销毁时按照依赖方向逆序释放

StopAcceptingFrames();
RequestRenderThreadStop();
JoinRenderThread();  // GL 资源在渲染线程的退出流程中释放

// 以下函数由渲染线程在返回前执行。
void CleanupOnRenderThread() {
    DeleteOwnedGlObjects();
    UnbindOwnedContext();
    DestroyOwnedSurface();
    DestroyOwnedContext();
    // 共享默认 display 不在这里无条件 eglTerminate。
}

这段用顺序清单说明生命周期,不应将注释后的释放函数理解成再次在 UI 线程调用。真实实现需要确保 GL 资源由持有上下文的线程清理,然后等待线程完成。若 Surface 已销毁但线程还在交换缓冲区,就可能访问失效对象。

默认 EGL display 可能与 AR 等路径共享,关闭页面并不等于应用里所有图形功能都结束。应释放当前查看器拥有的资源,而不是无条件终止共享 display。资源所有权写清楚,才能支持反复进入、退出以及从 AR 扫描切换到模型查看。

原生绘制带来对渲染和交互的控制,也要求应用承担资源管理。把 Surface、图形上下文和模型就绪分开,能够让加载、异常和退出都变得可解释。这是将图形算法稳定嵌入鸿蒙产品的基础。