Android SurfaceView的绘制详解


 在Android系统中,有一种特殊的视图,称为SurfaceView,它拥有独立的绘图表面,即它不与其宿主窗口共享同一个绘图表面。由于拥有独立的绘图表面,因此SurfaceView的UI就可以在一个独立的线程中进行绘制。又由于不会占用主线程资源,SurfaceView一方面可以实现复杂而高效的UI,另一方面又不会导致用户输入得不到及时响应。

SurfaceView也是继承View的,但是跟其他TextView,ImageView等就不一样;最大的不一样,SurfaceView并没有使用View的宿主窗口(下面我都管这个宿主窗口为顶级surface),它有自己独立的surface,绘制是在这个独立的surface上绘制。

而TextView,ImageView等视图控件同样也是继承View的子类,他们却是共用的一个surface,这个surface在ViewRootImpl创建,以lockCanvas()的形式返回canvas去管理绘制传递下去。

先从一个简单的demo讲起:

public class MySurfaceView extends SurfaceView implements Runnable, SurfaceHolder.Callback {
   private SurfaceHolder mHolder; // 用于控制SurfaceView 
   private Thread t; // 声明一条线程 
   private boolean flag; // 线程运行的标识,用于控制线程 
   private Canvas mCanvas; // 声明一张画布 
   private Paint p; // 声明一支画笔 
   private int x = 50, y = 50, r = 10; // 圆的坐标和半径 
   public MySurfaceView(Context context) { 
     super(context); 
     mHolder = getHolder(); // 获得SurfaceHolder对象 
     mHolder.addCallback(this); // 为SurfaceView添加状态监听 
     p = new Paint(); // 创建一个画笔对象 
     p.setColor(Color.WHITE); // 设置画笔的颜色为白色 
     setFocusable(true); // 设置焦点 
   } 
   /** 
   * 自定义一个方法,在画布上画一个圆 
   */
   public void doDraw() { 
     mCanvas = mHolder.lockCanvas(); // 获得画布对象,开始对画布画画 
     mCanvas.drawRGB(0, 0, 0); // 把画布填充为黑色 
     mCanvas.drawCircle(x, y, r, p); // 画一个圆 
     mHolder.unlockCanvasAndPost(mCanvas); // 完成画画,把画布显示在屏幕上 
   } 
   /** 
   * 当SurfaceView创建的时候,调用此函数 
   */
   @Override
   public void surfaceCreated(SurfaceHolder holder) { 
     t = new Thread(this); // 创建一个线程对象 
     flag = true; // 把线程运行的标识设置成true 
     t.start(); // 启动线程 
   } 
   /** 
   * 当SurfaceView的视图发生改变的时候,调用此函数 
   */
   @Override
   public void surfaceChanged(SurfaceHolder holder, int format, int width, 
       int height) { 
   } 
   /** 
   * 当SurfaceView销毁的时候,调用此函数 
   */
   @Override
   public void surfaceDestroyed(SurfaceHolder holder) { 
     flag = false; // 把线程运行的标识设置成false 
   } 
   /** 
   * 当屏幕被触摸时调用 
   */
   @Override
   public boolean onTouchEvent(MotionEvent event) { 
     x = (int) event.getX(); // 获得屏幕被触摸时对应的X轴坐标 
     y = (int) event.getY(); // 获得屏幕被触摸时对应的Y轴坐标 
     return true; 
   } 
   /** 
   * 当用户按键时调用 
   */
   @Override
   public boolean onKeyDown(int keyCode, KeyEvent event) { 
     if(keyCode == KeyEvent.KEYCODE_DPAD_UP){  //当用户点击↑键时 
       y--;  //设置Y轴坐标减1 
     } 
     return super.onKeyDown(keyCode, event); 
   } 
   @Override
   public void run() { 
     while (flag) { 
       doDraw(); // 调用自定义画画方法 
       try { 
         Thread.sleep(2000); // 让线程休息50毫秒 
       } catch (InterruptedException e) { 
         e.printStackTrace(); 
       } 
     } 
   } 
}

MySurfaceView的要点:

