GLSurfaceView渲染过程详解

GLSurfaceView提供了下列特性:
1> 管理一个surface,这个surface就是一块特殊的内存,能直接排版到android的视图view上。
2> 管理一个EGL display,它能让opengl把内容渲染到上述的surface上。
3> 用户自定义渲染器(render)。
4> 让渲染器在独立的线程里运作,和UI线程分离。
5> 支持按需渲染(on-demand)和连续渲染(continuous)。
6> 一些可选工具,如调试。

 

概念:

Display(EGLDisplay) 是对实际显示设备的抽象。
Surface(EGLSurface)是对用来存储图像的内存区域FrameBuffer的抽象,包括Color Buffer, Stencil Buffer ,Depth Buffer.
Context (EGLContext) 存储OpenGL ES绘图的一些状态信息。

 

步骤:

获取EGLDisplay对象
初始化与EGLDisplay 之间的连接。
获取EGLConfig对象
创建EGLContext 实例
创建EGLSurface实例
连接EGLContext和EGLSurface.
使用GL指令绘制图形
断开并释放与EGLSurface关联的EGLContext对象
删除EGLSurface对象
删除EGLContext对象
终止与EGLDisplay之间的连接。

 

GLSurfaceView的绘制流程

由上图可知,GLSurfaceView的主要绘制过程都是在一个子线程中完成,即整个绘制最终都是guardenRun()中完成。在这个过程中完成了整个EGL绘制的所有步骤。

我把guardenRun()的大多数细节代码都删掉了,剩下一些精华:

 

[java] view plain copy

  1. private void guardedRun() throws InterruptedException {  
  2.   
  3.                while (true) {  
  4.                    synchronized (sGLThreadManager) {  
  5.                        while (true) {  
  6.   
  7.                            // Ready to draw?  
  8.                            if (readyToDraw()) {  
  9.                                // If we don't have an EGL context, try to acquire one.  
  10.                                if (! mHaveEglContext) {  
  11.                                    if (sGLThreadManager.tryAcquireEglContextLocked(this)) {  
  12.                                         mEglHelper.start();  
  13.                                    }  
  14.                                }  
  15.   
  16.                            sGLThreadManager.wait();  
  17.                        }  
  18.                    } // end of synchronized(sGLThreadManager)  
  19.   
  20.                    if (createEglSurface) {  
  21.                        if (mEglHelper.createSurface()) {  
  22.                           ...  
  23.                        }  
  24.                    }  
  25.   
  26.                    if (createGlInterface) {  
  27.                        gl = (GL10) mEglHelper.createGL();  
  28.                    }  
  29.   
  30.                    if (createEglContext) {  
  31.                        if (view != null) {  
  32.                            view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);  
  33.                        }  
  34.                    }  
  35.   
  36.                    if (sizeChanged) {  
  37.                        if (view != null) {  
  38.                            view.mRenderer.onSurfaceChanged(gl, w, h);  
  39.                        }  
  40.                        sizeChanged = false;  
  41.                    }  
  42.   
  43.                  
  44.                    if (view != null) {  
  45.                        view.mRenderer.onDrawFrame(gl);  
  46.                    }  
  47.                      
  48.                    int swapError = mEglHelper.swap();  
  49.        }  


其中mEglHelper.start():

 

 

 

 

[java] view plain copy

  1. public void start() {  
  2.     /* 
  3.      * Get an EGL instance 
  4.      */  
  5.     mEgl = (EGL10) EGLContext.getEGL();  
  6.   
  7.     /* 
  8.      * Get to the default display. 
  9.      */  
  10.     mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);  
  11.   
  12.     
  13.     mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay);  
  14.   
  15.     /* 
  16.     * Create an EGL context. We want to do this as rarely as we can, because an 
  17.     * EGL context is a somewhat heavy object. 
  18.     */  
  19.     mEglContext = view.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig);  
  20.   
  21. }  


mEglHelper.start()就完成了4步:

 

1,获取EGLDisplay对象

2,初始化与EGLDisplay 之间的连接。
3,获取EGLConfig对象
4,创建EGLContext 实例

