Android SurfaceView详解

https://rainmonth.github.io/posts/A191103.html

摘要

本文分析的是API 24版本的SurfaceView(即Android N版本)通常情况下程序的View和用户响应都是在同一个线程(即主线程)中处理的,这就要求一些耗时操作(如网络请求、数据库查询)需要放在非UI线程中造作,否则就会造成ANR。View是通过刷新来重绘视图,系统通过发送VSSYNC信号来进行屏幕的重绘,这个信号的发送间隔是16ms,如果我们16ms内不能完成了绘制,就会表现出UI上的卡段。这就使得在一些逻辑很复杂或频繁更新的视图绘制方面显得比较吃力。那么到底能不能在非UI线程中进行绘制、更新、渲染UI的操作呢?怎么应对那种复杂层级或者频繁更新的UI绘制呢?关于这两个问题,接下来的SurfaceView会给我们答案。

SurfaceView在底层实现了游戏的双缓冲技术,这就可以很轻松的应对频繁更新这种场景下的视图绘制;其持有的Surface对象会开启一个独立线程去进行UI的渲染绘制,这就说明SurfaceView可以在非UI线程中进行UI的绘制操作。
与其说是SurfaceView可以在非UI线程中进行UI相关操作,不如说是其持有的surface对象提供了这种能力。SurfaceView的官方注释(我看的是API 24版本的)已将说明的很清楚了。

Provides a dedicated drawing surface embedded inside of a view hierarchy.
You can control the format of this surface and, if you like, its size; the
SurfaceView takes care of placing the surface at the correct location on the
screen

简单翻译一下就是:提供了一个嵌入在View层级中的专门用来绘图的Surface。你可以控制这个Surface的格式甚至是大小,而我们的SurfaceView只关心如何将这个Surface放在正确的位置。

特点

  • Surface是Z序的,其持有者SurfaceView位于当前视图树上(即SurfaceView位于当前的Window上),那么Surface就位于当前的Window下面;按道理他应该是被覆盖的,但SurfaceView会在当前视图树所在的窗口上”挖个洞“让其显示出来。至于SurfaceView的兄弟View(即同层级的View),有View Hierarchy来控制他们的中确显示;
  • SurfaceView上面的同层级的View如果应用了post-layout变换属性的话,会导致其不能和SurfaceView的surface很好的融合在一起;
  • 可以通过SurfaceView的SurfaceHolder接口来获取surface(先调用getHolder()方法来获取SurfaceHolder的实现,在进行Surface相关操作);
  • 当SurfaceView的Window可见时surface就被创建了。通过实现在SurfaceHolder.Callback的surfaceCreated和surfaceDestroyed来获取Surface的穿件与销毁时机;
  • 由于surface的绘制发送在另一个线程,所以在使用的时候要注意以下几个方面的线程同步:
    • 所有的SurfaceView和SurfaceHolder.Callback方法都是在应用主线程中被调用的,所以需要控制好线程间的状态同步;
    • 只有在SurfaceHolder.Callback的surfaceCreated和surfaceDestroyed期间获取的surface才是合法的状态;
  • 在Android N 之后(包括Android N),SurfaceView的Window的位置是随着其他视图的渲染而同步更新的,所以对SurfaceView应用动画时没啥影响的;但在这之前是有影响的,因为SurfaceView的位置是异步更新的。

具体分析

SurfaceView的构造同普通View的构造,没什么好分析的,主要看看它的以下关键成员变量和关键方法;

关键变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
final ArrayList<SurfaceHolder.Callback> mCallbacks
= new ArrayList<SurfaceHolder.Callback>();

final int[] mLocation = new int[2];

// 一个可重入锁(在updateSurface和lockCanvas时使用)
final ReentrantLock mSurfaceLock = new ReentrantLock();
final Surface mSurface = new Surface(); // Current surface in use
boolean mDrawingStopped = true;
// We use this to track if the application has produced a frame
// in to the Surface. Up until that point, we should be careful not to punch
// holes.
boolean mDrawFinished = false;

final Rect mScreenRect = new Rect();
// 连接SurfaceView和SurfaceFlinger的桥梁,后者提供了创建销毁Surface的方法
SurfaceSession mSurfaceSession;
// 提供Surface的控制
SurfaceControl mSurfaceControl;
// In the case of format changes we switch out the surface in-place
// we need to preserve the old one until the new one has drawn.
SurfaceControl mDeferredDestroySurfaceControl;
// SurfaceView的surface的持有者,外界通过调用getHolder来获取mSurfaceHolder从而操作surface
private final SurfaceHolder mSurfaceHolder

关键方法

