ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

不依赖GLSurfaceView,基于EGL构建Android自定义渲染管线

不依赖GLSurfaceView,基于EGL构建Android自定义渲染管线 做过Android图形开发的人十有八九对“渲染图像”的第一印象都是GLSurfaceView——一个可渲染OpenGL ES的View拿来就能用自带渲染线程、EGL环境、生命周期管理写个Renderer就能输出一个三角形或者贴张图。但真要在实际项目里做深了比如要同时管理多路相机预览、要在视频录制时插入滤镜、要把渲染结果拿去编码或者截帧GLSurfaceView就明显不够用了。它把核心流程封装得太“死”线程模型、渲染节奏和生命周期都替你定好了一旦想改反而是在跟框架较劲。我日常做Camera2滤镜、离屏渲染、视频帧处理比较多早就把GLSurfaceView从主链路里移除了自己基于OpenGL ES EGL搭了一套渲染框架。这篇文章就聊清楚一件事不用GLSurfaceView图像该怎么渲染。内容包括EGL环境的搭建、渲染线程的设计、离屏渲染的思路、以及不用GLSurfaceView时常见的一堆坑。想搞清底层渲染链路、或者遇到“SurfaceView黑屏”“纹理出不来”这类问题的人建议认真看完尤其是想自己掌控渲染管线而不是被框架绑住手脚的开发者。1. 先把GLSurfaceView干的事拆出来看1.1 它给你做了三件“倒忙”很多人以为GLSurfaceView就是“一个View”其实它内部做了三件独立的事创建并维护GL线程、初始化EGL环境、处理Surface的生命周期。这三个环节本质上是完全独立的可以拆开单独管理。EGL是OpenGL ES和操作系统窗口系统之间的中间层作用是建立一个从OpenGL ES到屏幕上的一块可绘制区域Surface的桥梁。GLSurfaceView所做的就是在View创建时自动帮你拿到Display、Config、Context、Surface然后在后台线程里不断执行“draw - swapBuffers”的循环。这省事确实省事但也牺牲了灵活性。举个例子我在做相机预览时曾经需要在Activity里同时显示两个实时画面并每帧做特效叠加。如果用GLSurfaceView就得塞两个GLSurfaceView在View树里每个都带一个自己的EGLContext内存开销和同步成本都会翻倍而且两个GLSurfaceView之间跨上下文同步纹理非常麻烦。这种局面下自控渲染链路才是靠谱方案。不止如此GLSurfaceView在“连续渲染”和“按需渲染”之间切换起来也很别扭。它默认是RENDERMODE_CONTINUOUSLY也就是一旦setRenderer它就会无限循环调用onDrawFrame哪怕画面根本没变化也照样刷非常耗电。改成RENDERMODE_WHEN_DIRTY之后又要手动调requestRender多线程写起来也很容易错。总之GLSurfaceView适合“快速演示Demo”不适合“精细控制渲染管线的产品”。1.2 绕过它之后你获得了什么不用GLSurfaceView之后核心是要自己管理EGL环境和渲染线程换句话说你获得了三个以前被框架占用的自由度。第一个自由度是线程模型。你可以把渲染放到任意线程里跟相机回调、视频编码线程做更好的同步。比如相机每来一帧你把该帧上传为纹理然后通知渲染线程绘制渲染线程工作在HandlerThread中就天然避免了对UI线程的阻塞和跨线程传递的复杂度。第二个自由度是Surface类型。GLSurfaceView默认绑定到View内部的Surface但自建EGL环境后你可以绑定到SurfaceView的Surface也可以绑定到TextureView的SurfaceTexture甚至可以绑定到离屏的EGL surface实现纯GPU计算输出完全不需要上屏。这对于“拿OpenGL做后处理、再送去编码”的场景是刚需。第三个自由度是渲染节奏。连续渲染还是按需渲染、要不要等到垂直同步、要不要限制帧率全都可以自己定。拿视频编辑App来说很多渲染操作并不需要60fps刷新静帧时渲染线程直接休眠能把整机功耗压下来一大截。从实现层面讲只需要一个标准的EGL流程获取Display、初始化、选择Config、创建Context、创建Surface最后把二者绑定在当前线程并开始绘制。下一节就该说清楚这条链路里的每个环节分别是什么、要注意什么。2. EGL核心链路你必须亲手打通的五个节点2.1 Display、Config、Context、Surface分别是什么EGL是OpenGL ES与窗口系统之间的协议层。放到Android上它负责把OpenGL ES指令输出到BufferQueue也就是系统图形合成器能消费的一帧帧图像。理解EGL最有效的方式是把它的核心对象当成人。EGLDisplay负责连接——它代表“跟系统显示服务建立的会话”所有EGL操作都基于这个display句柄。Android平台上一般传入EGL_DEFAULT_DISPLAY就能拿到可用的display。EGLConfig负责匹配——它描述了一个Surface的能力组合包括颜色位数、深度位数、模板位数、是否支持多采样等。OpenGL ES本身并不关心Surface长什么样是EGLConfig在“OpenGL上下文”和“渲染目标”之间做能力匹配的。你不可能用一个没有深度缓冲的Surface去开一个要求深度的上下文。所以创建EGLSurface时它必须匹配创建Context时的Config。EGLContext就是上下文本身保存着OpenGL ES的状态机当前着色器程序、纹理绑定、混合开关、视口尺寸等。同一个Context可以被用于多个Surface但同一时刻只能在一个线程上“当前”Current。EGLSurface则是实际渲染的“画布”。它可以是窗口Window Surface对应SurfaceView或SurfaceTexture也可以是离屏的PBuffer Surface或Bitmap Surface。平时说的“在屏幕上画图”本质是渲染到这个Surface然后由系统把缓冲上屏。2.2 一帧渲染的完整生命周期每一次绘制哪怕只是清屏然后swapBuffers也隐含了完整的生命周期eglGetDisplay - eglInitialize - eglChooseConfig - eglCreateContext - eglCreateWindowSurface或者eglCreatePbufferSurface- eglMakeCurrent - 调用GL命令 - eglSwapBuffers。eglMakeCurrent是关键步骤它把“上下文”和“目标Surface”绑定到“当前线程”。这句话反复出现的意义在于OpenGL ES不是线程安全的你只能在你MakeCurrent的那个线程里发起GL调用。跨线程调用GL命令轻则无效重则直接导致黑屏或崩溃。eglSwapBuffers负责交换前后缓冲。对窗口Surface来说交换完成后这一帧就被提交到BufferQueue系统会把图像合成到屏幕上。对离屏Surface来说交换只是把已经画好的内容留在纹理里供后续读取并不会真的上屏。从编码习惯看入门者很容易漏掉一件事创建EGLSurface的代码和实际进行绘制的代码必须在同一个线程执行。我的经验是最好把EGL初始化、绘制、销毁全部封装在一个自定义渲染线程内部对外只暴露异步方法比如postInitSurface、postDrawFrame。这个设计能避免大量莫名其妙的并发症。2.3 Android上EGL的版本与选择建议Android上OpenGL ES 3.0从API 18开始普及OpenGL ES 3.1从API 24开始稳定支持3.2在API 26后也逐步铺开。现在做新项目我一般会直接创建OpenGL ES 3.0的Context也就是在eglCreateContext的attribute里传EGL_CONTEXT_CLIENT_VERSION3。低版本的设备也可以降级但降级时必须保证着色器、纹理内部格式和API调用都不超出版本限制。实际开发中我采用“尽量要高失败再降”的策略先尝试创建3.2如果返回EGL_BAD_MATCH就尝试3.0再不行就回落到2.0。这个策略在老爷机面前也很稳。当然EGL本身的初始化也有小差异Android 10以后EGL 1.5普及老的EGL 1.4代码也一样能跑大多数项目无需特殊区分版本。真正容易翻车的反而是格式匹配后面单独开一节讲。3. 四个替代方案按场景选而不是按喜好选3.1 方案ASurfaceView 自管EGL上下文性能优先SurfaceView是系统中性能最高、最贴近合成器的一种方案。它本质是一块独立的Surface不参与View树常规绘制也没有View绘制的裁剪和透明度处理所以它适合全屏或区域占用的高性能渲染场景。用SurfaceView替代GLSurfaceView思路是把GLSurfaceView之前替你做的EGL初始化、渲染线程、Surface回调全部拆开。大致的流程是监听SurfaceHolder.Callback在surfaceCreated时拿到Surface然后让渲染线程创建EGLContext并用eglCreateWindowSurface绑定到这个Surface之后进入自绘循环每帧绘制后swapBuffers。这套方案的好处是性能极好不经过View层的onDraw直接输出到专属缓冲。坏处是叠加UI困难。要在SurfaceView上放Button或文字必须额外叠加一层悬浮窗口或者TextureView结构会比较别扭。所以方案A的核心适用场景是“画面为主、UI为辅”的地方比如相机取景、视频播放、地图图层渲染。3.2 方案BTextureView 自管EGL上下文UI融合优先TextureView走的是另一条路它把图像先渲染进SurfaceTexture再交给View架构作为纹理绘制。所以TextureView创造性地解决了“OpenGL渲染与普通View共存”的问题画面可以像普通View一样旋转、缩放、加圆角也可以跟其他View做层级重叠。自建EGL时TextureView的监听方式更简单在onSurfaceTextureAvailable回调里拿到一个SurfaceTexture然后用这个SurfaceTexture去创建EGLSurface。之后绘制线程的工作方式与SurfaceView方案几乎一致只是SurfaceHolder的回调换成了TextureView.SurfaceTextureListener。很多人初次接触时会疑惑SurfaceTexture明明是“生产纹理”的东西为什么能当作EGL窗口原因在于SurfaceTexture内部也有BufferQueue由生产者写入帧再由消费者通常是ViewRenderer消费。EGL可以把SurfaceTexture当作一个“离屏消费者”来绑定所以你可以用它创建一个EGLSurface在它上面draw从而把渲染结果直接送进View体系。方案B的性能比SurfaceView差一些因为纹理最终还要经过View的合成。但换来的是UI融合的灵活性。我个人的建议是静态UI动态画面或者UI层级比较复杂时优先考虑TextureView。3.3 方案C完全离屏渲染后台处理优先有些场景压根不需要显示到屏幕。比如把一个视频帧经过OpenGL特效后输出为另一张纹理再交给MediaCodec编码或者对一帧Sensor数据进行实时的边缘提取。这时你需要一个“不可见的EGL环境”。实现上有两种常见做法。一种是创建EGL pbuffer surface把这个离屏面当作EGLSurface绘制后调用glReadPixels把像素读出来。这种方法简单但glReadPixels是同步读取极其耗时只适合尺寸不大的图像。另一个更推荐的做法是FBOFramebuffer Object。在OpenGL ES内部创建一个与纹理关联的Framebuffer所有绘制指令直接输出到纹理。这样最终成果天然是一张GPU纹理不需要CPU参与后续再交给滤镜、缩放或者编码链路都能保持高效。我处理视频特效时最常用这种方式先计算出特效结果纹理再把这个纹理绑定到GL_TEXTURE_EXTERNAL_OES上交给相机管线继续消费。方案C的要点在于即使做离屏渲染你仍需要一个EGLContext来承载OpenGL状态。没有窗口Surface是可以的但必须有Context否则着色器编译、纹理创建都无法进行。所以实践上通常是“初始化一个不可显示的Context 一个FBO”专门跑后台任务。3.4 方案D配合Camera2与SurfaceTexture的实时预览流这是把相机和OpenGL串起来的经典套路。Camera2虽然可以直接向Surface预览但你拿不到那一帧去做特效。常规做法是让相机的输出端指向一个SurfaceTexture当有帧到达时会得到通知你再用OpenGL采样这个纹理并处理最后把结果绘制到显示Surface或编码Surface。不用GLSurfaceView时这个链路是这样组织的Camera2通过SurfaceTexture创建了预览输入源渲染线程维护自己的EGLContext并通过eglMakeCurrent把Context与“显示用的EGLSurface”绑定当收到相机帧回调时从纹理里采样、处理、绘制最后swapBuffers。这种“输入SurfaceTexture 输出EGLSurface”的结构能完成从相机到屏幕的完整闭环。这个方案里一个很容易被忽略的参数是setDefaultBufferSize。很多黑屏问题都出在这SurfaceTexture没有指定默认缓冲尺寸导致BufferQueue缓冲区尺寸是0或错误大小。你在创建SurfaceTexture之后务必调用setDefaultBufferSize(w, h)尺寸要和预览或输出一致。另外相机帧使用的是外部纹理GL_TEXTURE_EXTERNAL_OES需要用对应的扩展采样器samplerExternalOES而不是普通的sampler2D。3.5 四个方案怎么选一张表说清楚方案渲染目标最终呈现方式适用场景性能开销关键坑SurfaceView EGLSurface系统合成器直接合成相机取景、视频播放、游戏最低Surface创建时机、叠加UI困难TextureView EGLSurfaceTextureView体系纹理绘制UI融合型渲染、直播推流画面叠加中等需要设DefaultBufferSize、合成开销离屏FBO/Pbuffer纹理/像素缓冲区不上屏供编码或读取视频特效、截帧、图片处理中高glReadPixels效率低、需注意Context存在Camera2SurfaceTextureEGLSurfaceTexture可上屏亦可编码相机实时滤镜、相机特效高纹理类型OES、时间戳同步、帧率控制4. 实操手写一个最小可用的EGL渲染框架4.1 EGL环境工具类封装有了前面的理论这里直接给一个可复制的Java实现代码里我刻意删掉了无关业务只保留最干净的EGL骨架方便套用到自己的项目里。public class EglCore { private EGLDisplay mDisplay; private EGLConfig mConfig; private EGLContext mContext; public EglCore() { mDisplay EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); if (mDisplay EGL14.EGL_NO_DISPLAY) { throw new RuntimeException(eglGetDisplay failed); } int[] version new int[2]; if (!EGL14.eglInitialize(mDisplay, version, 0, version, 1)) { throw new RuntimeException(eglInitialize failed); } mConfig chooseConfig(); int[] attribs new int[] { EGL14.EGL_CONTEXT_CLIENT_VERSION, 3, EGL14.EGL_NONE }; mContext EGL14.eglCreateContext(mDisplay, mConfig, EGL14.EGL_NO_CONTEXT, attribs, 0); if (mContext EGL14.EGL_NO_CONTEXT) { throw new RuntimeException(eglCreateContext failed); } } private EGLConfig chooseConfig() { int[] attribs new int[] { EGL14.EGL_RED_SIZE, 8, EGL14.EGL_GREEN_SIZE, 8, EGL14.EGL_BLUE_SIZE, 8, EGL14.EGL_ALPHA_SIZE, 8, EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, EGL14.EGL_NONE }; EGLConfig[] configs new EGLConfig[1]; int[] numConfigs new int[1]; if (!EGL14.eglChooseConfig(mDisplay, attribs, 0, configs, 0, 1, numConfigs, 0)) { throw new RuntimeException(eglChooseConfig failed); } return configs[0]; } public EGLSurface createWindowSurface(Object surface) { int[] attribs new int[] { EGL14.EGL_NONE }; EGLSurface result EGL14.eglCreateWindowSurface(mDisplay, mConfig, surface, attribs, 0); if (result EGL14.EGL_NO_SURFACE) { throw new RuntimeException(eglCreateWindowSurface failed: 0x Integer.toHexString(EGL14.eglGetError())); } return result; } public void makeCurrent(EGLSurface surface) { if (!EGL14.eglMakeCurrent(mDisplay, surface, surface, mContext)) { throw new RuntimeException(eglMakeCurrent failed: 0x Integer.toHexString(EGL14.eglGetError())); } } public boolean swapBuffers(EGLSurface surface) { return EGL14.eglSwapBuffers(mDisplay, surface); } public void release() { if (mDisplay ! EGL14.EGL_NO_DISPLAY) { EGL14.eglMakeCurrent(mDisplay, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_SURFACE, EGL14.EGL_NO_CONTEXT); EGL14.eglDestroyContext(mDisplay, mContext); EGL14.eglReleaseThread(); EGL14.eglTerminate(mDisplay); } mDisplay EGL14.EGL_NO_DISPLAY; mContext EGL14.EGL_NO_CONTEXT; } }创建Context时我把EGL_CONTEXT_CLIENT_VERSION传成了3这是EGL和OpenGL ES版本协商的关键。如果某些设备不支持3.0这里可能返回EGL_NO_CONTEXT需要降级重试。生产级代码一般要在EglCore外面包一层版本探测逻辑。4.2 渲染线程与Surface回调的串联有了EglCore接着是渲染线程。我习惯用HandlerThread来做GL线程因为这样渲染任务可以通过Handler串行投递不容易并发出错。public class RenderThread extends HandlerThread implements SurfaceHolder.Callback { private Handler mRenderHandler; private EglCore mEglCore; private EGLSurface mEglSurface; private Surface mWindowSurface; private volatile boolean mSurfaceValid; private volatile boolean mRunning; public RenderThread() { super(RenderThread); } Override protected void onLooperPrepared() { mRenderHandler new Handler(getLooper()); } public void startRenderThread(Surface surface, int width, int height) { if (!mRunning) { start(); mRunning true; } mRenderHandler.post(() - initSurface(surface, width, height)); } private void initSurface(Surface surface, int width, int height) { if (mEglCore null) { mEglCore new EglCore(); } mWindowSurface surface; // 这里第一次绑定的时候把EGLSurface创建好 mEglSurface mEglCore.createWindowSurface(surface); mEglCore.makeCurrent(mEglSurface); GLES20.glViewport(0, 0, width, height); mSurfaceValid true; } public void frameAvailable() { if (mRenderHandler null || !mSurfaceValid) return; mRenderHandler.post(this::drawFrame); } private void drawFrame() { if (!mSurfaceValid) return; // 此处替换成你的绘制逻辑清屏、绑定纹理、绘制三角形等 GLES20.glClearColor(0f, 0f, 0f, 1f); GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); // 省略着色器绑定与绘制指令 mEglCore.swapBuffers(mEglSurface); } public void stopRenderThread() { mSurfaceValid false; if (mRenderHandler ! null) { mRenderHandler.post(this::release); } mRunning false; } private void release() { if (mEglCore ! null) { mEglCore.makeCurrent(mEglSurface); if (mEglSurface ! null mEglSurface ! EGL14.EGL_NO_SURFACE) { mEglCore.release(); mEglSurface EGL14.EGL_NO_SURFACE; } } quitSafely(); } }调用端就简单了。在SurfaceView的holder回调里启动线程和传SurfaceSurfaceView surfaceView findViewById(R.id.surface_view); RenderThread renderThread new RenderThread(); surfaceView.getHolder().addCallback(renderThread);SurfaceHolder.Callback里的方法由UI线程调用所以启动线程、传Surface这些操作要放到Handler里切到渲染线程执行。上面的框架正是按这个思路写的。提示所有EGL调用都必须在同一个线程里执行。上面的HandlerThread方案把这层约束天然固化在architecture里比直接在onDraw里乱调更稳。4.3 一个完整的矩形纹理绘制片段很多初学者走到“EGL环境OK”这步就卡住了因为不知道怎么在这套环境里画图像。这里补一个最简单的贴图流程创建纹理、上传像素数据、绘制两个三角形把纹理映射到屏幕。private int mProgram; private int mTexId; private void setupTexture(int width, int height, byte[] pixels) { int[] texIds new int[1]; GLES20.glGenTextures(1, texIds, 0); mTexId texIds[0]; GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexId); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); // GL_RGBA像素格式width/height按实际图片尺寸pixels传原始位图数据 GLES20.glTexImage2D(GLES20.GL_TEXTURE_2D, 0, GLES20.GL_RGBA, width, height, 0, GLES20.GL_RGBA, GLES20.GL_UNSIGNED_BYTE, pixels); }这段代码要放在渲染线程执行且前提是已经执行过makeCurrent。否则纹理创建会返回0后面绑定会静默失败。我早期踩过一个非常典型的坑在Activity的onCreate里调用setupTexture因为此时渲染线程还没MakeCurrent结果纹理一直无效画面全黑。把纹理初始化的逻辑挪到渲染线程首帧之后问题立刻消失。4.4 与SurfaceTexture配合时的特殊写法如果绑定的是TextureView创建EGLSurface的入口参数不再是Surface对象而是一个SurfaceTexture。创建方式依然是eglCreateWindowSurface无非最后一个参数传入SurfaceTexture实例。但别忘了TextureView的SurfaceTexture还有几个配套动作。Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { this.mSurfaceTexture surfaceTexture; // 重要必须显式设置默认缓冲尺寸 surfaceTexture.setDefaultBufferSize(width, height); renderThread.startRenderThread(surfaceTexture, width, height); } Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) { renderThread.stopRenderThread(); return true; }忘记设置DefaultBufferSize会导致SurfaceTexture的buffer尺寸可能为0EGL创建Window Surface时很容易报EGL_BAD_MATCH。这个问题在GLSurfaceView封装下你感知不到因为GLSurfaceView内部已经通过EGLConfig和View尺寸帮你处理了绕开它之后就变成开发者必须自己维护的细节。5. 常见问题与排查技巧实录5.1 高频问题速查表现象直接原因解决方向黑屏但日志无GL错误EGLSurface未创建成功或未MakeCurrent检查surface是否有效、是否在渲染线程里执行EGL调用eglSwapBuffers返回falseSurface已失效多为SurfaceDestroyed后仍在绘制在surfaceDestroyed回调里停止渲染或同步等待渲染线程释放纹理显示为全白或乱码纹理格式或数据对齐不对检查GL_RGBA/GL_UNSIGNED_BYTE、GL_TEXTURE_WRAP设置画面上下颠倒图像坐标系统差异调整纹理坐标或矩阵圆角裁剪失效没有启用GLScissor或模板测试打开GLES20.glEnable(GLES20.GL_SCISSOR_TEST)多次创建EGLContext后崩溃新旧context资源冲突释放旧context再创建新context别在旧context上创建纹理SurfaceView上涂了一层黑底未设置SurfaceFormatSurfaceView holder.setFormat(PixelFormat.RGBA_8888)5.2 高价值排查心法日志里的错误码要先看EGL开GL调试时大多数人第一反应是看GLES20.glGetError()但我更建议先打印EGL错误码。EGL错误发生得比GL早错误码也更直白。常见如EGL_BAD_SURFACE0x300D表示Surface对象无效EGL_BAD_MATCH0x3009表示Surface和Config不匹配EGL_BAD_CONTEXT0x3006表示Context已经销毁。定位到EGL层很多“黑屏”问题可以砍掉一半排查时间。前阵子有个项目同一台设备在旧版本系统上正常在Android 14上却是黑屏。排查下来发现是ColorSpace相关设置没跟上新的系统要求EGLConfig里显示特定的Color空间属性。这种问题在框架文档里根本不会写清楚只能靠“版本差异 打印EGLConfig参数”来判断。所以环境初始化前后我一般都会把关键的Config参数和EGL错误码都打印一遍发布包保留这个日志开关也很值得。5.3 性能与功耗优化建议不用GLSurfaceView之后自建渲染线程会把“要不要渲染”“什么时候渲染”的决定权交给你。如果每帧图像数据不变就别连续swapBuffers。相机预览按需刷新即可超过屏幕刷新率的上限也没有意义。我用Choreographer.FrameCallback或者Camera2的ImageReader回调做节流把渲染触发时机校准到“确实有帧进来时”。另一个优化点是避免频繁创建和销毁EGLSurface。EGLSurface创建成本很高尤其是SurfaceView每次surfaceCreated时都重建。如果画面在一屏内反复横跳后果就是肉眼可见的卡顿。建议在Activity生命周期内复用Context和核心纹理Surface销毁时只释放EGLSurfaceSurface重新创建时再新建EGLSurface不重建Context。框架层面最好做一个引用计数防止释放时把还被占用的Context一起干掉。纹理对象的缓存也很重要。如果一个SurfaceView上要叠加多路视频画面可以为每路视频各申请一个纹理缓存复用纹理ID而不是每帧都glGenTextures。glGenTextures和glDeleteTextures都会打断GPU流水线热路径上反复执行是非常不划算的。写在最后的一点个人体会绕开GLSurfaceView之后我做的第一个能稳定跑通Camera2实时滤镜的版本里还是保留了GLSurfaceView的习惯总想在Activity的onResume里初始化EGL。这种惯性是最大的坑。后来我把所有EGL初始化统一收敛进渲染线程用Handler方式投递任务整个项目的渲染稳定性肉眼可见地提升了一个台阶。另一个小建议如果你只在个别页面要脱离GLSurfaceView输出几个特效帧没必要把整个EGL封装库都背进来。先复制一份最精简的EglCore RenderThread把一帧图像循环打通再逐步加工成自己的基础库。这套“最小可运行”的打法比一开始就设计一个万能框架要高效得多也少踩很多设计过度的坑。
返回列表