1,继承SurfaceView父类和SurfaceHolder.Callback接口,并实现surfaceCreated(SurfaceHolder holder),surfaceChanged,surfaceDestroyed方法来管理surfaceview的生命周期。

2,构造方法中通过SurfaceHolder来设置this为callback。

mHolder = getHolder(); // 获得SurfaceHolder对象 
mHolder.addCallback(this); // 为SurfaceView添加状态监听 
3,进行具体的主体绘制时需主动通过lockCanvas(Rect dirty)获取canvas,这也是跟View不同的地方。

注意:

1,这里不知道你有没有注意到,我们在MySurfaceView的绘制里面开了个子线程。学过android肯定知道UI线程一定在主线程,而SurfaceView的绘制可以在子线程实现,这也正是SurfaceView的一大亮点。如此可以把一些耗时的渲染让在SurfaceView来绘制,就不会导致主线程阻塞。

可以使用子线程的原因(个人理解):android的主UI是在ViewRootImpl提供的surface,作为顶级surface是要一直呈现给用户的,所有的View都是在顶级Surface进行绘制的。而SurfaceView拥有自己独立的surface,由WindowManagerService完成绘制,并以在顶级surface上打洞的形式来呈现自己。

因为SurfaceView拥有独立的surface,最好的实现应该是要放在独立的线程来绘制。


这里有几个类要详细讲一下:Surface,SurfaceHolder ,SurfaceHolder.Callback

Surface

surface对应着一个layer,是原始图像缓冲区(raw buffer)的一个句柄,而原始图像缓冲区是由屏幕图像合成器(screen compositor)管理的。我们可以理解surface就是一段最终用来显示的内存。每个SurfaceView都有一个独立的surface。

而View以及一般View的子类并不持有独立的surface,他们持有的是一个顶级surface通过lockCanvas(Rect dirty)指定的部分区域,并通过canvas来绘制这部分区域。这个顶级surface也对应着一个layer。SurfaceView的surface是通过在顶级surface下打洞的形式来呈现自己。

最终,这些surface会被surfaceFlinger来合成并最终显示出来。

Surface是实现了Parcelable接口的,所以可以实现跨进程传输surface。事实上,surface的刷新都在updateWindow()中具体实现。而updateWindow()中正式跨进程的调用了WindowManagerService来进行surface的重绘并把新的surface返回SurfaceView。

IWindowSession mSession;

SurfaceView->updateWindow():

 relayoutResult = mSession.relayout(
                        mWindow, mWindow.mSeq, mLayout, mWidth, mHeight,
                            visible ? VISIBLE : GONE,
                            WindowManagerGlobal.RELAYOUT_DEFER_SURFACE_DESTROY,
                            mWinFrame, mOverscanInsets, mContentInsets,
                            mVisibleInsets, mStableInsets, mConfiguration, mNewSurface);

IWindowSession.aidl

 int relayout(IWindow window, int seq, in WindowManager.LayoutParams attrs,
            int requestedWidth, int requestedHeight, int viewVisibility,
            int flags, out Rect outFrame, out Rect outOverscanInsets,
            out Rect outContentInsets, out Rect outVisibleInsets,
            out Configuration outConfig, out Surface outSurface);

这里希望读者可以区分aidl的标识符in和out的区别。in代表是输入,out对应的字段是要输出的,这里跟c语言里指针的用法类似。这里的outSurface看起来像是输入,其实最终是作为一个输出来表示新的surface:mNewSurface.

在SurfaceView中输入的参数mWindow,mWindow.mSeq,mLayout等,输出的参数有outFrame,outConfig,outSurface等。

很明显,这里是把SurfaceView中的一些LayoutParams布局属性值传递给了mSession,而mSession又调用WindowManagerService的relayoutWindow()来完成重绘过程,并将新的outSurface返回给SurfaceView的mNewSurface。

mSurface.transferFrom(mNewSurface);
在updateWindow()中通过transferFrom()即可把新的mNewSurface赋值给mSurface。

SurfaceHolder

SurfaceHolder的主要作用:

1,getSurface(),对surface的持有和管理

2,lockCanvas(),unlockCanvasAndPost(),对canvas的创建和释放

3,addCallback(),removeCallback(),对实现SurfaceHolder.Callback的MySurfaceView子类注册管理。

