## 从源码角度剖析 Android 系统 EGL 及 GL 线程 文/华峥 从事 OpenGL ES 相关开发的技术人员,常常会对一些问题感到困惑,例如 GL 线程究竟是什么?为什么在这个 GL 线程申请的 texture 不能在另外一个 GL 线程使用?如何打破这种限制等。这些问题在我们团队中曾经十分让人困惑,因为在网上也找不到详细解释。这篇文章将从源码角度为大家讲解 EGL 的一些核心工作机制,以及 GL 线程的本质,帮助理解 EGL 及 OpenGL ES 在底层是如何动作的,并具体回答以下一些棘手而又很难搜到答案的问题: 1. GL 线程和普通线程有什么区别? 2. texture 所占用的空间是跟 GL 线程绑定的吗? 3. 为什么通常一个 GL 线程的 texture 等数据,在另一个 GL 线程没法用? 4. 为什么通常 GL 线程销毁后,texture 也跟着销毁了? 5. 不同线程如何共享 OpenGL 数据? ### OpenGL ES 绘图完整流程 首先来看使用 OpenGL ES 在手机上绘图的完整流程,这里为什么强调“完整流程”,难道平时用的都是不完整的流程?基本可以这么说。因为“完整流程”相当复杂,而 Android 系统把复杂的过程封装好了,开发人员接触到的部分是比较简洁易用的,一般情况下也不需要去关心 Android 帮我们封装好的复杂部分,因此才说通常我们所接触的 OpenGL ES 绘图流程都是“不完整”的。 以下是 OpenGL ES 在手机上绘图的完整流程: 1)获取显示设备 ``` EGL10 egl = (EGL10) EGLContext.getEGL(); EGLDisplay display = egl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY); ``` 这段代码的作用是获取一个代表屏幕的对象,即 EGLDisplay,传的参数是 EGL10.EGL _ DEFAULT _ DISPLAY,代表获取默认的屏幕,因为有些设备上可能不止一个屏幕。 2)初始化 ``` int[] version = new int[2]; egl.eglInitialize(display, version); ``` 这段代码的作用是初始化屏幕。 3)选择 config ``` egl.eglChooseConfig(display, attributes, null, 0, configNum); ``` 这段代码的作用是选择 EGL 配置, 即可以自己先设定好一个期望的 EGL 配置,比如 RGB 三种颜色各占几位,可以随便配。因为 EGL 可能无法满足所有要求,这时,它会返回一些与你的要求最接近的配置供选择。 4)创建 Context ``` EGLContext context = egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT, attrs); ``` 这段代码的作用就是用从上一步 EGL 返回的配置列表中选择一种配置,用来创建 EGL Context 。 5)获取 Surface ``` EGLSurface surface = egl.eglCreateWindowSurface(display, config, surfaceHolder, null); ``` 这段代码的作用是获取一个 EGLSurface,可以把它想象成是一个屏幕对应的内存区域。注意,这里有一个参数 surfaceHolder,它对应着 GLSurfaceView 的 surfaceHolder。 6)将渲染环境设置到当前线程 ``` egl.eglMakeCurrent(display, surface, surface, contxt); ``` 这段代码的作用是将渲染环境设置到当前线程,相当于让其拥有了 Open GL 的绘图能力。为什么做了这步操作线程就拥有了 Open GL 的绘图能力?下文会具体讲解。 接下来就是绘图逻辑了: ``` loop: { 7. 画画画画画画画... 8. 交换缓冲区,让绘图的缓冲区显示出来 egl.eglSwapBuffers(display, surface); } ``` 以上步骤,对于不常接触 EGL 的开发人员也许比较陌生。让我们来看看比较熟悉的 GLSurfaceView,看它里面和刚说的那一堆乱七八糟的东西有什么关系。 ### GLSurfaceView 内部的 EGL 相关逻辑 查看 GLSurfaceView 的源码,可以看见里面有一个叫作 GLThread 的类,即所谓的“GL 线程”,如图1所示。 图1  GLThread类 图1 GLThread 类 可以看到,虽然它名叫 GLThread,但也是从普通的 Thread 类继承而来,理论上就是一个普通的线程,为什么它拥有 OpenGL 绘图能力?继续往下看,里面最重要的部分就是 guardedRun() 方法,让我们来看一下 guardedRun() 方法里有什么以及大致做的事情: ``` while() { … mEglHelper.start(); // 对应“完整流程”中的(1)(2)(3)(4) … mEglHelper.createSurface(); // 对应“完整流程”中的(5)(6) … 回调Renderer的onSurfaceCreated() … 回调Renderer的onSurfaceChanged () … 回调Renderer的onDrawFrame() // 对应“完整流程”中的(7) … mEglHelper.swap(); // 对应“完整流程”中的(8) … } ``` 仔细阅读 guardedRun() 的源码会发现,里面做的事情和之前说的“完整流程”都能一一对应,其中还有我们非常熟悉的 onSurfaceCreated()、onSurfaceChanged() 和 onDrawFrame() 这三个回调。而一般情况下,我们使用 OpenGL 绘图,就是在 onDrawFrame() 回调里绘制的,完全不用关心“完整流程”中的复杂步骤,这就是前文为什么说“完整流程”相当复杂。而 Android 系统帮我们把复杂的过程封装好了,所接触到的部分是比较简洁易用的,一般情况下也无需去关心 Android 已经封装好的复杂部分。 至此,得到一个结论,那就是所谓的 GL 线程和普通线程没有什么本质的区别。它就是一个普通的线程,只不过按照 OpenGL 绘图的完整流程正确地操作了下来,因此它有 OpenGL 的绘图能力。那么,如果我们自己创建一个线程,也按这样的操作方法,那我们也可以在自己创建的线程里绘图吗?当然可以! ### EGL 如何协助 OpenGL 我们先随便看一下 OpenGL 的常用方法,例如最常用的 GLES2.0.glGen texture s() 和 GLES2.0.glDelete texture s(): ``` // C function void glGenTextures ( GLsizei n, GLuint *textures ) public static native void glGenTextures( int n, int[] textures, int offset ); // C function void glDeleteTextures ( GLsizei n, const GLuint *textures ) public static native void glDeleteTextures( int n, int[] textures, int offset ); ``` 可以看到,都是 native 的方法,并且是静态的,看起来和 EGL 没有关系,那它怎么知道是 GL 线程去调的,还是普通线程去调的?又是怎么把 GLES2.glDelete texture s() 和 GLES2.0.glGen texture s() 对应到正确的线程上?我们再来看看底层的源码。 图2  glGen texture s方法底层实现 图2 glGen textures 方法底层实现 如图2、图3所示,在底层,它会去拿一个 context,实际上这个 context 就是保存在底层的 EGL Context ,而这个 EGL Context 是 Thread Specific 的。什么是 Thread Specific?就是说,不同线程去拿,得到的 EGL Context 可能不一样,这取决于给这个线程设置的 EGL Context 是什么。可以想象成每个线程都有一个储物柜,去里面拿东西能得到什么,取决于你之前给这个线程在储物柜里放了什么东西。这是一个形象化的比喻,代码时的实现其实是给线程里自己维护了一个存储空间,相当于储物柜。因此每个线程去拿东西时,只能拿到自己储物柜里的,因此是 Thread Specific 的。 图3  底层用了thread specific的方式保存context 图3 底层用了 thread specific 的方式保存 context 那么,是什么时候把 EGL Context 放到线程的储物柜里去的呢?还记得前面提到过 eglMakeCurrent() 吗?我们来看看它的底层,如图4所示。 图4  eglMakeCurrent()底层实现 图4 eglMakeCurrent() 底层实现 可以看到,在调用 eglMakeCurrent() 时,会通过 setGLThreadSpecific() 将传给 eglMakeCurrent() 的 EGL Context 在底层保存一份到调用线程的储物柜里。 我们再来仔细看 eglMakeCurrent() 里一步一步都做了什么,这对于理解线程绑定 OpenGL 渲染环境至关重要。 ``` static int makeCurrent(ogles_context_t* gl) { ogles_context_t* current = (ogles_context_t*)getGlThreadSpecific(); if (gl) { egl_context_t* c = egl_context_t::context(gl); if (c->flags & egl_context_t::IS_CURRENT) { if (current != gl) { // it is an error to set a context current, if it's already // current to another thread return -1; } } else { if (current) { // mark the current context as not current, and flush glFlush(); egl_context_t::context(current)->flags &= ~egl_context_t::IS_CURRENT; } } if (!(c->flags & egl_context_t::IS_CURRENT)) { // The context is not current, make it current! setGlThreadSpecific(gl); c->flags |= egl_context_t::IS_CURRENT; } } else { if (current) { // mark the current context as not current, and flush glFlush(); egl_context_t::context(current)->flags &= ~egl_context_t::IS_CURRENT; } // this thread has no context attached to it setGlThreadSpecific(0); } return 0; } ``` 归纳下来就是以下几点: 1. 获取当前线程的 EGL Context current(底层用ogles _ context _ t存储); 2. 判断传递过来的 EGL Context gl 是不是还处于 IS _ CURRENT 状态; 3. 如果 gl 是 IS _ CURRENT 状态但又不是当前线程的 EGL Context,则 return; 4. 如果 gl 不是 IS _ CURRENT 状态,将 current 置为非 IS _ CURRENT 状态; 5. 将 gl 置为 IS _ CURRENT 状态,并将 gl 设置为当前线程的 Thread Local 的 EGL Context。 因此可以得出两点结论: 1. 如果一个 EGL Context 已被一个线程 makeCurrent(),它不能再次被另一个线程 makeCurrent(); 2. makeCurrent() 另外一个 EGL Context 后会与当前 EGL Context 脱离关系。 继续看 GLES2.0.glGentextures(),如下所示,给出了 glGentextures() 底层的一些调用关系。 ``` void glGenTextures(GLsizei n, GLuint *textures) { ogles_context_t* c = ogles_context_t::get(); if (n<0) { ogles_error(c, GL_INVALID_VALUE); return; } // generate unique (shared) texture names c->surfaceManager->getToken(n, textures); } class EGLSurfaceManager : public TokenManager { public: EGLSurfaceManager(); ~EGLSurfaceManager(); // protocol for sp<> inline void incStrong(__const void* id) const; inline void decStrong(__const void* id) const; typedef void weakref_type; sp createTexture(GLuint name); sp removeTexture(GLuint name); sp replaceTexture(GLuint name); void deleteTextures(GLsizei n, const GLuint *tokens); sp texture(GLuint name); private: mutable int32_t mCount; mutable Mutex mLock; KeyedVector< GLuint, sp > mTextures; }; status_t TokenManager::getToken(GLsizei n, GLuint *tokens) { Mutex::Autolock _l(mLock); for (GLsizei i=0 ; i ``` 而从图5可以看到调了 glGen textures(),分配的 texture 放在哪里了。 图5  纹理id在底层的保存位置 图5 纹理 id 在底层的保存位置 但事实上,这其实没那么重要,因为这里只是存了一个 texture id,并不是 texture 真正所占的存储空间。因此调 glGentextures() 方法时,也没有指定要多大的 texture。那么,texture 真正所占的存储空间在什么地方呢?那就要看给 texture 分配存储空间的方法了,即 glTexImage2D() 方法,如图6所示。 图6  glTexImage2D方法的底层实现 图6 glTexImage2D 方法的底层实现 这时,再看图7,展示了 texture 所占用的存储空间的空间放在什么地方。 图7  存储位置 图7 存储位置 到这里,又有了一个结论:本质上 texture 是跟 EGL Context 绑定的,而非与 GL 线程绑定。因此,当 GL 线程销毁时,如果不销毁 EGL Context,则 texture 没有销毁。我们可能常常听说这样一种说法:GL 线程销毁后,GL 的上下文环境就被销毁了,在其中分配的 texture 也自然就被销毁。这种说法会让人误以为 texture 是跟 GL 线程绑定在一起的,误认为 GL 线程销毁后 texture 也自动销毁,其实 GL 线程并不会自动处理 texture 的销毁,而需要手动销毁。有人想问了,我们平时用 GLSurfaceView 时,当 GLSurfaceView 销毁时,如果没有 delete 掉分配的 texture,这些 texture 也会自己释放,这是怎么回事?这是因为 GLSurfaceView 销毁时帮你把 texture 销毁了,我们来看看 GLSurfaceView 里相关的代码,如图8所示。 图8  GLSurfaceView相关代码 图8 GLSurfaceView 相关代码 因此,如果你自己创建了一个 GL 线程,当 GL 线程销毁时,若不主动销毁 texture,那么 texture 实际上是不会自动销毁的。 ### 总结 下面总结一下本文,回答文章开头提出的问题: ##### GL 线程和普通线程有什么区别? 答:没有本质区别,只是它按 OpenGL 的完整绘图流程正确地跑了下来,因而可以用 OpenGL 绘图。 ##### texture 所占用的空间是跟 GL 线程绑定的吗? 答:跟 EGL Context 绑定,本质上与线程无关。 ##### 为什么通常一个 GL 线程的 texture 等数据,在另一个 GL 线程没法用? 答:因为调用 OpenGL 接口时,在底层会获取 Thread Specific 的 EGL Context,因此通常情况下,不同线程获取到的 EGL Context 是不一样的,而 texture 又放在 EGL Context 中,因此获取不到另外一个线程创建的 texture 等数据。 ##### 为什么通常 GL 线程销毁后,texture 也跟着销毁了? 答:因为通常是用 GLSurfaceView,它销毁时显式调用了 eglDestroyContext() 销毁与之绑定的 EGL Context,从而其中的 texture 也跟着被销毁。 ##### 不同线程如何共享 OpenGL 数据? 答:在一个线程中调用 eglCreateContext() 里传入另一个线程的 EGL Context 作为 share context,或者先让一个线程解绑 EGL Context,再让另一个线程绑定这个 EGL Context。