GLSurfaceView渲染过程详解


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


概念android

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


步骤:多线程

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


GLSurfaceView的绘制流程ide


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

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

private void guardedRun() throws InterruptedException {

                while (true) {
                    synchronized (sGLThreadManager) {
                        while (true) {

                            // Ready to draw?
                            if (readyToDraw()) {
                                // If we don't have an EGL context, try to acquire one.
                                if (! mHaveEglContext) {
                                    if (sGLThreadManager.tryAcquireEglContextLocked(this)) {
                                         mEglHelper.start();
                                    }
                                }

                            sGLThreadManager.wait();
                        }
                    } // end of synchronized(sGLThreadManager)

                    if (createEglSurface) {
                        if (mEglHelper.createSurface()) {
                           ...
                        }
                    }

                    if (createGlInterface) {
                        gl = (GL10) mEglHelper.createGL();
                    }

                    if (createEglContext) {
                        if (view != null) {
                            view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);
                        }
                    }

                    if (sizeChanged) {
                        if (view != null) {
                            view.mRenderer.onSurfaceChanged(gl, w, h);
                        }
                        sizeChanged = false;
                    }

                
                    if (view != null) {
                        view.mRenderer.onDrawFrame(gl);
                    }
                    
                    int swapError = mEglHelper.swap();
        }

其中mEglHelper.start():

public void start() {
            /*
             * Get an EGL instance
             */
            mEgl = (EGL10) EGLContext.getEGL();

            /*
             * Get to the default display.
             */
            mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);

          
            mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay);

            /*
            * Create an EGL context. We want to do this as rarely as we can, because an
            * EGL context is a somewhat heavy object.
            */
            mEglContext = view.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig);
        
        }

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

1,获取EGLDisplay对象动画

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

请注意注解中提到createContext()建立的mEglContext是一个重量级对象,在建立的时候很耗资源,咱们尽量少的建立它。因此,在guardenRun()中咱们作了对mEglContext的是否存在的判断:

if (! mHaveEglContext) {
                       if (sGLThreadManager.tryAcquireEglContextLocked(this)) {
                                         mEglHelper.start();
                                    }
                                }

接下来createSurface()

/**
         * Create an egl surface for the current SurfaceHolder surface. If a surface
         * already exists, destroy it before creating the new surface.
         *
         * @return true if the surface was created successfully.
         */
        public boolean createSurface() {
            if (view != null) {
                mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl,
                        mEglDisplay, mEglConfig, view.getHolder());
            } 

            /*
             * Before we can issue GL commands, we need to make sure
             * the context is current and bound to a surface.
             */
            if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
                /*
                 * Could not make the context current, probably because the underlying
                 * SurfaceView surface has been destroyed.
                 */
                return false;
            }
            return true;
        }
这里主要完成了两件事:

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指令绘制图形

<span style="white-space:pre">		</span>    if (createGlInterface) {
                        gl = (GL10) mEglHelper.createGL();
                    }

                    if (createEglContext) {
                        if (view != null) {
                            view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);
                        }
                    }

                    if (sizeChanged) {
                        if (view != null) {
                            view.mRenderer.onSurfaceChanged(gl, w, h);
                        }
                        sizeChanged = false;
                    }

                
                    if (view != null) {
                        view.mRenderer.onDrawFrame(gl);
                    }
因此在实现Render看到的GL10 gl,就是从这里传过来的。

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

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

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


在使用GlSurfaceView的时候,一般会继承GLSurfaceView,并重载一些和用户输入事件有关的方法。若是你不须要重载事件方法,GLSurfaceView也能够直接使用, 你可使用set方法来为该类提供自定义的行为。

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

public class MainActivity extends Activity {
    private MyGLSurfaceView mGLView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        mGLView = new MyGLSurfaceView(this);
        mGLView.setRenderer(new ClearRenderer());
        setContentView(mGLView);
    }

    @Override
    protected void onPause() {
        super.onPause();
        mGLView.onPause();
    }

    @Override
    protected void onResume() {
        super.onResume();
        mGLView.onResume();
    }

    class ClearRenderer implements MyGLSurfaceView.Renderer {

        @Override
        public void onSurfaceCreated(GL10 gl, javax.microedition.khronos.egl.EGLConfig config) {

        }

        public void onSurfaceChanged (GL10 gl, int w, int h)
        {
            gl.glViewport(0, 0, w, h);
        }

        public void onDrawFrame(GL10 gl) {
            gl.glClearColor(mRed, mGreen, mBlue, 1.0f);
            gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
        }

    }
}



GLSurfaceView的绘制过程要点

1,GLSurfaceview的渲染模式RenderMode

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

固然,你也能够经过设置setRenderMode去设置主动刷新:

/**
     * Set the rendering mode. When renderMode is
     * RENDERMODE_CONTINUOUSLY, the renderer is called
     * repeatedly to re-render the scene. When renderMode
     * is RENDERMODE_WHEN_DIRTY, the renderer only rendered when the surface
     * is created, or when {@link #requestRender} is called. Defaults to RENDERMODE_CONTINUOUSLY.
     * <p>
     * Using RENDERMODE_WHEN_DIRTY can improve battery life and overall system performance
     * by allowing the GPU and CPU to idle when the view does not need to be updated.
     * <p>
     * This method can only be called after {@link #setRenderer(Renderer)}
     *
     * @param renderMode one of the RENDERMODE_X constants
     * @see #RENDERMODE_CONTINUOUSLY
     * @see #RENDERMODE_WHEN_DIRTY
     */
    public void setRenderMode(int renderMode) {
        mGLThread.setRenderMode(renderMode);
    }
注解中提到:系统默认mode==RENDERMODE_CONTINUOUSLY,这样系统会自动重绘;mode==RENDERMODE_WHEN_DIRTY时,只有surfaceCreate的时候会绘制一次,而后就须要经过requestRender()方法主动请求重绘。同时也提到,若是你的界面不须要频繁的刷新最好是设置成RENDERMODE_WHEN_DIRTY,这样能够下降CPU和GPU的活动,能够省电。


2,事件处理

为了处理事件,通常都是继承GLSurfaceView类并重载它的事件方法。可是因为GLSurfaceView是多线程操做,因此须要一些特殊的处理。因为渲染器在独立的渲染线程里,你应该使用Java的跨线程机制跟渲染器通信。queueEvent(Runnable)方法就是一种相对简单的操做。

class MyGLSurfaceView extends GLSurfaceView {  
    private MyRenderer mMyRenderer;  
  
        public void start() {  
            mMyRenderer = ...;  
            setRenderer(mMyRenderer);  
        }  
  
  
        public boolean onKeyDown(int keyCode, KeyEvent event) {  
  
            if (keyCode == KeyEvent.KEYCODE_DPAD_CENTER) {  
                queueEvent(new Runnable() {  
                    // 这个方法会在渲染线程里被调用  
                         public void run() {  
                             mMyRenderer.handleDpadCenter();  
                         }});  
                     return true;  
                 }  
  
                 return super.onKeyDown(keyCode, event);  
            }  
      }  
}


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

public void queueEvent(Runnable r) {

    synchronized(sGLThreadManager) {
        mEventQueue.add(r);
        sGLThreadManager.notifyAll();
    }

}

在guardenRun()中有以下代码:

<span>		</span>if (! mEventQueue.isEmpty()) {
                    event = mEventQueue.remove(0);
                    break;
                }
                
                ...
                
                if (event != null) {
                    event.run();
                    event = null;
                    continue;
                }
由于每次都会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之间的关系?

可是呢,

mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl,
                        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的时间