oynix

于无声处听惊雷,于无色处见繁花

SurfaceView的使用

我们在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的线程。

------------- (完) -------------
  • 本文作者: oynix
  • 本文链接: https://oynix.com/2022/04/7d919f167132/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!

欢迎关注我的其它发布渠道