4,通过调用surfaceholder的setFixedSize、setSizeFromLayout、setFormat都会触发重绘surface得到新的surface。

注意:

1,这里的lockCanvas()和lockCanvas(Rect dirty)的区别,dirty指向一个指定的重绘区域,区域之外部分不重绘,如此可提高绘制效率。


SurfaceHolder.Callback

SurfaceHolder.Callback管理Surface的生命周期,并监听surface的变化。每当surface状态发生变化的时候会以各种形式通知SurfaceView,并最终都会触发updateWindow(),进而触发了callback的三个方法,并把新的surfaceholder输出。

SurfaceHolder.Callback具有如下的接口:
surfaceCreated(SurfaceHolder holder):

当Surface第一次创建后会立即调用该函数。程序可以在该函数中做些和绘制界面相关的初始化工作,一般情况下都是在另外的线程来绘制界面,所以不要在这个函数中绘制Surface。 

surfaceChanged(SurfaceHolder holder, int format, int width,int height):

当Surface的状态(大小和格式)发生变化的时候会调用该函数,在surfaceCreated调用后该函数至少会被调用一次。 

surfaceDestroyed(SurfaceHolder holder):

当Surface被摧毁前会调用该函数,该函数被调用后就不能继续使用Surface了,一般在该函数中来清理使用的资源。 


SurfaceView和View的比较

1,SurfaceView和View最大的区别在于,surfaceView是在一个新起的单独线程中可以重新绘制画面而View必须在UI的主线程中更新画面。

2,View是通过ViewRootImpl提供的顶级surface进行lockCanvas(Rect dirty),返回的canvas会在指定的dirty范围进行绘制。

而每一个SurfaceView拥有独立的surface,通过在顶级surface上打洞来显示自己。这些surface对应底层的Layer,由SurfaceFlinger根据这些layer的内容以及层级进行混合并最终显示。

The surface is Z ordered so that it is behind the window holding its SurfaceView; the SurfaceView punches a hole in its window to allow its surface to be displayed. 

3,在使用上SurfaceView需通过SurfaceHolder.lockCanvas()主动获取canvas。而View是直接由顶级surface在ViewRootImple就已经lockCanvas后,把canvas传递到View的draw(canvas)。

4,因为View是只拥有一个顶级surface,子view都是共用一个surface,所以在绘制时performTraversals()是对一个树结构的view群进行测量、布局、绘制的遍历。

而一个SurfaceView拥有一个surface,它只需要对自己进行测量、布局和绘制。流程简单的多。

5,SurfaceView实现重绘的方式更直观,每次主动调用doDraw()(demo中我自定义的方法)就是重新绘制。而View因为不能直接通过surface.lockCanvas获取canvas,只能通过调用invalidate()去触发父视图ViewRootImpl去调用performTraversal()去实现重绘。


SurfaceView和View的使用场景

1 ,被动更新画面的。比如棋类,这种用view就好了。因为画面的更新是依赖于 onTouch 来更新,可以直接使用 invalidate。 因为这种情况下,这一次Touch和下一次的Touch需要的时间比较长些,不会产生影响。

2 ,主动更新。比如一个人在一直跑动。这就需要一个单独的thread不停的重绘人的状态,避免阻塞main UI thread。所以显然view不合适,需要surfaceView来控制。


Surface和Canvas的区别

surface就可以这样理解:它是内存中一块区域,它是surfaceview可见的那个部分,绘图操作作用于它,然后它就会被显卡之类的显示控制器绘制到屏幕上。大家都知道,内存区的对象是有生命周期的,可以动态的申请创建和销毁,当然也可能会更新。于是,就有了作用于这个内存区的操作,这些操作就是surfaceCreated/Changed/Destroyed。三个操作放在一起,就是SurfaceHolder.Callback。

Canvas倒像是一个拥有一段绘制区域的画家,通过surface.lockCanvas(Rect dirty)来获得,并指定canvas只能在dirty范围进行对surface指定区域的绘制。

阅读更多
个人分类: Android
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

不良信息举报

Android SurfaceView的绘制详解

最多只允许输入30个字

加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!
关闭
关闭