updateSurface()
该方法主要就是更新surface,分析部分都在注释里

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
protected void updateSurface() {
if (!mHaveFrame) {
return;
}
ViewRootImpl viewRoot = getViewRootImpl();
if (viewRoot == null || viewRoot.mSurface == null || !viewRoot.mSurface.isValid()) {
return;
}

mTranslator = viewRoot.mTranslator;
if (mTranslator != null) {
mSurface.setCompatibilityTranslator(mTranslator);
}

int myWidth = mRequestedWidth;
if (myWidth <= 0) myWidth = getWidth();
int myHeight = mRequestedHeight;
if (myHeight <= 0) myHeight = getHeight();

final boolean formatChanged = mFormat != mRequestedFormat;
final boolean visibleChanged = mVisible != mRequestedVisible;
final boolean creating = (mSurfaceControl == null || formatChanged || visibleChanged)
&& mRequestedVisible;
final boolean sizeChanged = mSurfaceWidth != myWidth || mSurfaceHeight != myHeight;
final boolean windowVisibleChanged = mWindowVisibility != mLastWindowVisibility;
boolean redrawNeeded = false;
// 处于以下状态中的任何一种,都要进行更新
if (creating || formatChanged || sizeChanged || visibleChanged || windowVisibleChanged) {
getLocationInWindow(mLocation);
。。。
try {
// 计算出要在屏幕中绘制的矩形区域
final boolean visible = mVisible = mRequestedVisible;
mWindowSpaceLeft = mLocation[0];
mWindowSpaceTop = mLocation[1];
mSurfaceWidth = myWidth;
mSurfaceHeight = myHeight;
mFormat = mRequestedFormat;
mLastWindowVisibility = mWindowVisibility;

mScreenRect.left = mWindowSpaceLeft;
mScreenRect.top = mWindowSpaceTop;
mScreenRect.right = mWindowSpaceLeft + getWidth();
mScreenRect.bottom = mWindowSpaceTop + getHeight();
if (mTranslator != null) { // 类似于移动画布操作
mTranslator.translateRectInAppWindowToScreen(mScreenRect);
}

final Rect surfaceInsets = getParentSurfaceInsets();
mScreenRect.offset(surfaceInsets.left, surfaceInsets.top);

if (creating) { // 通过SurfaceSession来链接SurfaceFlinger,其构造函数中会调用nativeCreate方法创建surface
mSurfaceSession = new SurfaceSession(viewRoot.mSurface);
mDeferredDestroySurfaceControl = mSurfaceControl;

updateOpaqueFlag();
// 创建SurfaceControl
mSurfaceControl = new SurfaceControlWithBackground(mSurfaceSession,
"SurfaceView - " + viewRoot.getTitle().toString(),
mSurfaceWidth, mSurfaceHeight, mFormat,
mSurfaceFlags);
} else if (mSurfaceControl == null) {
return;
}

boolean realSizeChanged = false;

// 获取锁,同步操作
mSurfaceLock.lock();
try {
mDrawingStopped = !visible;

if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+ "Cur surface: " + mSurface);

SurfaceControl.openTransaction();
try {
mSurfaceControl.setLayer(mSubLayer);
if (mViewVisibility) {
mSurfaceControl.show();
} else {
mSurfaceControl.hide();
}

// While creating the surface, we will set it's initial
// geometry. Outside of that though, we should generally
// leave it to the RenderThread.
//
// There is one more case when the buffer size changes we aren't yet
// prepared to sync (as even following the transaction applying
// we still need to latch a buffer).
// b/28866173
if (sizeChanged || creating || !mRtHandlingPositionUpdates) {
mSurfaceControl.setPosition(mScreenRect.left, mScreenRect.top);
mSurfaceControl.setMatrix(mScreenRect.width() / (float) mSurfaceWidth,
0.0f, 0.0f,
mScreenRect.height() / (float) mSurfaceHeight);
}
if (sizeChanged) {
mSurfaceControl.setSize(mSurfaceWidth, mSurfaceHeight);
}
} finally {
SurfaceControl.closeTransaction();
}

if (sizeChanged || creating) {
redrawNeeded = true;
}

mSurfaceFrame.left = 0;
mSurfaceFrame.top = 0;
if (mTranslator == null) {
mSurfaceFrame.right = mSurfaceWidth;
mSurfaceFrame.bottom = mSurfaceHeight;
} else {
float appInvertedScale = mTranslator.applicationInvertedScale;
mSurfaceFrame.right = (int) (mSurfaceWidth * appInvertedScale + 0.5f);
mSurfaceFrame.bottom = (int) (mSurfaceHeight * appInvertedScale + 0.5f);
}

final int surfaceWidth = mSurfaceFrame.right;
final int surfaceHeight = mSurfaceFrame.bottom;
realSizeChanged = mLastSurfaceWidth != surfaceWidth
|| mLastSurfaceHeight != surfaceHeight;
mLastSurfaceWidth = surfaceWidth;
mLastSurfaceHeight = surfaceHeight;
} finally {
mSurfaceLock.unlock();
}

try {
redrawNeeded |= visible && !mDrawFinished;

SurfaceHolder.Callback callbacks[] = null;

final boolean surfaceChanged = creating;
// 已将创建了但surfaceChanged且有可见变为不可见时,需要先销毁surface
if (mSurfaceCreated && (surfaceChanged || (!visible && visibleChanged))) {
mSurfaceCreated = false;
if (mSurface.isValid()) {
if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+ "visibleChanged -- surfaceDestroyed");
// 获取到SurfaceView的订阅者列表
callbacks = getSurfaceCallbacks();
// 通知surfaceDestroyed
for (SurfaceHolder.Callback c : callbacks) {
// 发布surfaceDestroyed
c.surfaceDestroyed(mSurfaceHolder);
}
// Since Android N the same surface may be reused and given to us
// again by the system server at a later point. However
// as we didn't do this in previous releases, clients weren't
// necessarily required to clean up properly in
// surfaceDestroyed. This leads to problems for example when
// clients don't destroy their EGL context, and try
// and create a new one on the same surface following reuse.
// Since there is no valid use of the surface in-between
// surfaceDestroyed and surfaceCreated, we force a disconnect,
// so the next connect will always work if we end up reusing
// the surface.
if (mSurface.isValid()) {
mSurface.forceScopedDisconnect();
}
}
}

if (creating) {
mSurface.copyFrom(mSurfaceControl);
}

if (sizeChanged && getContext().getApplicationInfo().targetSdkVersion
< Build.VERSION_CODES.O) {
// Some legacy applications use the underlying native {@link Surface} object
// as a key to whether anything has changed. In these cases, updates to the
// existing {@link Surface} will be ignored when the size changes.
// Therefore, we must explicitly recreate the {@link Surface} in these
// cases.
mSurface.createFrom(mSurfaceControl);
}

if (visible && mSurface.isValid()) {
if (!mSurfaceCreated && (surfaceChanged || visibleChanged)) {
mSurfaceCreated = true;
mIsCreating = true;
if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+ "visibleChanged -- surfaceCreated");
if (callbacks == null) {
callbacks = getSurfaceCallbacks();
}
// 通知surfaceCreated
for (SurfaceHolder.Callback c : callbacks) {
c.surfaceCreated(mSurfaceHolder);
}
}
if (creating || formatChanged || sizeChanged
|| visibleChanged || realSizeChanged) {
if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+ "surfaceChanged -- format=" + mFormat
+ " w=" + myWidth + " h=" + myHeight);
if (callbacks == null) {
callbacks = getSurfaceCallbacks();
}
// 通知surfaceChanged
for (SurfaceHolder.Callback c : callbacks) {
c.surfaceChanged(mSurfaceHolder, mFormat, myWidth, myHeight);
}
}
if (redrawNeeded) {
if (DEBUG) Log.i(TAG, System.identityHashCode(this) + " "
+ "surfaceRedrawNeeded");
if (callbacks == null) {
callbacks = getSurfaceCallbacks();
}

mPendingReportDraws++;
viewRoot.drawPending();
SurfaceCallbackHelper sch =
new SurfaceCallbackHelper(this::onDrawFinished);
sch.dispatchSurfaceRedrawNeededAsync(mSurfaceHolder, callbacks);
}
}
} finally {
mIsCreating = false;
if (mSurfaceControl != null && !mSurfaceCreated) {
mSurface.release();
// If we are not in the stopped state, then the destruction of the Surface
// represents a visual change we need to display, and we should go ahead
// and destroy the SurfaceControl. However if we are in the stopped state,
// we can just leave the Surface around so it can be a part of animations,
// and we let the life-time be tied to the parent surface.
if (!mWindowStopped) {
mSurfaceControl.destroy();
mSurfaceControl = null;
}
}
}
} catch (Exception ex) {
Log.e(TAG, "Exception configuring surface", ex);
}
if (DEBUG) Log.v(
TAG, "Layout: x=" + mScreenRect.left + " y=" + mScreenRect.top
+ " w=" + mScreenRect.width() + " h=" + mScreenRect.height()
+ ", frame=" + mSurfaceFrame);
} else {
// Calculate the window position in case RT loses the window
// and we need to fallback to a UI-thread driven position update
getLocationInSurface(mLocation);
final boolean positionChanged = mWindowSpaceLeft != mLocation[0]
|| mWindowSpaceTop != mLocation[1];
final boolean layoutSizeChanged = getWidth() != mScreenRect.width()
|| getHeight() != mScreenRect.height();
if (positionChanged || layoutSizeChanged) { // Only the position has changed
mWindowSpaceLeft = mLocation[0];
mWindowSpaceTop = mLocation[1];
// For our size changed check, we keep mScreenRect.width() and mScreenRect.height()
// in view local space.
mLocation[0] = getWidth();
mLocation[1] = getHeight();

mScreenRect.set(mWindowSpaceLeft, mWindowSpaceTop,
mWindowSpaceLeft + mLocation[0], mWindowSpaceTop + mLocation[1]);

if (mTranslator != null) {
mTranslator.translateRectInAppWindowToScreen(mScreenRect);
}

if (mSurfaceControl == null) {
return;
}

if (!isHardwareAccelerated() || !mRtHandlingPositionUpdates) {
try {
if (DEBUG) Log.d(TAG, String.format("%d updateSurfacePosition UI, " +
"postion = [%d, %d, %d, %d]", System.identityHashCode(this),
mScreenRect.left, mScreenRect.top,
mScreenRect.right, mScreenRect.bottom));
setParentSpaceRectangle(mScreenRect, -1);
} catch (Exception ex) {
Log.e(TAG, "Exception configuring surface", ex);
}
}
}
}
}

