简述SurfaceView及常见问题

Posted sparrowlhl

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了简述SurfaceView及常见问题相关的知识,希望对你有一定的参考价值。

  在android开发中,SurfaceView平常并不常用,但是遇到一些视频播放或者拍照等情况,就需要用到。下面对该控件进行简单的介绍,并列举出使用过程中遇到的问题进行QA形式的解答!

声明:  关于SurfaceView的原理的介绍主要参考文章:https://blog.csdn.net/luoshengyang/article/details/8661317

一、运用场景:

  普通的Android控件,它们的UI都是在应用程序的主线程中进行绘制的。而应用程序除了绘制外,还需要及时响应用户输入,否则,会引起ANR。为了避免ANR,对于一些游戏画面,摄像预览、视频播放等UI比较复杂,而且要求能够进行高效绘制的视图,它们的UI就不适合在应用程序的主线程中进行绘制。这时需要给此类视图生成一个独立的绘图表面,并使用一个独立的线程来绘制这些视图的UI,此时就必须使用SurfaceView进行开发。

二、SurfaceView是什么?

1、定义:

  继承自View,该类内嵌了一个专门用于绘制的Surface。你可以控制这个Surface的格式和尺寸。SurfaceView控制这个Surface的绘制位置。

  Surface是纵深排序(Z-ordered)的,这表明它总在自己所在窗口的后面。Surfaceview提供了一个可见区域,只有在这个可见区域内 的surface部分内容才可见,可见区域外的部分不可见。Surface的排版显示受到视图层级关系的影响,它的兄弟视图结点会在顶端显示。这意味者 surface的内容会被它的兄弟视图遮挡,这一特性可以用来放置遮盖物(overlays)(例如,文本和按钮等控件)。注意,如果surface上面 有透明控件,那么它的每次变化都会引起框架重新计算它和顶层控件的透明效果,这会影响性能。

2、负责绘制UI的SurfaceFlinger:

  讲到Surface,在此需要简要介绍一下SurfaceFlinger,一个负责绘制Android应用程序UI的服务。SurfaceFlinger服务运行在Android系统的System进程中,它负责管理Android系统的帧缓冲区(Frame Buffer)。Android应用程序为了能够将自己的UI绘制在系统的帧缓冲区上,就必须要与SurfaceFlinger服务进行通信.具体流程概括为:

    (1) Android应用程序请求SurfaceFlinger服务创建Surface;

    (2) Surface创建后,Android应用程序在上面绘制自己的UI;

     (3) 再请求SurfaceFlinger服务将已经绘制好UI的Surface渲染到设备显示屏上。

    技术图片

 

  一般来说,每一个窗口在SurfaceFlinger服务中都对应有一个Layer,用来描述它的绘图表面。对于那些具有SurfaceView的窗口来说,每一个SurfaceView在SurfaceFlinger服务中还对应有一个独立的Layer或者LayerBuffer,用来单独描述它的绘图表面,以区别于它的宿主窗口的绘图表面。

  由于拥有独立的绘图表面,因此SurfaceView的UI就可以在一个独立的线程中进行绘制。又由于不会占用主线程资源,SurfaceView一方面 可以实现复杂而高效的UI,另一方面又不会导致用户输入得不到及时响应。

3、单独介绍SurfaceView的一个成员变量:(仅做了解)

  SurfaceView类的成员变量mRequestedType描述的是SurfaceView的绘图表面的类型,有以下四个值:摘自源码。  

SURFACE_TYPE_NORMAL:用RAM缓存原生数据的普通Surface 
SURFACE_TYPE_HARDWARE:适用于DMA(Direct memory access )引擎和硬件加速的Surface 
SURFACE_TYPE_GPU:适用于GPU加速的Surface 
SURFACE_TYPE_PUSH_BUFFERS:表明该Surface不包含原生数据,Surface用到的数据由其他对象提供,在Camera图像预览中就使用该类型的Surface,有Camera负责提供给预览Surface数据,这样图像预览会比较流畅。如果设置这种类型则就不能调用lockCanvas来获取Canvas对象了。

  需要说明的是,虽然SurfaceHolder中已经不建议使用setType()方法了,我们自己写demo也可以看到该方法已经被声明为@Deprecated

    /**
     * Sets the surface‘s type.
     *
     * @deprecated this is ignored, this value is set automatically when needed.
     */
    @Deprecated
    public void setType(int type);

  但是,在使用自定义的SurfaceView实现视频播放时,还为了兼容低版本,仍需要设置setType

  //设置SurfaceHolder类型,为了兼容低版本,需要设置此类型,否则播放时,只有声音,而没有图像
    getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

