#一.概述
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能够正常刷新和完成操作响应。)
##.普通ViewSurfaceView的主要区别:
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的三个方法        **********/@Overridepublic void surfaceCreated(@NonNull SurfaceHolder holder) {ILog.d(TAG, "surfaceCreated()");initDrawSetting();//开始在TimerThread线程中执行绘制操作mTimer = new Timer();//每次都要新建,因为一旦cancel,就不能再次start()使用mTimerTask = new TimerTask() {@Overridepublic void run() {timeNow += mPeriod;draw();}};mTimer.schedule(mTimerTask, 0, mPeriod);}@Overridepublic void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {ILog.d(TAG, "surfaceChanged()");}@Overridepublic 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);}
}
相关笔记:
Android Surface & Canvas简介_丞恤猿的博客-CSDN博客
参考剪藏:
SurfaceView的源代码:
http://androidxref.com/9.0.0_r3/xref/frameworks/base/core/java/android/view/SurfaceView.java
(AndroidStudio中只能看到SurfaceView部分方法的方法名,看不到完整源代码)

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

Android SurfaceView总结及代码示例相关推荐

  1. 【Google Play】从 Android 应用中跳转到 Google Play 中 ( 跳转代码示例 | Google Play 页面的链接格式 | Google Play 免安装体验 )

    文章目录 前言 一.从 Android 应用跳转到 Google Play 代码 二.Google Play 页面的链接格式 三.Google Play 免安装体验 前言 本博客参考资料 链接到 Go ...

  2. 【Android 逆向】函数拦截 ( ARM 架构下的插桩拦截 | 完整代码示例 )

    文章目录 一.ARM 架构下的插桩拦截 二.完整代码示例 一.ARM 架构下的插桩拦截 ARM 架构下的跳转指令 : 下面的二进制数都是十六进制数 ; 323232 位指令 ; 04 F0 1F E5 ...

  3. 【IOC 控制反转】Android 事件依赖注入 ( 事件依赖注入代码示例 )

    文章目录 总结 一.Android 事件依赖注入示例 1.创建依赖注入库 2.声明注解 (1).修饰注解的注解 (2).修饰方法的注解 3.Activity 基类 4.动态代理类调用处理程序 5.依赖 ...

  4. 【IOC 控制反转】Android 视图依赖注入 ( 视图依赖注入步骤 | 视图依赖注入代码示例 )

    文章目录 总结 一.Android 视图依赖注入步骤 二.Android 布局依赖注入示例 1.创建依赖注入库 2.声明注解 3.Activity 基类 4.依赖注入工具类 5.客户端 Activit ...

  5. 【OkHttp】Android 项目导入 OkHttp ( 配置依赖 | 配置 networkSecurityConfig | 配置 ViewBinding | 代码示例 )

    OkHttp 系列文章目录 [OkHttp]OkHttp 简介 ( OkHttp 框架特性 | Http 版本简介 ) [OkHttp]Android 项目导入 OkHttp ( 配置依赖 | 配置 ...

  6. 【Android NDK 开发】Kotlin 语言中使用 NDK ( 创建支持 Kotlin 的 NDK 项目 | Kotlin 语言中使用 NDK 要点 | 代码示例 )

    文章目录 一.创建支持 Kotlin 的 NDK 项目 二.Kotlin 语言中使用 NDK 要点 1.加载动态库 2.声明 ndk 方法 3.Project 下的 build.gradle 配置 4 ...

  7. 【Android 异步操作】HandlerThread 示例 ( 初始化并执行 | 获取Looper | 获取 Handler | 获取消息队列 | 设置空闲队列 | 代码示例 )

    文章目录 一.HandlerThread 初始化 二.HandlerThread 获取Looper 三.HandlerThread 获取消息队列 MessageQueue 四.HandlerThrea ...

  8. 【Android 异步操作】线程池 ( 线程池使用示例 | 自定义线程池使用流程 | 自定义任务拒绝处理策略 | 完整代码示例 )

    文章目录 一.自定义线程池使用流程 二.自定义任务拒绝处理策略 三.完整代码示例 在博客 [Android 异步操作]线程池 ( 线程池简介 | 线程池初始化方法 | 线程池种类 | AsyncTas ...

  9. 【Android 应用开发】Google 官方 EasyPermissions 权限申请库 ( 完整代码示例 | 申请权限 | 申请权限原理对话框 | 引导用户手动设置权限对话框 )

    文章目录 一.申请权限 二.申请权限原理对话框 三.引导用户手动设置权限对话框 四.在 AndroidManifest.xml 中配置权限 五.完整代码示例 六.GitHub 地址 一.申请权限 申请 ...

最新文章

  1. Linux下多网卡MAC配置问题
  2. 计算机网络---非归零码、曼彻斯特编码和差分曼彻斯特编码
  3. 嵌入式linux硬件成本,嵌入式Linux驱动和固件有何区别?供应商是如何用固件压缩成本的?...
  4. 数字图像处理:第十八章 彩色图象处理
  5. DBA(三):MySQL主从同步、复制模式
  6. 架构师眼中的高并发架构
  7. PHP 学习 一 基础
  8. 3-4:类与对象中篇——默认成员函数之拷贝构造函数
  9. SQL Server 2008内存性能监控
  10. java html转图片_Python一键转Java?“Google翻译”你别闹
  11. 分布式、中间件、消息队列的工作模式
  12. zabbix系统日志文件监控key
  13. (附源码)小程序 平衡膳食小程序 毕业设计 250859
  14. STM32读取AD芯片ADS1110数据
  15. RationalDMIS 2020高级编程之提取数据OBTAIN语句
  16. 多个域名指向一个ip
  17. APP静默安装卸载管理器实现与上架到应用宝和豌豆荚
  18. Linux下core文件介绍与使用方法
  19. BOOT ROM 初始化内容、启动设备、镜像烧写
  20. 抖音跳转微信加好友功能实现解析

热门文章

  1. 清理linux服务器缓存,详解Linux手动释放缓存的方法
  2. win7安装vs2015问题总结
  3. 数据结构、数据类型、抽象数据类型之间的区别
  4. java实现购物车的原理及步骤_购物车的原理及实现
  5. 企业上云?也许只差一个代码生成器
  6. uni-app 日历组件的实现
  7. C#_Unit Testing 一(xUnit)
  8. 不离开电脑还能健身的简易方法
  9. BGP选路 ——起源属性 + MED属性 + EBGP优于IBGP属性
  10. NCBI 数据介绍和下载