上周在做一款实时滤镜相机应用时,遇到了严重的性能瓶颈。在 ArkTS 层使用 Image 结合 Canvas 进行像素级图像处理,处理一张 1080P 的图片耗时超过了 2 秒,界面直接卡死。要解决这种计算密集型与渲染密集型叠加的需求,唯一的出路就是下沉到 Native 层,用 C++ 结合 OpenGL ES 来接管渲染管线。

构建跨语言的渲染桥梁

要在 ArkTS 中嵌入原生的 OpenGL 渲染,核心组件是 XComponent。它会在底层创建一个 Surface,并允许我们将这个 Surface 的 ID 传递给 C++ 层。

typescript

@Component
struct GlFilterView {
  private xComponentController: XComponentController = new XComponentController();

  build() {
    XComponent({
      id: 'gl_surface',
      type: XComponentType.SURFACE,
      // 指定加载的 native so 库名称
      libraryname: 'native_renderer' 
    })
    .onLoad(() => {
      // 将 SurfaceID 传递给原生层
      const surfaceId = this.xComponentController.getXComponentSurfaceId();
      nativeRender.initRenderer(surfaceId);
    })
    .onDestroy(() => {
      nativeRender.releaseRenderer();
    })
    .width('100%')
    .height('100%')
  }
}

这里有一个极大的警示:onLoad 回调是在 UI 树绑定完成后才触发的。如果你在 aboutToAppear 里就试图去拿 Surface ID 并传递给 C++,原生层拿到的一定是空指针,导致应用直接闪退。

C++ 层的 EGL 环境初始化

在 C++ 侧,我们需要通过 NAPI 接收 ArkTS 传来的 Surface ID,并利用 EGL 库将其与 OpenGL 的上下文绑定。

cpp

#include <EGL/egl.h>
#include <GLES3/gl3.h>

void InitRenderer(std::string surfaceId) {
    // 获取原生窗口
    OHNativeWindow* window = nullptr;
    OH_NativeWindow_CreateNativeWindowFromSurfaceId(surfaceId.c_str(), &window);

    // 初始化 EGLDisplay
    EGLDisplay display = eglGetDisplay(EGL_DEFAULT_DISPLAY);
    eglInitialize(display, nullptr, nullptr);

    // 配置 EGLConfig(选择 RGB888 等格式)
    EGLConfig config;
    EGLint numConfigs;
    EGLint attribs[] = { EGL_RENDERABLE_TYPE, EGL_OPENGL_ES3_BIT, EGL_NONE };
    eglChooseConfig(display, attribs, &config, 1, &numConfigs);

    // 创建 EGLSurface,绑定原生窗口
    EGLSurface surface = eglCreateWindowSurface(display, config, window, nullptr);

    // 创建并绑定 EGLContext(激活 OpenGL 渲染上下文)
    EGLContext context = eglCreateContext(display, config, EGL_NO_CONTEXT, nullptr);
    eglMakeCurrent(display, surface, surface, context);
}

这段代码是原生渲染的命脉。如果不调用 eglMakeCurrent,后续所有的 OpenGL 指令(如 glClear、glDrawArrays)都会失效,且不会报错。

着色器编写与双缓冲提交

滤镜的核心在于片元着色器(Fragment Shader)。我们需要写一段 GLSL 代码来实现黑白或者复古效果,然后编译并链接到程序中。

每一次渲染循环(Render Loop),我们需要在 C++ 侧的独立渲染线程里执行绘制,并调用 eglSwapBuffers 提交画面。注意:绝不能把这个渲染循环放在 ArkTS 的主线程里,否则不仅毫无性能可言,还会拖垮整个应用的响应速度。

生命周期与内存回收

原生渲染最容易发生内存泄漏。当用户在应用中穿梭,销毁包含 XComponent 的页面时,onDestroy 被触发。此时在 C++ 侧,必须按严格顺序销毁资源:

  1. eglMakeCurrent(display, EGL_NO_SURFACE, EGL_NO_SURFACE, EGL_NO_CONTEXT);

  2. eglDestroySurface(display, surface);

  3. eglDestroyContext(display, context);

  4. eglTerminate(display);

遗漏其中任何一步,都会导致底层的 GPU 内存无法释放,几次页面进出后,应用就会因为显存耗尽而闪退。

准备两张图片,一张作为背景,一张作为蒙版。在 C++ 侧的片元着色器里,通过纹理采样(Texture Sampling),根据蒙版像素的明暗度,动态混合两张图片。编写一个 NAPI 接口暴露给你的 ArkTS 页面,尝试用滑动条动态调节混合比例,感受原生渲染的丝滑。

Logo

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

更多推荐