4、关于View的setWillNotDraw():

  查看SurfaceView的源码可知,在其构造函数中调用了setWillNotDraw(true);该方法会导致draw()、onDraw()都不执行。

 /**
     * If this view doesn‘t do any drawing on its own, set this flag to
     * allow further optimizations. By default, this flag is not set on
     * View, but could be set on some View subclasses such as ViewGroup.
     *
     * Typically, if you override @link #onDraw(android.graphics.Canvas)
     * you should clear this flag.
     *
     * @param willNotDraw whether or not this View draw on its own
     */
    public void setWillNotDraw(boolean willNotDraw) 
        setFlags(willNotDraw ? WILL_NOT_DRAW : 0, DRAW_MASK);
    

  自定义SurfaceView,运行结果如下:

(1)默认执行结果:

技术图片

(2)在CommSurfaceView的构造函数中,手动设置setWillNotDraw(false),运行结果如下:

技术图片

三、SurfaceView的实现过程:(详情请参看原文)

  SurfaceView的整个实现过程分为三步:绘图表面的创建、在宿主窗口设置一块透明区域用于显示、在独立线程中进行自己UI的绘制。

  1.、创建独立的绘图表面;

  2、在宿主窗口上设置一块透明区域来显示自己(使绘制的UI可见);【SurfaceView的窗口类型所表示的Z轴位置小于Activity窗口的Z轴位置

  3、在独立的线程中进行其UI绘制。

四、SurfaceView的使用:

  由于SurfaceView的底层实现过程已经进行了封装,并为开发者提供了上层使用接口,即:系统给SurfaceView提供了一个专门绘图的Surface,嵌入在了SurfaceView视图层中,画面在Surface中绘制完成,在SurfaceView中通过获得SurfaceHolder的对象,管理并展示Surface的数据内容。所以,开发时只需按照下述步骤即可:  

1、一般都是重新自定义SurfaceView,起到封装的作用:

(1)继承SurfaceView;

(2)利用getHolder()获取SurfaceHolder对象;

(3)给SurfaceHolder对象添加实现SurfaceHolder.Callback接口的对象,即添加回调函数SurfaceHolder.addCallback(callback);

(4)重写Callback的三个方法:surfaceChanged,surfaceCreated,surfaceDestroyed;

(5)利用SurfaceHolder对象设置其类型、格式、大小等。

 

  (一)在SurfaceView上进行UI绘制的流程如下:  

(1). 在绘图表面的基础上建立一块画布,即获得一个Canvas对象。

(2). 利用Canvas类提供的绘图接口在前面获得的画布上绘制任意的UI。

(3). 将已经填充好UI数据的画布缓冲区提交给SurfaceFlinger服务,以便SurfaceFlinger服务可以将它合成到屏幕上去。

  代码格式:
  Canvas c=surfaceHolder.lockCanvas();
  .....具体画图操作......
  surfaceHolder.unlockCanvasAndPost(c);

  (二)利用SurfaceView播放视频:

    Android中有三种实现形式,均可实现视频的播放:

    • 原生VideoView(继承SurfaceView)
    • 直接使用SurfaceView
    • 自定义播放控件(继承SurfaceView)

具体视频的播放均是使用的MediaPlayer,只是在自定义及VideoView中,被封装在各自的类中,对调用者不可见。

视频的画面一般由两部分组成:视频内容画面+上层操作布局(标题、播放进度、时长、暂停等控件)。

