Android SurfaceView总结及代码示例

#一.概述
     SurfaceView与普通View不同,View树上的普通View共享一个Surface,而SurfaceView拥有单独的Surface。
    而且普通View必须在UI线程中绘制,而SurfaceView可以在非UI线程中完成绘制工作,不占用UI主线程。
    
    SurfaceView可以通过SurfaceHolder获取其Surface的尺寸和状态变化,并通过SurfaceHolder控制在Surface上的控制流程。
    从Android 1.0(API level 1)时就有SurfaceView类。

##.SurfaceView出现的原因

    Android手机上每当显示屏把对应的帧缓冲区上数据扫描显示完毕后,系统就会发出一个垂直同步VSYNC信号来触发下一轮的View重绘。显示屏的刷新频率一般是60Hz,即大约每16ms就会触发一轮绘制。一般情况下,View在这个时间间隔内是能完成其绘制的。但View是在主线程中绘制,主线程中运行着大量事务,当画面的绘制逻辑比较复杂、绘制频率又比较频繁时,一方面,主线程有可能来不及完成绘制,就会出现画面卡顿现象;另一方面,过多的绘制任务执行在主线程中,会占用过多的主线程执行时间,妨碍主线程其它任务的执行。
    为此,Android中提供了SurfaceView来应对这些场景,它可以在非UI线程中执行绘制任务,不占用主线程的执行时间。至于其继承类GLSurfaceView,更是引入了OpenGL ES,通过GPU硬件绘制来大大提升绘制速度。
(SurfaceView如果不能及时完成自己的绘制,一样会画面卡顿。只是它可以运行在独立的线程中完成绘制,不像主线程中有那么多任务,所以用于绘制的时间更“宽裕”一些。而且就算SurfaceView上画面卡顿,对主线程也不会造成干扰,起码主线程上一切如常,其它View能够正常刷新和完成操作响应。)
##.普通 View SurfaceView 的主要区别:
1 . 最本质差别是,普通View必须在主线程中绘制界面,而SurfaceView可以开启一个新线程来绘制界面。
2 . 因此View适用于耗时较短的绘制,否则容易引发画面卡顿;
     而SurfaceView则适用于较频繁或耗时较久的绘制,不会因此阻塞UI线程。
3 . SurfaceView由于独占一个Surface,所以本身具有双缓冲机制,可以通过传递脏区的方式,每次只进行局部绘制,没必要全部重新绘制一遍;
    而Window上的所有View共享一个Surface,单个普通View并没有双缓冲机制,每次绘制必须完全重新绘制一遍,不能只绘制View的局部。
##.SurfaceView单独拥有的Surface是如何显示的
    SurfaceView本身直接管理一个独立的Surface,同时SurfaceView又属于某个View树,附在对应的Window上,所以它与两个Surface相关联:
        一个是SurfaceView自己单独拥有的Surface,其显示层级较低;
        另一个是View树所在Window的Surface,其显示层级较高。
因此,前者其实显示在后者的下面。
    SurfaceView真实画面绘制在自己独自拥有的Surface上,而这张Surface位于Window下面,为何没被遮挡,而是会显示出来的呢?
    因为Window上的View树绘制时,SurfaceView也会参与,它会把Window的Surface上自己对应的区域绘制成透明色,于是Window下面SurfaceView独立的Surface就可以显示出来了。这个过程,就如同在Window上对应位置嵌了块透明玻璃一样,透过透明玻璃可以看到玻璃下面的东西。

二、SurfaceView的双缓冲

    SurfaceView实际上是利用了Surface的双缓冲机制,其SurfaceHolder的lockCanvas()和unlockCanvasAndPost()在实现中其实最终是调用Surface的相应方法来完成功能。
    其双缓冲机制,可以简化理解为有两个缓冲区引用,一个frontBuffer和一个backBuffer,backBuffer指向后置缓冲区,用于缓存正在绘制的画面;而frontBuffer指向前置缓冲区,用于缓存最近绘制完毕、要提交使用的画面。二者可以互相切换。  
