问题:两个 GLSurfaceView 之间没法共享纹理
先说一个实际会遇到的场景:你用 OpenGL ES 在一个 GLSurfaceView 上渲染了内容(比如视频帧、3D 场景、自定义 GL 动画),现在想在另一个 View 上也显示同样的画面。
直觉的做法是把纹理 ID 传过去,让第二个 View 直接 glBindTexture 画出来。但这不行——两个 GLSurfaceView 各自有独立的 EGL Context,Context A 创建的纹理在 Context B 里是不存在的。
EGL 规范提供了解决办法:创建 Context B 时可以指定和 Context A 共享。共享之后,A 的纹理、Buffer Object、Shader Program 在 B 里都可见。
这个机制叫 Shared Context,eglCreateContext 的第三个参数就是用来传共享对象的。
问题在于 Android 的 GLSurfaceView 不给你这个口子。
GLSurfaceView 的 EGL 版本问题
Android 的 GLSurfaceView 内部用的是 javax.microedition.khronos.egl 包——这是 EGL 1.0 的 Java 绑定,从 API 1 就有了。它的 EGLContextFactory 接口长这样:
// android.opengl.GLSurfaceView.EGLContextFactory
public interface EGLContextFactory {
javax.microedition.khronos.egl.EGLContext createContext(
javax.microedition.khronos.egl.EGL10 egl,
javax.microedition.khronos.egl.EGLDisplay display,
javax.microedition.khronos.egl.EGLConfig eglConfig
);
void destroyContext(
javax.microedition.khronos.egl.EGL10 egl,
javax.microedition.khronos.egl.EGLDisplay display,
javax.microedition.khronos.egl.EGLContext context
);
}
参数和返回值全是 javax.microedition.khronos.egl 下的类型。
而 Android 从 API 17 开始提供了 android.opengl.EGL14——EGL 1.4 的绑定,也是现在主流使用的 EGL 接口。很多第三方 SDK(视频 SDK、相机库等)内部都用 EGL14,回调给你的 Context 类型是 android.opengl.EGLContext。
这两个类 javax.microedition.khronos.egl.EGLContext 和 android.opengl.EGLContext 之间没有继承关系,没法互转:
javax.microedition.khronos.egl.EGLContext -- GLSurfaceView 用的
android.opengl.EGLContext -- EGL14 / 第三方 SDK 用的
所以如果你想让一个 GLSurfaceView 和某个 EGL14 Context 共享纹理,标准 API 做不到。EGLContextFactory 的参数类型就把路堵死了。
即便不涉及第三方 SDK,想让两个 GLSurfaceView 之间做 Context 共享,原版的接口也很别扭——你得在旧版 EGL10 的接口里折腾,而 EGL10 的 API 比 EGL14 难用不少。
方案:从 AOSP fork 出 GL14SurfaceView
思路很直接:把 GLSurfaceView 的源码拷出来,把内部所有 javax.microedition.khronos.egl 调用换成 android.opengl.EGL14。
这就是 GL14SurfaceView。逻辑和原版完全一样——GLThread 渲染循环、EglHelper 管理 EGL 生命周期、SurfaceHolder.Callback 处理 Surface 创建销毁——只是 EGL 接口从 1.0 换到了 1.4。
接口改造
原版 DefaultContextFactory:
// 原版 GLSurfaceView 内部 (javax.microedition.khronos.egl)
public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig eglConfig) {
return egl.eglCreateContext(display, eglConfig, EGL10.EGL_NO_CONTEXT, attrib_list);
}
改成 EGL14:
// GL14SurfaceView 内部 (android.opengl.EGL14)
public android.opengl.EGLContext createContext(
android.opengl.EGLDisplay display,
android.opengl.EGLConfig config
) {
int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion, EGL14.EGL_NONE};
return EGL14.eglCreateContext(display, config, EGL14.EGL_NO_CONTEXT, attrib_list, 0);
}
对外暴露的 EGLContextFactory 接口也跟着变:
public interface EGLContextFactory {
android.opengl.EGLContext createContext(EGLDisplay display, EGLConfig eglConfig);
void destroyContext(EGLDisplay display, android.opengl.EGLContext context);
}
EglHelper 改造
EglHelper 里整条 EGL 生命周期全部切到 EGL14 静态方法。改动是机械性的,举几个例子:
// 获取 Display
mEglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
// 初始化
EGL14.eglInitialize(mEglDisplay, version, 0, version, 1);
// 创建 Context(通过 EGLContextFactory,允许外部注入 sharedContext)
mEglContext = view.mEGLContextFactory.createContext(mEglDisplay, mEglConfig);
// 创建 Window Surface
mEglSurface = EGL14.eglCreateWindowSurface(display, config, nativeWindow, null, 0);
// 绑定
EGL14.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext);
// 交换缓冲
EGL14.eglSwapBuffers(mEglDisplay, mEglSurface);
另外多加一个方法让外部能拿到当前 GLThread 的 EGLContext:
public EGLContext getEglContext() {
return mGLThread.getEglContext();
}
这在后面做 Context 共享时会用到。
用法:跨 View 纹理共享
有了 GL14SurfaceView,做 Context 共享就很自然了。
SharedEGLContextFactory
写一个 EGLContextFactory 实现,在 eglCreateContext 时把要共享的 Context 传进去:
class SharedEGLContextFactory(
private val sharedContext: EGLContext
) : GL14SurfaceView.EGLContextFactory {
private val EGL_CONTEXT_CLIENT_VERSION = 0x3098
override fun createContext(display: EGLDisplay, eglConfig: EGLConfig): EGLContext {
val attribList = intArrayOf(EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE)
return EGL14.eglCreateContext(
display,
eglConfig,
sharedContext, // 替代 EGL_NO_CONTEXT
attribList,
0
)
}
override fun destroyContext(display: EGLDisplay?, context: EGLContext?) {
EGL14.eglDestroyContext(display, context)
}
}
改动就一行——EGL14.EGL_NO_CONTEXT 换成 sharedContext。效果是新建的 Context 和传入的 Context 共享纹理命名空间。
示例:两个 View 显示同一个纹理
假设 viewA 是主渲染 View,它创建了一个纹理并往上画了东西。现在想让 viewB 也显示同样的内容。
// viewA: 主渲染 View,正常创建
val viewA = GL14SurfaceView(context).apply {
setEGLContextClientVersion(2)
setRenderer(mainRenderer)
}
// mainRenderer 在 onSurfaceCreated 里创建了一个纹理
// textureId 保存在某个共享变量里
等 viewA 的 GL 线程跑起来拿到 EGLContext 后,创建 viewB:
// viewB: 共享 viewA 的 Context
val viewB = GL14SurfaceView(context).apply {
setEGLContextFactory(SharedEGLContextFactory(viewA.eglContext))
setEGLContextClientVersion(2)
setRenderer(mirrorRenderer) // 直接用 viewA 创建的 textureId 绘制
renderMode = GL14SurfaceView.RENDERMODE_WHEN_DIRTY
}
mirrorRenderer 的 onDrawFrame 里可以直接绑 viewA 创建的纹理:
val mirrorRenderer = object : GL14SurfaceView.Renderer {
override fun onSurfaceCreated(config: EGLConfig?) {
// 不需要再创建纹理,viewA 的纹理在这个 Context 里已经可见
}
override fun onSurfaceChanged(width: Int, height: Int) {
GLES20.glViewport(0, 0, width, height)
}
override fun onDrawFrame() {
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT)
// 直接用 viewA 的 textureId 画
drawTexture(sharedTextureId)
}
}
没有 glReadPixels,没有 Bitmap 中转,没有 CPU 参与的像素数据搬运。两个 View 画的是 GPU 显存里同一份纹理数据。
可以继续扩展
同一个 Context 可以被多个 View 共享。创建 viewC、viewD 时都指向同一个 sharedContext,就可以做 N 路分发。每多一路只增加一次 draw call 的开销。
实际应用场景
这个方案能用在不少地方:
- 视频会议多窗口:SDK 只给一个 Surface 的情况下,用共享 Context 把同一路视频分发到主画面、缩略图、画中画
- 第三方 SDK 纹理拦截:很多视频/地图/广告 SDK 内部用 EGL14 渲染,通过回调拿到 textureId 和 EGLContext 后,可以用这个方案做二次渲染
- GL 渲染结果复用:比如一个 3D 渲染引擎的输出,需要同时出现在主界面和悬浮窗里
- 多屏镜像:同一个渲染结果同时输出到内屏和外接显示器
举个第三方 SDK 的使用方式。比如某个视频 SDK 在回调里给了你 textureId 和 eglContext:
sdk.setVideoFrameCallback { textureId, eglContext ->
if (surfaceView == null) {
// 首次回调:创建共享 Context 的 GL14SurfaceView
surfaceView = GL14SurfaceView(context).apply {
setEGLContextFactory(SharedEGLContextFactory(eglContext))
setEGLContextClientVersion(2)
setRenderer(myRenderer)
renderMode = GL14SurfaceView.RENDERMODE_WHEN_DIRTY
}
container.addView(surfaceView)
}
// 后续回调:更新纹理 ID,请求重绘
currentTextureId = textureId
surfaceView?.requestRender()
}
不需要 YUV 回调,不需要 CPU 转码,不需要开多个 SDK 实例。
注意事项
线程安全
两个 View 的 GLThread 是不同线程。纹理 ID 的写入和读取如果在不同线程,要注意同步。requestRender() 本身是线程安全的(内部有 synchronized),可以在任意线程调用。
如果遇到偶发的画面撕裂,可以在写纹理的一端加 glFenceSync / glWaitSync。
生命周期
共享 Context 有依赖关系:如果 viewA 的 Context 被销毁了,而 viewB 还在用共享的纹理,会 EGL_BAD_CONTEXT 或直接 crash。
销毁顺序要注意——先停消费者(共享方),再停生产者(被共享方)。
preserveEGLContextOnPause
两个 View 都建议设成 true。不然 Activity 暂停时 Context 被销毁,恢复后共享关系就断了,得重新建。
OES 纹理
如果共享的纹理类型是 GL_TEXTURE_EXTERNAL_OES(外部纹理,常见于相机预览和视频解码),Shader 里要声明:
#extension GL_OES_EGL_image_external : require
uniform samplerExternalOES sTexture;
普通的 sampler2D 采样不了 OES 纹理,会黑屏。
和 YUV 回调方案的对比
如果不做 Context 共享,常见的替代方案是从 GPU 把像素数据读回 CPU(glReadPixels 或 YUV 回调),再上传到另一个 View。1080P 30fps 下的开销大概是:
| YUV 回调 | 共享 Context | |
|---|---|---|
| 数据路径 | GPU -> CPU -> GPU | GPU -> GPU |
| CPU 额外占用 | ~25% | 接近零 |
| 单帧延迟 | 40~60ms | <5ms |
| 每帧内存搬运 | ~6MB (1080P YUV) | 0 |
| 扩展开销 | 每多一路翻倍 | 每多一路多一次 draw call |
差距来源很直接:YUV 方案有两次跨总线传输(GPU->CPU 回读 + CPU->GPU 上传),共享 Context 方案里数据始终在 GPU 显存里。
总结
GL14SurfaceView 的改动不大——从 AOSP fork 出来把 EGL 调用换一遍,实际改动不超过 100 行。SharedEGLContextFactory 更短,20 行左右。但有了这两个东西,Android 上跨 View 的 GPU 纹理共享就通了,不再需要像素数据过 CPU 绕一圈。
代码本身不复杂。复杂的是搞清楚为什么标准 GLSurfaceView 做不了这件事——两套 EGL 接口的类型隔离,不翻 AOSP 源码的话不太容易意识到。