为了配合上层操作布局,一般都会实现MediaPlayerControl接口(当然,完全可以自定义),获取视频播放相关信息。

 

    以原生VideoView为例,展现代码格式:
    VideoView  nativeVideoView = (VideoView) findViewById(R.id.nativeVideoView);
    MediaController  nativeMediaController = new MediaController(this);

    //设置播放路径:实际内部封装了MediaPlayer的创建及数据源、监听事件、播放类型、画面显示等的设置,即视频播放前的准备工作,
    //其中 mMediaPlayer.setDisplay(mSurfaceHolder);即是画面相关的设置
    nativeVideoView.setVideoPath(path);
    nativeVideoView.setMediaController(nativeMediaController);     nativeVideoView.requestFocus();     nativeVideoView.start();//内部执行mediaPlayer.start()

五、常见问题:(以VideoView为例)

  1、在播放视频时,滑动进度,会出现闪屏(画面一直在变)?

  原因:原生VideoView使用SeekBar,在滑动进度的过程中,在OnSeekBarChangeListener的onProgressChanged()中调用了MediaPlayerControl的seekTo(),即实时更新视频画面,出现闪屏。

  

 1 源码:frameworks\\base\\core\\java\\android\\widget\\MediaController.java
 2 
 3     private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() 
 4         @Override
 5         public void onStartTrackingTouch(SeekBar bar) 
 6             show(3600000);
 7 
 8             mDragging = true;
 9 
10             // By removing these pending progress messages we make sure
11             // that a) we won‘t update the progress while the user adjusts
12             // the seekbar and b) once the user is done dragging the thumb
13             // we will post one of these messages to the queue again and
14             // this ensures that there will be exactly one message queued up.
15             removeCallbacks(mShowProgress);
16         
17 
18          @Override
19         public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) 
20             if (!fromuser) 
21                 // We‘re not interested in programmatically generated changes to
22                 // the progress bar‘s position.
23                 return;
24             
25 
26             long duration = mPlayer.getDuration();
27             long newposition = (duration * progress) / 1000L;
28             mPlayer.seekTo( (int) newposition);   //实时更新播放画面
29             if (mCurrentTime != null)
30                 mCurrentTime.setText(stringForTime( (int) newposition));
31         
32 
33         @Override
34         public void onStopTrackingTouch(SeekBar bar) 
35             mDragging = false;
36             setProgress();
37             updatePausePlay();
38             show(sDefaultTimeout);
39 
40             // Ensure that progress is properly updated in the future,
41             // the call to show() does not guarantee this because it is a
42             // no-op if we are already showing.
43             post(mShowProgress);
44         
45     ;    

  处理:解决此问题,可将视频画面的更新放在滑动结束(即onStopTrackingTouch()中)即可。

  引入新的问题:若用户看视频,需要从30分钟开始播放,但是具体位置不确定,因此需要在30分钟附近查看,此时则需要随着用户的滑动,画面切换,
但是,又不能出现上述闪屏问题。

  新问题的解决方案:可以监听两次滑动的时间间隔,然后画面更新(例:两次间隔500ms更新一次画面)

  测试结果:证实可以。具体为在滑动过程中即onProgressChanged()中进行如下操作:  