从上面的代码可以看出,SurfaceHolder.Callback定义的方法都在updateSurface中被调用。

具体使用

SurfaceView在Android上有两个系统实现,分别是GLSurfaceView和VideoView,前者专门负责OpenGL渲染的,后者负责视频渲染的。当然我们也可以自定义SurfaceView。

GLSurfaceView

GLSurfaceView 通常在游戏中使用,更详细的使用可以参见OpenGL 开发指南。

特点

  • 管理一个surface对象,该surface是一块能被组装到Android视图系统的特殊内存;

  • 管理一个EGL display对象,该对象可以把OpenGL内容渲染到surface中;

  • 接收一个用户自定义的渲染器来完成真正的渲染工作;

  • 在一个专门的线程中渲染内容

  • 支持按序(on-demand)渲染和持续(continuous)渲染;

  • 支持调试OpenGL的调用

    基本使用

  1. 创建GLSurfaceView对象(没有特需要求的话,直接使用GLSurfaceView,然后通过其提供的set方法来配置GLSurfaceView,如setReanderer());

    1. 初始化GLSurfaceView,在调用setRenderer之前,先进行GLSurfaceView的配置,可参见其他set方法;
    2. 指定Surface,默认提供的是RGB_888格式的Surface,可以通过SurfaceHolder的setFormat来更改;
    3. 在开始渲染之前要指定EGLConfig(因为Android设备支持不同的渲染配置,且不同的配置表现不同);
    4. 指定调试模式,通过调用setDebugFlags来更改要调试的选项;
    5. 设置渲染器;
  2. 设置渲染模式,是按需渲染还是持续渲染;

    生命周期相关

    在Activity stops时请调用GLSurfaceView的onPause,在Activity starts时请调用GLSurfaceView的onResume,这样GLSurfaceView就能正确的通知渲染线程的工作状态,同时更好的管理OpenGL display这个对象。

    事件处理

    可以采用经典的时间处理模式来处理(即继承重写相关方法),但要利用线程同步机制来处理好UI线程(姑且说是主线程)和渲染线程(暂时称作子线程)之间的通信。

    VideoView

    用来展示视频的一个SurfaceView的实现,之所以姚勇SurfaceView,是因为视频流在短时间内存在复杂的渲染计算,SurfaceView很适合干这个工作。
    VideoView内部持有一个MediaPlayer,同时实现了MediaPlayerControl接口,可以提供基本的视频播放控制操作(如播放、暂停、拖动等)
    使用VideoView时应注意:

  3. 只要处理好VideoView的测量工作,它可以被使用在任意的布局管理器中,并且提供了不同的展示模式;

  4. VideoView进入后台后并不会保存它的全部状态,如播放状态、播放位置、播放轨道,需要我们在Activity的onSaveInstanceState和onRestoreInstanceState来手动管理;

    自定义SurfaceView

    要自定义SurfaceView,需要满足以下条件:

  • 继承SurfaceView
  • 实现SurfaceHolder中的Callback接口;

注意事项

  1. 在单个页面中不宜有过多SurfaceView实例,毕竟单独开始一个线程去完成绘制操作时非常消耗系统资源的;
  2. 一定要注意访问SurfaceView的surface时机的合法性(即在SurfaceHolder.Callback的surfaceCreated和surfaceDestroyed之间);