绘制中 不断地循环这个过程:
1.当使用lockCanvas()获取画布,获取Surface的Canvas对象,用于在backBuffer上进行绘制新的内容;
2. 当上述绘图结束 后,调用 unlockCanvasAndPost(canvas)互换二者身份,原来的backBuffer变为frontBuffer用于提交给外部使用,而原来的frontBuffer变为backBuffer, 等待下一次 lockCanvas() 并参与绘制新内容
   其中步骤1可以使用lockCanvas(Rect dirty)传入一个区域范围,这个范围一般称为”脏区“,通过脏区,可以告诉SurfaceView本轮想去重绘哪部分范围的画面。(这名字挺形象的,这部分区域脏了,所以需要抹干净重新绘制。)
 当然,也可以直接调用lockCanvas(),此时在实现中其实传入的脏区其实为null,这代表着整个backBuffer都是脏区,需要完全重绘一遍。  
   在lockCanvas()或lockCanvas(Rect dirty)对应的native实现逻辑中,会判断是否能将frontBuffer中的图像复制到backBuffer中,如果可以的话,会把frontBuffer中”上一轮脏区 - 本轮脏区“ 对应范围的画面复制到backBuffer上。这样在上面不断循环的绘制中,其实每一轮所需要绘制的,只是每一轮脏区内的范围,脏区外的范围会保持跟上一次画面相同。
   上述是否能将frontBuffer中的图像复制到backBuffer中的判断条件是:frontBuffer已存在 且 前后缓冲区宽、高和格式都完全一致。 
     
   至于为何每次复制范围是”上一轮脏区 - 本轮脏区“,可以这么理解:
   每一轮在原有基础上只有脏区内容发生了改变。所以当本轮需要在backBuffer上绘制时,frontBuffer上只有上一轮脏区的内容是针对当前backBuffere内容做的改变,只要把这部分内容复制过来,backBuffer上画面就与frontBuffer中的上一轮绘制结果完全相同。但本轮会重绘本轮脏区内容,所以只需要复制frontBuffer中”上一轮脏区 - 本轮脏区“ 对应范围的画面。
   通过指定脏区,每轮只需要局部绘制,这算是SurfaceView双缓冲特性的一个重要应用。普通View在自己的绘制流程中无双缓冲特性,如果需要重绘,就必须完全绘制一遍。
(但Window的整体画面对应一个Surface,也有双缓冲特性,ViewRootImpl内部会记录每一轮需要重绘的脏区,每次View树绘制只会重绘需要绘制的View。无论SurfaceView还是普通View所依附的Window,其画面载体都是Surface,当然都可以利用Surface的双缓冲特性。)
三、 相关 重要API
    SurfaceView持有一个SurfaceHolder,而SurfaceHolder中持有一个Surface,ViewHolder就像是一个Surface的管理器,可以监听器状态改变并针对其做一些操作。
###.SurfaceHolder
   SurfaceHolder是一个接口,类似于一个surace的监听器。通过下面三个回调方法监听Surface的创建、销毁或者改变。    
  SurfaceView中调用getHolder方法,可以获得当前SurfaceView中的surface对应SurfaceHolder。
SurfaceHolder中重要的方法有:
1. void addCallback(SurfaceHolder.Callback callback );
    为SurfaceHolder添加一个SurfaceHolder.Callback回调接口。
2. Canvas lockCanvas() ;
3. Canvas lockCanvas(Rect dirty)
   调用后可获取Canvas用于绘制。
   实际执行逻辑是在native层完成的,在native层,会为Surface的backBuffer分配可用图形缓冲区,把这个图像缓冲区作为画布创建Canvas对象,并返回给java层。
4. abstract  void unlockCanvasAndPost(Canvas canvas);
    绘制完成后调动。   
    实际执行逻辑是在native层完成的,在native层会将当前绘制好的后置缓冲区提交供画面消费方使用。BufferQueue中只有两个GraphicBuffer时,这一步加上下一次的lockCanvas(),最后总的效果是互换了前后缓冲区。
###.SurfaceHolder.Callback
    SurfaceHolder.Callback是SurfaceHolder接口内部的静态子接口,可用于监听持有的Surface状态变化,SurfaceHolder.Callback中定义了三个接口方法:
1: public void surfaceCreated(SurfaceHolder holder);
                //Surface创建后触发,一般在这里启动绘制画面的线程。
                Surface开始显示,会触发Surface的创建,例如Activity从后台不显示切换回前台显示。
2:public void sufaceChanged(SurfaceHolder holder,int format,int width,int height);
               //Surface的大小、数据格式发生改变时调用。
3: public void surfaceDestroyed(SurfaceHolder holder);
               //销毁时激发,一般在这里将绘制画面的线程停止、释放。
                  Surface不显示,会触发Surface的销毁,例如Activity从前台切换到后台不再显示。
###.SurfaceView类中的API
1.SurfaceHolder getHolder()
    获取SurfaceView中的SurfaceHolder对象;
2.setZOrderOnTop(boolean onTop)
    控制SurfaceView的Surface是否置于其所属Window的上方(默认是在Window下方的)。