//startTouchTime:首次为拖动开始的时间,之后为每次更新时重新赋值更新时的时间         
//changeTouchTime:时刻获取滑动变化的时间
            
            if(mDragging && changeTouchTime - startTouchTime >= 500)
                Log.e(TAG, "prepare fromuser onProgressChanged ++++++++++++++  ");
                startTouchTime = changeTouchTime;                    //重置,最近一次修改
                mPlayer.seekTo(newPosition);
                if (currentTime != null) 
                    currentTime.setText(stringForTime(newPosition));
                
            

  2、横竖屏切换时,视频从头播放,不会连续。

  原因:默认情况下, Activity在横竖屏切换时会重新创建,因此视频重播.结合是否设置属性对Activity的生命周期的影响,即可明白。

  解决方案:横竖屏切换必须在AndroidManifest.xml中设置属性android:configChanges="screenSize|orientation"。

  3、播放完成,停止时,播放进度未显示在最后?(该问题是我的项目中出现的,并非通用问题)

  现象:由于视频播放完成,做退出处理,一般看不到此现象。  

  原因:根据视频“剪辑”时,进度可以完全显示。查找原因,发现剪辑视频时,整个过程中进度条可见,一直在实时更新界面进度,因此可完全显示;而普通播放视频时,进度条等播放布局是可隐藏的,而代码中在MoviePlayer的setProgress()中,判断当进度等布局不可见时,直接返回,而不更新界面显示进度,因此,在播放完成时,界面显示的是最近一次布局可见时的进度(一般不在最后)。

  4、视频播放过程中,操作暂停播放后点击Home键退回桌面,再次进入会出现黑屏?【注:播放情况下正常是由于播放再次进入执行了继续播放,即刷新了界面】  

  原因:点击Home键时,SurfaceView中的Surface被销毁(从执行回调函数surfaceDestroy()即可看出),播放画面显示为黑色。

  测试现象:

      (1) 看到黑屏效果,是因为Activity背景被设置为黑色,在去掉背景色之后,发现仅有播放视频区域显示黑色;

      (2)将窗体设置为白色,按上述操作,发现仅视频播放区域为黑色;将主题设置为android:Theme.Translucent,按上述操作,发现仅视频播放区域为透明,即可看见MainActivity。这也可以解释SurfaceView的实现原理中的第二条,在宿主窗口中设置一块透明区域显示自己。运行效果图如下:MainActivity界面(左侧)、右侧为视频播放界面。

技术图片        技术图片

  

  得出结论:Surface被销毁后,播放区域显示的背景为当前主题Theme中的配置。

  5、其他问题:SurfaceView、GLSurfaceView、SurfaceTexture、TextureView的区别

  SurfaceView:Android1.0就有,继承自View,因为有单独的Surface【不在View hierachy】中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中,一些View中的特性也无法使用。

  GLSurfaceView:Android1.5引入,继承自SurfaceView,在SurfaceView的基础上,它加入了EGL的管理,并自带了渲染线程。另外它定义了用户需要实现的Render接口,提供了用Strategy pattern更改具体Render行为的灵活性。作为GLSurfaceView的Client,只需要将实现了渲染函数的Renderer的实现类设置给GLSurfaceView即可。

  SurfaceTexture:Android 3.0(API level 11)加入。单独的类,和SurfaceView不同的是,它对图像流的处理并不直接显示,而是转为GL外部纹理,因此可用于图像流数据的二次处理(如Camera滤镜,桌面特效等)。比如Camera的预览数据,变成纹理后可以交给GLSurfaceView直接显示,也可以通过SurfaceTexture交给TextureView作为View heirachy中的一个硬件加速层来显示。

  TextureView:在4.0(API level 14)中引入。继承自View,它可以将内容流直接投影到View中,可以用于实现Live preview【实时预览】等功能.和SurfaceView不同,它不会在WMS中单独创建窗口,而是作为View hierachy中的一个普通View,因此可以和其它普通View一样进行移动,旋转,缩放,动画等变化。值得注意的是TextureView必须在硬件加速的窗口中。它显示的内容流数据可以来自App进程或是远端进程。

六、总结:

  SurfaceView是android系统中特殊的视图,继承自View。它具有独立的绘图表面,但是,由于其窗口类型所表示的Z轴位置小于Activity窗口的Z轴位置,因此,为了使其可见,需要在宿主窗口申请设置一块透明区域来显示。一切条件就绪后,就可以在独立的线程中进行复杂的UI绘制,且不会影响应用程序的主线程响应用户输入。

  

以上是关于简述SurfaceView及常见问题的主要内容,如果未能解决你的问题,请参考以下文章

Android SurfaceView 绘图覆盖刷新及脏矩形刷新方法

SurfaceView及TextureView区别

Android笔记:SurfaceView与SurfaceHolder对象

SurfaceView

自己定义控件(2.2):SurfaceView和SurfaceHolder

Android游戏开发十九(必看篇)SurfaceView运行机制详解—剖析Back与Home按键及切入后台等异常处理!