请注意注解中提到createContext()创建的mEglContext是一个重量级对象,在创建的时候很耗资源,我们尽可能少的创建它。所以,在guardenRun()中我们做了对mEglContext的是否存在的判断:

 

[java] view plain copy

  1. if (! mHaveEglContext) {  
  2.                       if (sGLThreadManager.tryAcquireEglContextLocked(this)) {  
  3.                                         mEglHelper.start();  
  4.                                    }  
  5.                                }  


接下来createSurface()

 

 

[java] view plain copy

  1. /** 
  2.         * Create an egl surface for the current SurfaceHolder surface. If a surface 
  3.         * already exists, destroy it before creating the new surface. 
  4.         * 
  5.         * @return true if the surface was created successfully. 
  6.         */  
  7.        public boolean createSurface() {  
  8.            if (view != null) {  
  9.                mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl,  
  10.                        mEglDisplay, mEglConfig, view.getHolder());  
  11.            }   
  12.   
  13.            /* 
  14.             * Before we can issue GL commands, we need to make sure 
  15.             * the context is current and bound to a surface. 
  16.             */  
  17.            if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {  
  18.                /* 
  19.                 * Could not make the context current, probably because the underlying 
  20.                 * SurfaceView surface has been destroyed. 
  21.                 */  
  22.                return false;  
  23.            }  
  24.            return true;  
  25.        }  

这里主要完成了两件事:

 

5,创建mEglSurface,这个代表了将要被渲染的那段内存。请注意到createWindowSurface()的四个参数,尤其是最后一个参数view.getHolder()。

createSurface()上面有一句注解:Create an egl surface for the current SurfaceHolder surface.这个只能意会,很难言传。我理解是被渲染后的mEglSurface也是为了给mSurface来呈现的。总之mEglSurface和mSurface之间一定有着很重要的关系的,在一定程度上你也可以理解他们代表着同一块用来渲染的内存。

6,连接EGLContext和EGLSurface:eglMakeCurrent()。


7,使用GL指令绘制图形

 

[java] view plain copy

  1. <span style="white-space:pre">        </span>    if (createGlInterface) {  
  2.                         gl = (GL10) mEglHelper.createGL();  
  3.                     }  
  4.   
  5.                     if (createEglContext) {  
  6.                         if (view != null) {  
  7.                             view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);  
  8.                         }  
  9.                     }  
  10.   
  11.                     if (sizeChanged) {  
  12.                         if (view != null) {  
  13.                             view.mRenderer.onSurfaceChanged(gl, w, h);  
  14.                         }  
  15.                         sizeChanged = false;  
  16.                     }  
  17.   
  18.                   
  19.                     if (view != null) {  
  20.                         view.mRenderer.onDrawFrame(gl);  
  21.                     }  

所以在实现Render看到的GL10 gl,就是从这里传过来的。

 

在整个guardenRun()过程中,你应该要发现一个很重要的点,这是一个无限循环的程序,而onDrawFrame(gl)几乎是没有设置任何障碍就可以每次循环都被触发。而onDrawFrame(gl)的实现正是整个渲染的主体部分,由Render的子类来实现。

后面几个步骤就不一一讲诉了

8,断开并释放与EGLSurface关联的EGLContext对象
9,删除EGLSurface对象
10,删除EGLContext对象
11,终止与EGLDisplay之间的连接。

 

在使用GlSurfaceView的时候,通常会继承GLSurfaceView,并重载一些和用户输入事件有关的方法。如果你不需要重载事件方法,GLSurfaceView也可以直接使用, 你可以使用set方法来为该类提供自定义的行为。

说到这里,我就上一个最简化的demo:

 

