场景
在Android平台。在做分屏应用时,特别是低端设备上,为了实现极致的性能优化,就需要利用GLContext共享来实现。
假设有2个Surface组件,一个是GLSurfaceView,另一个也是GLSurfaceView,前一个我们叫它A,后一个我们称它B,为了实现后台渲染,我们利用EGL实现了一个EGLContext和EGL Texture。
我们的需求是,在视频解码后,能够实现A、B极致的性能渲染。
这是一个非常经典且高级的应用场景!利用"星形"共享架构来实现视频分屏渲染,是性能最高、内存占用最低的做法。
在视频播放场景中,核心链路是: 视频播放器 (解码)
→ Surface → SurfaceTexture → OES 纹理 (Root Context) → 共享给 A 和 B 渲染。
这里有三个极其重要的 OpenGL 机制需要处理:
- OES 纹理:Android 视频解码出来的数据通常是 YUV 格式,不能直接用普通的 GL_TEXTURE_2D 渲染,必须使用 GL_TEXTURE_EXTERNAL_OES。
- SurfaceTexture 的线程限制:SurfaceTexture.updateTexImage() 必须在创建它的 EGLContext 所在的线程调用。因此,我们需要为 Root Context 开启一个后台线程。
- 纹理矩阵(Transform Matrix):视频解码到纹理后,方向通常是颠倒或错位的,必须使用 SurfaceTexture 提供的矩阵进行坐标变换。
OES纹理
要实现分屏渲染,首先是离屏能力必须具备
java
public class RootEGLManager {
private static RootEGLManager instance;
private EGL10 mEgl;
private EGLDisplay mEglDisplay;
private EGLContext mRootContext;
private RootEGLManager() {
initRootContext();
}
public static synchronized RootEGLManager getInstance() {
if (instance == null) {
instance = new RootEGLManager();
}
return instance;
}
private void initRootContext() {
mEgl = (EGL10) EGLContext.getEGL();
mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
int[] version = new int[2];
mEgl.eglInitialize(mEglDisplay, version);
// 配置属性:支持 OpenGL ES 2.0/3.0
int[] configAttribs = {
EGL10.EGL_RENDERABLE_TYPE, 4, // EGL_OPENGL_ES2_BIT
EGL10.EGL_RED_SIZE, 8,
EGL10.EGL_GREEN_SIZE, 8,
EGL10.EGL_BLUE_SIZE, 8,
EGL10.EGL_ALPHA_SIZE, 8,
EGL10.EGL_DEPTH_SIZE, 16,
EGL10.EGL_NONE
};
EGLConfig[] configs = new EGLConfig[1];
int[] numConfigs = new int[1];
mEgl.eglChooseConfig(mEglDisplay, configAttribs, configs, 1, numConfigs);
EGLConfig config = configs[0];
// 指定 OpenGL ES 2.0 (如果用3.0,改为3)
int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
int[] contextAttribs = {
EGL_CONTEXT_CLIENT_VERSION, 2,
EGL10.EGL_NONE
};
// 创建 Root Context,第三个参数 EGL10.EGL_NO_CONTEXT 表示它不与其他Context共享
mRootContext = mEgl.eglCreateContext(mEglDisplay, config, EGL10.EGL_NO_CONTEXT, contextAttribs);
}
public EGLContext getRootContext() {
return mRootContext;
}
public void release() {
if (mEglDisplay != EGL10.EGL_NO_DISPLAY && mRootContext != EGL10.EGL_NO_CONTEXT) {
mEgl.eglDestroyContext(mEglDisplay, mRootContext);
mRootContext = EGL10.EGL_NO_CONTEXT;
}
}
}
共享方案
下面是完整的代码实现方案:
第一步:创建后台视频纹理管理器 (VideoTextureManager) 这个类负责在后台线程绑定 Root Context,生成 OES 纹理,并提供 Surface 给播放器。
java
public class VideoTextureManager {
private HandlerThread mGLThread;
private Handler mGLHandler;
private SurfaceTexture mSurfaceTexture;
private Surface mSurface;
private int mOesTextureId;
private float[] mTransformMatrix = new float[16];
// 回调接口,通知 View A 和 B 刷新
public interface OnFrameUpdatedListener {
void onFrameUpdated(int textureId, float[] matrix);
}
private OnFrameUpdatedListener mListener;
public VideoTextureManager(OnFrameUpdatedListener listener) {
this.mListener = listener;
// 1. 启动一个后台 GL 线程
mGLThread = new HandlerThread("RootGLThread");
mGLThread.start();
mGLHandler = new Handler(mGLThread.getLooper());
// 2. 在后台线程初始化 EGL 环境并创建纹理
mGLHandler.post(this::initGLAndSurface);
}
private void initGLAndSurface() {
EGL10 egl = (EGL10) EGLContext.getEGL();
EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
EGLContext rootContext = RootEGLManager.getInstance().getRootContext();
// 为了让 Root Context 能在这个线程 makeCurrent,我们需要创建一个 1x1 的离屏 Pbuffer Surface
int[] pbufferAttribs = {
EGL10.EGL_WIDTH, 1,
EGL10.EGL_HEIGHT, 1,
EGL10.EGL_NONE
};
// 注意:这里需要获取 RootEGLManager 中的 EGLConfig,为了简便,已经在 RootEGLManager 中暴露了 getConfig()
EGLConfig config = RootEGLManager.getInstance().getConfig();
EGLSurface pbufferSurface = egl.eglCreatePbufferSurface(display, config, pbufferAttribs);
// 绑定 Root Context 到当前后台线程
egl.eglMakeCurrent(display, pbufferSurface, pbufferSurface, rootContext);
// 3. 创建 OES 纹理
int[] textures = new int[1];
GLES20.glGenTextures(1, textures, 0);
mOesTextureId = textures[0];
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mOesTextureId);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameterf(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE);
GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE);
// 4. 创建 SurfaceTexture 和 Surface
mSurfaceTexture = new SurfaceTexture(mOesTextureId);
mSurface = new Surface(mSurfaceTexture);
// 5. 监听视频帧到达
mSurfaceTexture.setOnFrameAvailableListener(surfaceTexture -> {
// 必须在当前 GL 线程调用 updateTexImage
mGLHandler.post(() -> {
try {
surfaceTexture.updateTexImage();
surfaceTexture.getTransformMatrix(mTransformMatrix);
GLES20.glFlush();
// 通知 A 和 B 渲染
if (mListener != null) {
mListener.onFrameUpdated(mOesTextureId, mTransformMatrix);
}
} catch (Exception e) {
e.printStackTrace();
}
});
});
}
// 提供给播放器 (如 MediaPlayer / ExoPlayer)
public Surface getSurface() {
return mSurface;
}
public void release() {
mGLHandler.post(() -> {
if (mSurface != null) mSurface.release();
if (mSurfaceTexture != null) mSurfaceTexture.release();
mGLThread.quitSafely();
});
}
}
第二步:编写支持 OES 的 Renderer (A 和 B 可以共用同一个类) 因为是视频渲染,着色器必须使用 #extension GL_OES_EGL_image_external : require。
kotlin
import android.opengl.GLES11Ext;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
public class VideoRenderer implements GLSurfaceView.Renderer {
// OES 专属顶点着色器 (接收矩阵变换)
private static final String VERTEX_SHADER =
"attribute vec4 aPosition;\n" +
"attribute vec4 aTexCoord;\n" +
"uniform mat4 uTexMatrix;\n" + // 接收 SurfaceTexture 的矩阵
"varying vec2 vTexCoord;\n" +
"void main() {\n" +
" gl_Position = aPosition;\n" +
" vTexCoord = (uTexMatrix * aTexCoord).xy;\n" +
"}";
// OES 专属片段着色器
private static final String FRAGMENT_SHADER =
"#extension GL_OES_EGL_image_external : require\n" +
"precision mediump float;\n" +
"varying vec2 vTexCoord;\n" +
"uniform samplerExternalOES uTexture;\n" + // 注意类型是 samplerExternalOES
"void main() {\n" +
" gl_FragColor = texture2D(uTexture, vTexCoord);\n" +
"}";
private int mProgram;
private FloatBuffer mVertexBuffer;
// 由 VideoTextureManager 传递过来的数据
private int mTextureId = 0;
private float[] mTransformMatrix = new float[16];
public VideoRenderer() {
float[] vertices = {
-1f, -1f, 0f, 0f,
1f, -1f, 1f, 0f,
-1f, 1f, 0f, 1f,
1f, 1f, 1f, 1f
};
mVertexBuffer = ByteBuffer.allocateDirect(vertices.length * 4)
.order(ByteOrder.nativeOrder()).asFloatBuffer().put(vertices);
}
// 供外部更新纹理状态
public void updateFrame(int textureId, float[] matrix) {
this.mTextureId = textureId;
System.arraycopy(matrix, 0, this.mTransformMatrix, 0, 16);
}
@Override
public void onSurfaceCreated(GL10 gl, EGLConfig config) {
mProgram = buildProgram(VERTEX_SHADER, FRAGMENT_SHADER);
}
@Override
public void onSurfaceChanged(GL10 gl, int width, int height) {
GLES20.glViewport(0, 0, width, height);
}
@Override
public void onDrawFrame(GL10 gl) {
GLES20.glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
if (mTextureId == 0) return; // 视频还没准备好
GLES20.glUseProgram(mProgram);
// 绑定 OES 纹理
GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, mTextureId);
GLES20.glUniform1i(GLES20.glGetUniformLocation(mProgram, "uTexture"), 0);
// 传递矩阵
int matrixHandle = GLES20.glGetUniformLocation(mProgram, "uTexMatrix");
GLES20.glUniformMatrix4fv(matrixHandle, 1, false, mTransformMatrix, 0);
// 绘制顶点
int posHandle = GLES20.glGetAttribLocation(mProgram, "aPosition");
int texHandle = GLES20.glGetAttribLocation(mProgram, "aTexCoord");
mVertexBuffer.position(0);
GLES20.glVertexAttribPointer(posHandle, 2, GLES20.GL_FLOAT, false, 16, mVertexBuffer);
GLES20.glEnableVertexAttribArray(posHandle);
mVertexBuffer.position(2);
GLES20.glVertexAttribPointer(texHandle, 2, GLES20.GL_FLOAT, false, 16, mVertexBuffer);
GLES20.glEnableVertexAttribArray(texHandle);
GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);
}
private int buildProgram(String vertex, String fragment) {
// 编译着色器代码 (同上一个回答的 GLUtils)
// ...
return program;
}
}
第三步:在 Activity 中组装 (播放器 + 共享渲染) 在这里,我们将 GLSurfaceView 设置为 按需渲染 (RENDERMODE_WHEN_DIRTY)。只有当视频解码出新的一帧时,才通知 A 和 B 刷新,这样极其省电。
kotlin
public class VideoSplitActivity extends AppCompatActivity {
private GLSurfaceView glViewA;
private GLSurfaceView glViewB;
private VideoRenderer rendererA;
private VideoRenderer rendererB;
private VideoTextureManager mVideoTextureManager;
private MediaPlayer mMediaPlayer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_split);
glViewA = findViewById(R.id.gl_view_a);
glViewB = findViewById(R.id.gl_view_b);
// 1. 初始化 Root Context (确保在 View 初始化前调用)
EGLContext rootContext = RootEGLManager.getInstance().getRootContext();
// 2. 配置 View A
rendererA = new VideoRenderer();
glViewA.setEGLContextClientVersion(2);
glViewA.setEGLContextFactory(new SharedEGLContextFactory(rootContext));
glViewA.setRenderer(rendererA);
glViewA.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); // 按需渲染
// 3. 配置 View B
rendererB = new VideoRenderer();
glViewB.setEGLContextClientVersion(2);
glViewB.setEGLContextFactory(new SharedEGLContextFactory(rootContext));
glViewB.setRenderer(rendererB);
glViewB.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY); // 按需渲染
// 4. 初始化视频纹理管理器
mVideoTextureManager = new VideoTextureManager((textureId, matrix) -> {
// 当视频有一帧新数据到达时:
// 更新 A 和 B 的数据
rendererA.updateFrame(textureId, matrix);
rendererB.updateFrame(textureId, matrix);
// 唤醒 A 和 B 进行绘制
glViewA.requestRender();
glViewB.requestRender();
});
// 5. 延迟一点时间等待 Surface 创建完成,然后初始化播放器
glViewA.postDelayed(this::setupPlayer, 500);
}
private void setupPlayer() {
mMediaPlayer = new MediaPlayer();
try {
// 设置视频源 (替换为你的视频路径)
mMediaPlayer.setDataSource("http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4");
// 将 Root Context 生成的 Surface 交给播放器!
mMediaPlayer.setSurface(mVideoTextureManager.getSurface());
mMediaPlayer.setLooping(true);
mMediaPlayer.prepareAsync();
mMediaPlayer.setOnPreparedListener(MediaPlayer::start);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mMediaPlayer != null) {
mMediaPlayer.release();
}
if (mVideoTextureManager != null) {
mVideoTextureManager.release();
}
RootEGLManager.getInstance().release();
}
}
架构优势总结
- 零拷贝 (Zero-Copy):视频解码器直接将数据写入显存(OES纹理),View A 和 View B 直接读取同一块显存进行渲染。整个过程 CPU 完全不参与像素搬运,性能极高。
- 完美的同步:利用 SurfaceTexture.setOnFrameAvailableListener 配合 GLSurfaceView.requestRender(),实现了视频帧驱动渲染。视频每解码一帧,屏幕才刷新一次,避免了无效的 GPU 绘制。
- 各自独立的后处理:虽然 A 和 B 共享了同一个视频纹理,但它们有各自的 VideoRenderer。这意味着我们可以给 A 加一个黑白滤镜(修改 A 的 Fragment Shader),给 B 加一个镜像翻转(修改 B 的 Vertex Shader),两者互不干扰!
SurfaceView和TextureView适配
当前,我们仅仅实现了GLSurfaceView的渲染性能优化,那SurfaceView和TextureView是否也可以优化呢?
其实是支持的,在很多音视频应用中,几乎全都是用普通的 SurfaceView 或 TextureView,很少是 GLSurfaceView。
之所以会有这个疑问,是因为前面的代码使用了 GLSurfaceView 提供的便捷 API (setEGLContextFactory) 来实现共享。普通的 SurfaceView 和 TextureView 确实没有这个 API。
但是,GLSurfaceView 本质上只是 SurfaceView + 一个自带的后台 EGL 线程。 只要我们自己接管 EGL 的初始化和线程管理,就可以把任何共享的 OpenGL 画面渲染到普通的 SurfaceView 或 TextureView 上。
方案
在普通的 View 上实现星形共享架构。
核心原理:万物皆可 eglCreateWindowSurface 在 OpenGL ES 的世界里,它根本不知道什么是 SurfaceView 或 TextureView。它只认识一个东西:NativeWindow(原生窗口)。
在 Android 中,无论是 SurfaceView 提供的 Surface,还是 TextureView 提供的 SurfaceTexture,都可以作为 NativeWindow 传给 EGL,用来创建一个 EGLSurface(OpenGL 的画布)。
SurfaceView:通过 surfaceHolder.getSurface() 获取。 TextureView:通过 onSurfaceTextureAvailable 回调拿到 SurfaceTexture,然后 new Surface(surfaceTexture) 获取。 如何实现?(自定义 EGL 渲染线程) 既然不用 GLSurfaceView,我们需要为 View A 和 View B 各自写一个简单的后台渲染线程。这个线程接收 Root Context 和目标 View 的 Surface。
- 编写一个通用的自定义 GL 渲染线程 这个线程的核心任务是:拿着 Root Context 作为共享源,拿着 View 的 Surface 作为画布,建立自己的 EGL 环境并循环渲染。
kotlin
public class CustomGLRenderThread extends Thread {
private Surface mSurface; // 来自 SurfaceView 或 TextureView
private EGLContext mRootContext; // 我们的星形架构中心节点
private VideoRenderer mRenderer; // 之前写的渲染逻辑
private boolean isRunning = true;
private final Object mLock = new Object();
private boolean mHasNewFrame = false;
public CustomGLRenderThread(Surface surface, EGLContext rootContext) {
this.mSurface = surface;
this.mRootContext = rootContext;
this.mRenderer = new VideoRenderer();
}
@Override
public void run() {
// 1. 初始化 EGL 环境 (核心:共享 RootContext,绑定目标 Surface)
EGLDisplay display = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY);
EGL14.eglInitialize(display, null, 0, null, 0);
// 获取配置 (省略具体的 chooseConfig 代码,需与 RootContext 保持一致)
EGLConfig config = getConfig(display);
// 【关键点 1】:创建 Context 时,传入 mRootContext 作为共享上下文!
int[] ctxAttribs = { EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, EGL14.EGL_NONE };
EGLContext myContext = EGL14.eglCreateContext(display, config, mRootContext, ctxAttribs, 0);
// 【关键点 2】:将 Android 的 Surface 变成 OpenGL 的画布
int[] surfaceAttribs = { EGL14.EGL_NONE };
EGLSurface eglSurface = EGL14.eglCreateWindowSurface(display, config, mSurface, surfaceAttribs, 0);
// 绑定当前线程
EGL14.eglMakeCurrent(display, eglSurface, eglSurface, myContext);
// 2. 初始化渲染器 (编译着色器等)
mRenderer.onSurfaceCreated(null, null);
// 假设宽高已知,或者通过外部传入
mRenderer.onSurfaceChanged(null, 1080, 1920);
// 3. 进入渲染循环
while (isRunning) {
synchronized (mLock) {
while (!mHasNewFrame && isRunning) {
try {
mLock.wait(); // 等待视频帧到来的唤醒信号
} catch (InterruptedException e) {
e.printStackTrace();
}
}
mHasNewFrame = false;
}
if (!isRunning) break;
// 绘制画面 (直接读取共享的 OES 纹理)
mRenderer.onDrawFrame(null);
// 【关键点 3】:将画好的内容提交到屏幕显示!
// 这相当于 GLSurfaceView 内部自动调用的方法
EGL14.eglSwapBuffers(display, eglSurface);
}
// 4. 退出清理
EGL14.eglMakeCurrent(display, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT);
EGL14.eglDestroySurface(display, eglSurface);
EGL14.eglDestroyContext(display, myContext);
mSurface.release();
}
// 供外部 (VideoTextureManager) 调用,通知有新画面了
public void requestRender(int textureId, float[] matrix) {
mRenderer.updateFrame(textureId, matrix);
synchronized (mLock) {
mHasNewFrame = true;
mLock.notifyAll(); // 唤醒渲染线程
}
}
public void release() {
isRunning = false;
synchronized (mLock) {
mLock.notifyAll();
}
}
}
- 在 Activity 中结合普通的 SurfaceView / TextureView 使用 现在,我们可以完全抛弃 GLSurfaceView 了。
java
public class VideoSplitActivity extends AppCompatActivity {
private SurfaceView surfaceViewA;
private TextureView textureViewB;
private CustomGLRenderThread threadA;
private CustomGLRenderThread threadB;
private VideoTextureManager mVideoTextureManager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_split);
surfaceViewA = findViewById(R.id.surface_view_a);
textureViewB = findViewById(R.id.texture_view_b);
EGLContext rootContext = RootEGLManager.getInstance().getRootContext();
// 监听 SurfaceView A 的创建
surfaceViewA.getHolder().addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
// 拿到 Surface,启动 A 的专属渲染线程
threadA = new CustomGLRenderThread(holder.getSurface(), rootContext);
threadA.start();
}
// ... 省略 changed 和 destroyed
});
// 监听 TextureView B 的创建
textureViewB.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() {
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
// 拿到 SurfaceTexture 包装成 Surface,启动 B 的专属渲染线程
Surface bSurface = new Surface(surface);
threadB = new CustomGLRenderThread(bSurface, rootContext);
threadB.start();
}
// ... 省略其他回调
});
// 初始化视频纹理管理器 (和之前一样)
mVideoTextureManager = new VideoTextureManager((textureId, matrix) -> {
// 视频解码出新帧了,通知 A 和 B 的线程去画!
if (threadA != null) threadA.requestRender(textureId, matrix);
if (threadB != null) threadB.requestRender(textureId, matrix);
});
// ... 初始化 MediaPlayer 绑定 mVideoTextureManager.getSurface()
}
}
虽然写 CustomGLRenderThread 看起来代码变多了,但它带来了巨大的好处:
- 生命周期解耦:GLSurfaceView 的 EGL Context 生命周期死死绑定在 View 上。View 销毁(比如切后台、屏幕旋转),Context 就没了,纹理全得重新加载。自己管理 EGL 线程,可以让 EGL Context 活在后台,View 销毁时只销毁 EGLSurface,View 重建时重新 eglCreateWindowSurface 即可,实现真正的无缝黑屏切换。
- 支持 TextureView:GLSurfaceView 只能基于 SurfaceView。如果需要对视频画面做复杂的 View 动画(平移、缩放、透明度、放进 ScrollView 里滑动),SurfaceView 表现很差(会穿透、黑边),而 TextureView 表现完美。自己管理 EGL,就可以轻松把画面渲染到 TextureView 上。
- 精准的音视频同步: 在while(isRunning) 循环中,可以极其精确地控制 eglSwapBuffers 的调用时机,配合 Choreographer (VSync 信号) 和音频时间戳,实现完美的音视频同步。