我们在View中绘制的内容,是由系统绘制的,每隔16ms,系统发出一次VSYNC信号,重新绘制屏幕,这个操作在主线程,所以如果我们在两次绘制之间做的操作耗时超过16ms,页面就会出现卡顿。而SurfaceView则是由我们主动绘制,在子线程,不会卡主线程,同时,SurfaceView实现了双缓存机制。
摘抄一段关于双缓存的介绍
双缓冲技术是游戏开发中的一个重要的技术。当一个动画争先显示时,程序又在改变它,前面还没有显示完,程序又请求重新绘制,这样屏幕就会不停地闪烁。而双缓冲技术是把要处理的图片在内存中处理好之后,再将其显示在屏幕上。双缓冲主要是为了解决 反复局部刷屏带来的闪烁。把要画的东西先画到一个内存区域里,然后整体的一次性画出来。
1. SurfaceHolder
SurfaceHolder是一个接口,里面定义了使用SurfaceView的相关方法接口。比如,添加Surface状态改变时的回掉、获取画布,等。SurfaceView中维护了一个该类型的变量,我们的操作都是通过这个叫做holder的变量。
2. SurfaceHolder.Callback
因为SurfaceView是我们主动绘制的,所以我们就需要知道其状态,以确保在绘制的时候它是存在且可用的,通过添加此Callback来接收Surface状态发生改变时的回调。Surface有两种状态,即,已创建和已销毁。
3. 使用
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 
 | class CustomerSurfaceView : SurfaceView, SurfaceHolder.Callback, Runnable {
 constructor(ctx: Context) : super(ctx)
 constructor(ctx: Context, attrs: AttributeSet) : super(ctx, attrs)
 
 init {
 holder.addCallback(this)
 }
 
 @Volatile
 private var running = false
 private var thread: Thread? = null
 private var nextDraw = 0L
 
 private val engine: GameEngine = GameEngine(context)
 
 override fun surfaceCreated(holder: SurfaceHolder) {
 engine.begin()
 running = true
 thread = Thread(this)
 thread?.start()
 }
 
 override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
 }
 
 override fun surfaceDestroyed(holder: SurfaceHolder) {
 running = false
 if (thread == null) return
 synchronized(holder) {
 while (true) {
 try {
 val t = thread!!
 t.join(3000)
 break
 } catch (e: Exception) {
 Log.e("==", e.toString())
 }
 }
 }
 thread = null
 engine.end()
 holder.surface.release()
 }
 
 override fun run() {
 while (running) {
 while (System.currentTimeMillis() < nextDraw) {
 Thread.yield()
 }
 if (!running) return
 if (holder == null) {
 return
 }
 val start = System.currentTimeMillis()
 synchronized(holder) {
 val canvas = holder.lockCanvas()
 if (canvas != null) {
 try {
 engine.draw(canvas)
 } catch (e: Exception) {
 Log.e("==", e.toString())
 } finally {
 try {
 holder.unlockCanvasAndPost(canvas)
 } catch (e: Exception) {
 Log.e("==", e.toString())
 }
 }
 }
 }
 nextDraw = start + CraftEngine.FRAME_INTERVAL
 }
 }
 
 @SuppressLint("ClickableViewAccessibility")
 override fun onTouchEvent(event: MotionEvent?): Boolean {
 return gestureDetector.onTouchEvent(event)
 }
 
 private val gestureDetector = GestureDetector(context, object :
 GestureDetector.SimpleOnGestureListener() {
 
 private lateinit var downEvent: MotionEvent
 
 override fun onDown(e: MotionEvent): Boolean {
 downEvent = e
 return true
 }
 
 override fun onScroll(
 e1: MotionEvent?,
 e2: MotionEvent?,
 distanceX: Float,
 distanceY: Float
 ): Boolean {
 engine.onTouchMove(distanceX, distanceY)
 return true
 }
 })
 }
 
 | 
我用它写了一个飞机大战的游戏,创建一个类继承SurfaceView,为了监听它的状态,直接用它实现了Callback,然后在类初始化里,通过holder添加了监听,在回调方法里就可以处理相关的逻辑。
简单说就是,在创建后,启动一个子线程绘制,绘制的画布通过holder锁定获取,绘制完成后需要解锁,在销毁后,将绘制线程停止,节省资源。
这个类里面主要负责处理Surface状态改变,并处理绘制线程,至于具体的游戏逻辑,我都放到了GameEngine里,这里面处理飞机的生成、移动、攻击、碰撞,等。
实时绘制自然视觉效果最好,但由于肉眼的生理限制,当两帧之间的间隔不超过16ms,肉眼就感觉不到卡顿,这样看来1ms就绘制一次,或者5ms就绘制一次就没有必要了,浪费资源,所以,还需要控制一下绘制间隔,控制在16ms即可。
这里有个小坑,开始我的设计是,每次绘制直接交给engine处理,包括帧距控制,SurfaceView里的while循环里只调用了一个engine的draw方法,但是,这种写法,在我的测试机上切换页面之后,Surface的销毁回调就会出问题,要么是不回调,要么就是等一会才会回调。四处查了一些资料,折腾了好久,但也没找到有效的解决方法,最后偶然一试,发现把帧距控制放到SurfaceView里,也就是在while循环里做点事,也就是像上面那些写,甚至只是在里面打行log,问题就莫名其妙的不在了。我想这里面一定还有着什么我所不知道的小秘密,暂时也不管它了,又不是不能用。
需要注意的是,holder每次拿到的画布,都是上一次绘制完的画布,需要手动处理脏区域。拿我做的飞机来说,如果每次只管把飞机绘制到最新位置,那么屏幕上就会出现一串飞机拉线,等同于,每次绘制时拿到的纸,就是上次交出去的纸,所以需要先把上个位置的飞机擦掉,然后再在最新的位置上画上飞机。当然,最简单的办法就是每次都绘制一遍背景图,用背景图盖住上次的内容。另外,为了进一步提高性能,可以不用每次都绘制整张画布,而是选择只绘制需要绘制的区域,lockCanvas有个重载方法,传入一个需要绘制的区域,就可以获取到对应的画布。
此外,我还加了一个GestureDetector,玩游戏难免需要有用户操作输入,通过GestureDetector可以最简单的获取到用的操作。
线程等待这里用的是Thread的yield方法,而不是sleep方法,这二者的区别是,sleep方法会一直占用线程,让卡在这里等着,而yield,顾名思义,大喊一声,你们谁要用就拿去用,我现在还不用,然后释放掉CPU时间片,将自己变成就绪状态。而CPU在被释放掉之后,会再次从当前所有处于就绪状态的线程中,选一个执行,所以这个时候,有可能还会选中刚刚yield的线程。