[java] view plain copy

  1. public class MainActivity extends Activity {  
  2.     private MyGLSurfaceView mGLView;  
  3.   
  4.     @Override  
  5.     protected void onCreate(Bundle savedInstanceState) {  
  6.         super.onCreate(savedInstanceState);  
  7.         mGLView = new MyGLSurfaceView(this);  
  8.         mGLView.setRenderer(new ClearRenderer());  
  9.         setContentView(mGLView);  
  10.     }  
  11.   
  12.     @Override  
  13.     protected void onPause() {  
  14.         super.onPause();  
  15.         mGLView.onPause();  
  16.     }  
  17.   
  18.     @Override  
  19.     protected void onResume() {  
  20.         super.onResume();  
  21.         mGLView.onResume();  
  22.     }  
  23.   
  24.     class ClearRenderer implements MyGLSurfaceView.Renderer {  
  25.   
  26.         @Override  
  27.         public void onSurfaceCreated(GL10 gl, javax.microedition.khronos.egl.EGLConfig config) {  
  28.   
  29.         }  
  30.   
  31.         public void onSurfaceChanged (GL10 gl, int w, int h)  
  32.         {  
  33.             gl.glViewport(00, w, h);  
  34.         }  
  35.   
  36.         public void onDrawFrame(GL10 gl) {  
  37.             gl.glClearColor(mRed, mGreen, mBlue, 1.0f);  
  38.             gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);  
  39.         }  
  40.   
  41.     }  
  42. }  

 

 

 

 

 

 

GLSurfaceView的绘制过程要点

1,GLSurfaceview的渲染模式RenderMode

在onAttachedToWindow后就启动了一个无线循环的子线程,该子线程完成了整个绘制流程,并系统默认是负责不断刷新重绘,刷新的帧率是60FPS。从这里也可以看出来,GLSurfaceView系统默认是16ms就重绘一次,这样的耗性能的重绘操作一定是要用在那种有持续动画的效果才有意义。

当然,你也可以通过设置setRenderMode去设置主动刷新:

 

[java] view plain copy

  1. /** 
  2.  * Set the rendering mode. When renderMode is 
  3.  * RENDERMODE_CONTINUOUSLY, the renderer is called 
  4.  * repeatedly to re-render the scene. When renderMode 
  5.  * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface 
  6.  * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY. 
  7.  * <p> 
  8.  * Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance 
  9.  * by allowing the GPU and CPU to idle when the view does not need to be updated. 
  10.  * <p> 
  11.  * This method can only be called after {@link #setRenderer(Renderer)} 
  12.  * 
  13.  * @param renderMode one of the RENDERMODE_X constants 
  14.  * @see #RENDERMODE_CONTINUOUSLY 
  15.  * @see #RENDERMODE_WHEN_DIRTY 
  16.  */  
  17. public void setRenderMode(int renderMode) {  
  18.     mGLThread.setRenderMode(renderMode);  
  19. }  

注解中提到:系统默认mode==RENDERMODE_CONTINUOUSLY,这样系统会自动重绘;mode==RENDERMODE_WHEN_DIRTY时,只有surfaceCreate的时候会绘制一次,然后就需要通过requestRender()方法主动请求重绘。同时也提到,如果你的界面不需要频繁的刷新最好是设置成RENDERMODE_WHEN_DIRTY,这样可以降低CPU和GPU的活动,可以省电。

 

 

2,事件处理

 

为了处理事件,一般都是继承GLSurfaceView类并重载它的事件方法。但是由于GLSurfaceView是多线程操作,所以需要一些特殊的处理。由于渲染器在独立的渲染线程里,你应该使用Java的跨线程机制跟渲染器通讯。queueEvent(Runnable)方法就是一种相对简单的操作。

 

[java] view plain copy

  1. class MyGLSurfaceView extends GLSurfaceView {    
  2.     private MyRenderer mMyRenderer;    
  3.     
  4.         public void start() {    
  5.             mMyRenderer = ...;    
  6.             setRenderer(mMyRenderer);    
  7.         }    
  8.     
  9.     
  10.         public boolean onKeyDown(int keyCode, KeyEvent event) {    
  11.     
  12.             if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {    
  13.                 queueEvent(new Runnable() {    
  14.                     // 这个方法会在渲染线程里被调用    
  15.                          public void run() {    
  16.                              mMyRenderer.handleDpadCenter();    
  17.                          }});    
  18.                      return true;    
  19.                  }    
  20.     
  21.                  return super.onKeyDown(keyCode, event);    
  22.             }    
  23.       }    
  24. }    

 

调用queueEvent就是给队列中添加runnable

[java] view plain copy

  1. public void queueEvent(Runnable r) {  
  2.   
  3.     synchronized(sGLThreadManager) {  
  4.         mEventQueue.add(r);  
  5.         sGLThreadManager.notifyAll();  
  6.     }  
  7.   
  8. }  


