我们在View中绘制的内容,是由系统绘制的,每隔16ms,系统发出一次VSYNC信号,重新绘制屏幕,这个操作在主线程,所以如果我们在两次绘制之间做的操作耗时超过16ms,页面就会出现卡顿。而SurfaceView则是由我们主动绘制,在子线程,不会卡主线程,同时,SurfaceView实现了双缓存机制。
摘抄一段关于双缓存的介绍
双缓冲技术是游戏开发中的一个重要的技术。当一个动画争先显示时,程序又在改变它,前面还没有显示完,程序又请求重新绘制,这样屏幕就会不停地闪烁。而双缓冲技术是把要处理的图片在内存中处理好之后,再将其显示在屏幕上。双缓冲主要是为了解决 反复局部刷屏带来的闪烁。把要画的东西先画到一个内存区域里,然后整体的一次性画出来。
1. SurfaceHolder
SurfaceHolder是一个接口,里面定义了使用SurfaceView的相关方法接口。比如,添加Surface状态改变时的回掉、获取画布,等。SurfaceView中维护了一个该类型的变量,我们的操作都是通过这个叫做holder的变量。
2. SurfaceHolder.Callback
因为SurfaceView是我们主动绘制的,所以我们就需要知道其状态,以确保在绘制的时候它是存在且可用的,通过添加此Callback来接收Surface状态发生改变时的回调。Surface有两种状态,即,已创建和已销毁。
3. 使用
1 2 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的线程。