(“Control whether the surface view's surface is placed on top of its window.”)
3.setZOrderMediaOverlay(boolean isMediaOverlay)
    Window上可能会添加多个SurfaceView或TextureView,这些特殊View内部都含有自己独立的Surface。
    而这个方法的作用是,控制该SurfaceView的Surface是否置于其它这些Surface的上方,但仍然会在Window下方。
(“ Control whether the surface view's surface is placed on top of another regular surface view in the window (but still behind the window itself).”)
四、代码示例
public class TestSurfaceView extends SurfaceView implements SurfaceHolder.Callback{
    private final String TAG = getClass().getSimpleName();
    private SurfaceHolder mHolder = getHolder();

    public TestSurfaceView(Context context) {
        this(context, null);
    }

    public TestSurfaceView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public TestSurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr, 0);
    }

    public TestSurfaceView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        //为Surface添加状态监听
        mHolder.addCallback(this);
    }

    /*********单独的线程用于绘制********/
    //这里使用Timer来进行控制,方便控制间隔时间
    //Timer中包含一个TimerThread线程,它继承自Thread,任务都是在TimerThread线程中执行的
    private Timer mTimer;
    private TimerTask mTimerTask;
    private long mPeriod = 1000/30;//定义刷新间隔为1000/30ms,即每秒钟刷新30次

    /**********     继承自SurfaceHolder.Callback的三个方法        **********/
    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        ILog.d(TAG, "surfaceCreated()");

        initDrawSetting();
        //开始在TimerThread线程中执行绘制操作
        mTimer = new Timer();//每次都要新建,因为一旦cancel,就不能再次start()使用
        mTimerTask = new TimerTask() {
            @Override
            public void run() {
                timeNow += mPeriod;
                draw();
            }
        };
        mTimer.schedule(mTimerTask, 0, mPeriod);
    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
        ILog.d(TAG, "surfaceChanged()");
    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
        ILog.d(TAG, "surfaceDestroyed()");

        if(mTimer != null){
            mTimer.cancel();
        }
    }


    /***************      绘制逻辑       ****************/
    private Paint mPaint;//画笔
    //初始化画笔
    private void initDrawSetting(){
        if(mPaint == null){
            mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
            mPaint.setTextSize(DeviceUtils.spToPx(16));
            Typeface font = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD);
            mPaint.setTypeface( font );
            mPaint.setStrokeWidth(DeviceUtils.dpToPx(0.5f));
        }
        textWidth = mPaint.measureText(text);
    }
    private long totalTime = 5000;
    private long timeNow = 0;
    private int startX = DeviceUtils.dpToPx(10);
    private String text = "种一棵树,最好的时间是十年前,其实是现在";
    private float textWidth;
    private int lineHeight = DeviceUtils.dpToPx(100);
    private int textBaseline = lineHeight/2;
    private int mNormalTextColor = getResources().getColor(R.color.common_white, null);
    private int mHighlightTextColor = getResources().getColor(R.color.common_red, null);;
    private int mStrokeTextColor = Color.parseColor("#66000000");
    private int mBgColor = getResources().getColor(R.color.common_white60, null);
    //自定义的方法,封装绘制逻辑
    private void draw(){
        //1.锁定画布,将在后台缓冲区中做修改
        Canvas canvas = mHolder.lockCanvas();

        //2.具体的绘制
        //这里是模拟卡拉ok时一行歌词从左到右逐渐变高亮的过程
        float highlightTextWidth = textWidth * (timeNow%totalTime)/totalTime;
//        canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);//清除背景
        canvas.drawColor(mBgColor, PorterDuff.Mode.SRC);//设置背景颜色
        //首先,确定左边文字选中的裁剪区域,然后用高亮色绘制文字
        canvas.save();
        canvas.clipRect(startX, 0, startX + highlightTextWidth, lineHeight);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(mStrokeTextColor);
        canvas.drawText(text, startX, textBaseline, mPaint);
        mPaint.setColor(mHighlightTextColor);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawText(text, startX, textBaseline, mPaint);
        canvas.restore();
        //确定右边文字的选中裁剪区域,紧邻左边已绘制的文字,然后用普通色绘制文字。
        //这样最终效果是,一行文字,左边部分是高亮色,右边文字是普通色
        canvas.save();
        canvas.clipRect(startX + highlightTextWidth, 0, startX + textWidth, lineHeight);
        mPaint.setStyle(Paint.Style.STROKE);
        mPaint.setColor(mStrokeTextColor);
        canvas.drawText(text, startX, textBaseline, mPaint);
        mPaint.setColor(mNormalTextColor);
        mPaint.setStyle(Paint.Style.FILL);
        canvas.drawText(text, startX, textBaseline, mPaint);
        canvas.restore();

        //3.将画布的内容保存到后台缓冲区,然后将后台缓冲区切换到前台并显示在surface中
        //原先的前台缓冲区将切换为后台缓冲区
        mHolder.unlockCanvasAndPost(canvas);
    }
}

相关笔记:
参考剪藏:
SurfaceView的源代码:
(AndroidStudio中只能看到SurfaceView部分方法的方法名,看不到完整源代码)

Android-Surface之双缓冲及SurfaceView解析 - 博客 - 编程圈

猜你喜欢

转载自blog.csdn.net/u013914309/article/details/124677834
今日推荐