在guardenRun()中有如下代码:

 

 

[java] view plain copy

  1. <span>        </span>if (! mEventQueue.isEmpty()) {  
  2.                     event = mEventQueue.remove(0);  
  3.                     break;  
  4.                 }  
  5.                   
  6.                 ...  
  7.                   
  8.                 if (event != null) {  
  9.                     event.run();  
  10.                     event = null;  
  11.                     continue;  
  12.                 }  

因为每次都会remove掉添加的runnable,所以上面那个demo就是非常好的解释,每次按键就是添加runnable。当然,这也是要求绘制是一直在循环重绘的状态才能看到效果。
(注:如果在UI线程里调用渲染器的方法,很容易收到“call to OpenGL ES API with no current context”的警告,典型的误区就是在键盘或鼠标事件方法里直接调用opengl es的API,因为UI事件和渲染绘制在不同的线程里。更甚者,这种情况下调用glDeleteBuffers这种释放资源的方法,可能引起程序的崩溃,因为UI线程想释放它,渲染线程却要使用它。)

 

关于GLSurfaceView的渲染过程的重要知识点已经介绍完毕,了解这些对开发当然是很有用的,很多时候你需要实现自定义的类GLSurfaceView的类。

那么现在,最后剩下的就是onDrawFrame(GL10 gl)的主体绘制的实现,这也是最重要的一个部分,因为涉及的内容较多,就不在这里陈述了。这里使用的就是opengl的绘制引擎进行渲染操作,跟之前View的渲染是使用的Skia渲染引擎。

还记得View的绘制onDraw(Canvas canvas)吗,对比onDrawFrame(GL10 gl),我想你该知道区别了。一个使用Skia引擎渲染,一个使用opengl引擎渲染。

 

问题:

1,GLSurfaceView继承了SurfaceView,它自己的mEglSurface和从父类继承的mSurface之间的关系?

但是呢,

 

[java] view plain copy

  1. mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl,  
  2.                       mEglDisplay, mEglConfig, view.getHolder());  

mEglSurface在创建的时候,是有view.getHolder作为输入的,我们知道SurfaceHolder是持有Surface的。我一直跟踪到android_opengl_EGL14.cpp和com_google_android_gles_jni_EGLImpl.cpp 发现:surface总是作为一种输入后再加上其他参数,才能返回mEglSurface。我就开始怀疑他们是不是同一个surface,他们是不是指向了同一快内存地址?

 

为了验证我的这个想法,于是我打印了mSurface和mEglSurface的地址,发现他们却不是同一块地址。这就让人深思了,现在的情况只能说明,他们两个一定有关系,但是又不是指向同一块地址。对这方面有经验的朋友欢迎指导。


2,你怎么知道onDrawFrame是60FPS的帧率呢?

Android系统每隔16ms发出VSYNC信号,触发GPU对UI进行渲染,如果每次渲染都成功,这样就能够达到流畅的画面所需要的60fps,为了能够实现60fps,这意味着程序的大多数操作都必须在16ms内完成。

如果你的某个操作花费时间是24ms,系统在得到VSYNC信号的时候就无法进行正常渲染,这样就发生了丢帧现象。那么用户在32ms内看到的会是同一帧画面。(卡顿现象)

用户容易在UI执行动画或者滑动ListView的时候感知到卡顿不流畅,是因为这里的操作相对复杂,容易发生丢帧的现象,从而感觉卡顿。有很多原因可以导致丢帧,也许是因为你的layout太过复杂,无法在16ms内完成渲染,有可能是因为你的UI上有层叠太多的绘制单元,还有可能是因为动画执行的次数过多。这些都会导致CPU或者GPU负载过重。

 

3,那我们为什么要选择16ms,60fps的帧率去VSYNC一次呢?

12fps大概类似手动快速翻动书籍的帧率,这明显是可以感知到不够顺滑的。 24fps使得人眼感知的是连续线性的运动,电影胶圈通常使用的帧率, 低于30fps是无法顺畅表现绚丽的画面内容的,60fps来达到想要的效果, 超过60fps是没有必要的。 开发app的性能目标就是保持60fps,这意味着每一帧你只有16ms=1000/60的时间