Android SurfaceView 源码分析
01.Android中绘制模型
1.1 软件绘制模型
- 这里由CPU主导绘图,视图按照以下2个步骤绘图。
- 让视图结构(view hierarchy)失效。
- 绘制整个视图结构。
- 当应用程序需要更新它的部分UI时,都会调用内容发生改变的View对象的invalidate()方法。无效(invalidation)消息请求会在View对象层次结构中传递,以便计算出需要重绘的屏幕区域(脏区)。然后,Android系统会在View层次结构中绘制所有的跟脏区相交的区域。但是,这种方法有两个缺点:
- 绘制了不需要重绘的视图(与脏区域相交的区域)
- 掩盖了一些应用的bug(由于会重绘与脏区域相交的区域)
- 比如:在View对象的属性发生变化时,如背景色或TextView对象中的文本等,Android系统会自动的调用该View对象的invalidate()方法。
1.2 硬件加速绘制模型
- 这里由GPU主导绘图,视图按照以下3个步骤绘图。
- 让视图结构失效。
- 记录和更新显示列表(Display List)。
- 绘制显示列表。
- 这种模式下,Android系统依然会使用invalidate()方法和draw()方法来请求屏幕更新和展现View对象。但Android系统并不是立即执行绘制命令,而是首先把这些View的绘制函数作为绘制指令记录一个显示列表中,然后再读取显示列表中的绘制指令调用OpenGL相关函数完成实际绘制。另一个优化是,Android系统只需要针对由invalidate()方法调用所标记的View对象的脏区进行记录和更新显示列表。没有失效的View对象就简单重用先前显示列表记录的绘制指令来进行简单的重绘工作。
- 使用显示列表的目的是,把视图的各种绘制函数翻译成绘制指令保存起来,对于没有发生改变的视图把原先保存的操作指令重新读取出来重放一次就可以了,提高了视图的显示速度。而对于需要重绘的View,则更新显示列表,然后再调用OpenGL完成绘制。
- 在这种绘制模型下,我们不能依赖一个视图与脏区(dirty region)相交而导致它的draw()方法被自动调用,所以必须要手动调用该视图的invalidate()方法去更新显示列表。如果忘记这么做可能导致视图在改变后不会发生变化。
- 硬件加速提高了Android系统显示和刷新的速度,但它也不是万能的,它有三个缺陷:
- 兼容性(部分绘制函数不支持或不完全硬件加速
- 内存消耗(OpenGL API调用就会占用8MB,而实际上会占用更多内存
- 电量消耗(GPU耗电)
02.SurfaceView是什么
2.1 继承自类View
- 它继承自类View,因此它本质上是一个View。
- 但与普通View不同的是,它有自己的Surface。有自己的Surface,在WMS中有对应的WindowState,在SurfaceFlinger中有Layer。我们知道,一般的Activity包含的多个View会组成View hierachy的树形结构,只有最顶层的DecorView,也就是根结点视图,才是对WMS可见的。这个DecorView在WMS中有一个对应的WindowState。相应地,在SF中对应的Layer。
- 一般的View只能在UI线程中绘制,而SurfaceView却能在非UI线程中绘制,或者说SurfaceView则在一个子线程中去更新自己,这样的结果是即使SurfaceView频繁的刷新重绘也不会阻塞主线程导致卡顿甚至ANR。
- 而SurfaceView自带一个Surface,这个Surface在WMS中有自己对应的WindowState,
- 在SF中也会有自己的Layer。虽然在App端它仍在View hierachy中,但在Server端(WMS和SF)中,它与宿主窗口是分离的。这样的好处是对这个Surface的渲染可以放到单独线程去做,渲染时可以有自己的GL context。这对于一些游戏、视频等性能相关的应用非常有益,因为它不会影响主线程对事件的响应。
- 但它也有缺点,因为这个Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中,一些View中的特性也无法使用。
2.2 SurfaceView优缺点
- 优点:
- 可以在一个独立的线程中进行绘制,不会影响主线程
- 使用双缓冲机制,播放视频时画面更流畅
- 缺点:
- Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,也不能放在其它ViewGroup中。SurfaceView 不能嵌套使用。
2.3 使用SurfaceView播放视频
SurfaceView+MediaPlayer
- 使用SurfaceView播放一个视频流媒体。MediaPlayer播放视频需要SurfaceView的配合,SurfaceView主要用于显示MediaPlayer播放的视频流媒体的画面渲染。
- MediaPlayer也提供了相应的方法设置SurfaceView显示图片,只需要为MediaPlayer指定SurfaceView显示图像即可。
大概步骤如下所示
- 调用player.setDataSource()方法设置要播放的资源,可以是文件、文件路径、或者URL。
- 调用MediaPlayer.setDisplay(holder)设置surfaceHolder,surfaceHolder可以通过surfaceview的getHolder()方法获得。
- 调用MediaPlayer.prepare()来准备。
- 调用MediaPlayer.start()来播放视频。
- 注意:在setDisplay方法中,需要传递一个SurfaceHolder对象,SurfaceHolder可以理解为SurfaceView装载需要显示的一帧帧图像的容器,它可以通过SurfaceHolder.getHolder()方法获得。
代码如下所示,简单型的
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
29VideoSurfaceView videoSurfaceView = new VideoSurfaceView(mContext);
SurfaceHolder holder = videoSurfaceView.getHolder();
holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
holder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
});
mMediaPlayer.setDataSource(this, Uri.parse(url));
mMediaPlayer.setDisplay(holder);
mMediaPlayer.prepare()
mMediaPlayer.start()
//添加到视图中
LayoutParams params = new LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT, Gravity.CENTER);
mContainer.addView(videoSurfaceView, 0, params);
03.SurfaceView双缓冲
3.1 双缓冲机制由来
- 问题的由来
- CPU访问内存的速度要远远快于访问屏幕的速度。如果需要绘制大量复杂的图像时,每次都一个个从内存中读取图形然后绘制到屏幕就会造成多次地访问屏幕,从而导致效率很低。这就跟CPU和内存之间还需要有三级缓存一样,需要提高效率。
- 第一层缓冲
- 在绘制图像时不用上述一个一个绘制的方案,而采用先在内存中将所有的图像都绘制到一个Bitmap对象上,然后一次性将内存中的Bitmap绘制到屏幕,从而提高绘制的效率。Android中View的onDraw()方法已经实现了这一层缓冲。onDraw()方法中不是绘制一点显示一点,而是都绘制完后一次性显示到屏幕。
- 第二层缓冲
- onDraw()方法的Canvas对象是和屏幕关联的,而onDraw()方法是运行在UI线程中的,如果要绘制的图像过于复杂,则有可能导致应用程序卡顿,甚至ANR。因此我们可以先创建一个临时的Canvas对象,将图像都绘制到这个临时的Canvas对象中,绘制完成之后再将这个临时Canvas对象中的内容(也就是一个Bitmap),通过drawBitmap()方法绘制到onDraw()方法中的canvas对象中。这样的话就相当于是一个Bitmap的拷贝过程,比直接绘制效率要高,可以减少对UI线程的阻塞。
3.2 如何理解SurfaceView双缓冲
- 双缓冲在运用时可以理解为:
- SurfaceView在更新视图时用到了两张Canvas,一张frontCanvas和一张backCanvas,每次实际显示的是frontCanvas,backCanvas存储的是上一次更改前的视图,当使用lockCanvas()获取画布时,得到的实际上是backCanvas而不是正在显示的frontCanvas,之后你在获取到的backCanvas上绘制新视图,再unlockCanvasAndPost(canvas)此视图,那么上传的这张canvas将替换原来的frontCanvas作为新的frontCanvas,原来的frontCanvas将切换到后台作为backCanvas。例如,如果你已经先后两次绘制了视图A和B,那么你再调用lockCanvas()获取视图,获得的将是A而不是正在显示的B,之后你将重绘的C视图上传,那么C将取代B作为新的frontCanvas显示在SurfaceView上,原来的B则转换为backCanvas。
- 在网上还看到一段解释
- 双缓冲技术是游戏开发中的一个重要的技术。当一个动画争先显示时,程序又在改变它,前面还没有显示完,程序又请求重新绘制,这样屏幕就会不停地闪烁。而双缓冲技术是把要处理的图片在内存中处理好之后,再将其显示在屏幕上。双缓冲主要是为了解决 反复局部刷屏带来的闪烁。把要画的东西先画到一个内存区域里,然后整体的一次性画出来。
04.SurfaceView源码分析
4.1 为何不会阻塞Ui线程
首先看一下下面代码
- 实现 SurfaceHolder.Callback 接口,并且重写里面的三个方法
- 在 SurfaceHolder.Callback 的 surfaceCreated 方法中开启一个线程进行图像的绘制
- 在 SufaceHolder.Callback 的 surfaceDestroyed 方法中,结束绘制线程并调用 SurfaceHolder 的 removeCallbck 方法
- 在绘制线程每帧开始之前,调用 lockCanvas 方法锁住画布进行绘图
- 绘制完一帧的数据之后,调用 unlockCanvasAndPost 方法提交数据来显示图像
- 用于控制子线程绘制的标记参数,如上面代码中的mIsDrawing变量,需要用volatile关键字修饰,以保证多线程安全。
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/**
* 必须实现SurfaceHolder.Callback接口和Runnable接口
*/
public class MySurfaceView extends SurfaceView implements SurfaceHolder.Callback, Runnable {
// 是否绘制
private volatile boolean mIsDrawing;
// SurfaceView 控制器
private SurfaceHolder mSurfaceHolder;
// 画笔
private Paint mPaint;
// 画布
private Canvas mCanvas;
// 独立的线程
private Thread mThread;
public MySurfaceView(Context context) {
super(context);
init();
}
public MySurfaceView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public MySurfaceView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
mSurfaceHolder = getHolder();
// 注册回调事件
mSurfaceHolder.addCallback(this);
mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
mPaint.setStyle(Paint.Style.STROKE);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
VideoLogUtil.d("onSurfaceCreated");
mThread = new Thread(this, "yc");
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
VideoLogUtil.d("onSurfaceChanged"+format+ "----"+ width+ "----"+ height);
//并开启线程
mIsDrawing = true;
mThread.start();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
VideoLogUtil.d("onSurfaceDestroyed");
// 不再绘制,移除回调,线程终止
mIsDrawing = false;
mSurfaceHolder.removeCallback(this);
mThread.interrupt();
}
@Override
public void run() {
while (mIsDrawing) {
VideoLogUtil.d("draw canvas");
// 锁定画布,获得画布对象
mCanvas = mSurfaceHolder.lockCanvas();
try {
//使用画布做具体的绘制
draw();
// 线程休眠 100 ms
Thread.sleep(100);
} catch (Exception e) {
VideoLogUtil.d(e.getMessage());
} finally {
// 解锁画布,提交绘制,显示内容
if (mCanvas != null) {
mSurfaceHolder.unlockCanvasAndPost(mCanvas);
}
}
}
}
private void draw() {
//开始绘制
}
}由于SurfaceView常被用于游戏、视频等场景,绘制操作会相对复杂很多,通常都需要开启子线程,在子线程中执行绘制操作,以免阻塞UI线程。在子线程中,我们通过SurfaceHolder的lockCanvas方法获取Canvas对象来进行具体的绘制操作,此时Canvas对象被当前线程锁定,绘制完成后通过SurfaceHolder的unlockCanvasAndPost方法提交绘制结果并释放Canvas对象。
4.2 SurfaceHolder.Callback
- SurfaceView必须实现SurfaceHolder的Callback接口
- 主要是3个方法,分别是surfaceCreated、surfaceChanged、surfaceDestroyed。从名字就可以看出来这个是监听SurfaceView状态的,跟Activity的生命周期有点像。
- 接口中有以下三个方法作用说明
- public void surfaceCreated(SurfaceHolder holder):
- Surface 第一次创建时被调用,例如 SurfaceView 从不可见状态到可见状态 。在这个方法被调用到 surfaceDestroyed 方法被调用之前,Surface 对象可以被操作。也就是说,在界面可见的情况下,可以对 SurfaceView 进行绘制。
- surfaceCreated方法中一般做初始化动作,比如设置绘制线程的标记位,创建用于绘制的子线程等
- public void surfaceChanged(SurfaceHolder holder, int format, int width, int height):
- Surface 大小和格式改变时会被调用,例如横竖屏切换时,如果需要对 Surface 的图像进行处理,就需要在这里实现。这个方法在 surfaceCreated 之后至少会被调用一次 。
- public void surfaceDestroyed(SurfaceHolder holder):
- Surface 被销毁时被调用,例如 SurfaceView 从可见到不可见状态时。 在这个方法被调用过之后,不能够再对 Surface 对象进行任何操作,所以绘图线程不能再对 SurfaceView 进行操作。
- surfaceDestroyed被调用后,就不能再对Surface对象进行任何操作,所以我们需要在surfaceDestroyed方法中将绘制的子线程停掉。
- public void surfaceCreated(SurfaceHolder holder):
4.3 SurfaceHolder类源码
首先看一下源码,这里省略了部分代码
- SurfaceHolder中的接口可以分为2类
- 一类是Callback接口,也就是我们上面模版代码中实现的3个接口方法,这类接口主要是用于监听SurfaceView的状态,以便我们进行相应的处理,比如创建绘制子线程,停止绘制等。
- 另一类方法主要用于和Surface以及SurfaceView交互,比如lockCanvas方法和unlockCanvasAndPost方法用于获取Canvas以及提交绘制结果等。
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
39public interface SurfaceHolder {
public interface Callback {
public void surfaceCreated(SurfaceHolder holder);
public void surfaceChanged(SurfaceHolder holder, int format, int width,int height);
public void surfaceDestroyed(SurfaceHolder holder);
}
public interface Callback2 extends Callback {
void surfaceRedrawNeeded(SurfaceHolder holder);
default void surfaceRedrawNeededAsync(SurfaceHolder holder, Runnable drawingFinished) {
surfaceRedrawNeeded(holder);
drawingFinished.run();
}
}
public void addCallback(Callback callback);
public void removeCallback(Callback callback);
public Canvas lockCanvas();
public Canvas lockCanvas(Rect dirty);
default Canvas lockHardwareCanvas() {
throw new IllegalStateException("This SurfaceHolder doesn't support lockHardwareCanvas");
}
public void unlockCanvasAndPost(Canvas canvas);
public Rect getSurfaceFrame();
public Surface getSurface();
}
4.4 SurfaceView部分源码
代码如下所示,这里省略了部分代码
- SurfaceView继承自View,但是其实和View是有很大的不同的,除了前面介绍的几点SurfaceView的特性外,在底层SurfaceView也很大的不同,包括拥有自己独立的绘图表面等。
- 从下面SurfaceView的源码中我们可以看到,调用SurfaceHolder的lockCanvas方法实际上调用的是Surface的lockCanvas方法,返回的是Surface中的Canvas。并且调用过程加了一个可重入锁mSurfaceLock。所以绘制过程中只能绘制完一帧内容并提交更改以后才会释放Canvas,也就是才能继续下一帧的绘制操作。
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
128public class SurfaceView extends View {
final Surface mSurface = new Surface();
//锁
final ReentrantLock mSurfaceLock = new ReentrantLock();
//当滑动的时候,也会不断更新surface
private final ViewTreeObserver.OnScrollChangedListener mScrollChangedListener
= new ViewTreeObserver.OnScrollChangedListener() {
@Override
public void onScrollChanged() {
updateSurface();
}
};
//这个方法是更新surface
protected void updateSurface() {
if (!mHaveFrame) {
return;
}
if (creating || formatChanged || sizeChanged || visibleChanged || windowVisibleChanged) {
try {
mSurfaceLock.lock();
try {
mDrawingStopped = !visible;
SurfaceControl.openTransaction();
try {
mSurfaceControl.setLayer(mSubLayer);
if (mViewVisibility) {
mSurfaceControl.show();
} else {
mSurfaceControl.hide();
}
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();
}
} finally {
mSurfaceLock.unlock();
}
}
}
private final SurfaceHolder mSurfaceHolder = new SurfaceHolder() {
@Override
public void addCallback(Callback callback) {
synchronized (mCallbacks) {
if (mCallbacks.contains(callback) == false) {
mCallbacks.add(callback);
}
}
}
@Override
public void removeCallback(Callback callback) {
synchronized (mCallbacks) {
mCallbacks.remove(callback);
}
}
@Override
public Canvas lockCanvas() {
return internalLockCanvas(null);
}
@Override
public Canvas lockCanvas(Rect inOutDirty) {
return internalLockCanvas(inOutDirty);
}
private final Canvas internalLockCanvas(Rect dirty) {
mSurfaceLock.lock();
Canvas c = null;
if (!mDrawingStopped && mWindow != null) {
try {
c = mSurface.lockCanvas(dirty);
} catch (Exception e) {
Log.e(LOG_TAG, "Exception locking surface", e);
}
}
if (c != null) {
mLastLockTime = SystemClock.uptimeMillis();
return c;
}
long now = SystemClock.uptimeMillis();
long nextTime = mLastLockTime + 100;
if (nextTime > now) {
try {
Thread.sleep(nextTime-now);
} catch (InterruptedException e) {
}
now = SystemClock.uptimeMillis();
}
mLastLockTime = now;
mSurfaceLock.unlock();
return null;
}
@Override
public void unlockCanvasAndPost(Canvas canvas) {
mSurface.unlockCanvasAndPost(canvas);
mSurfaceLock.unlock();
}
@Override
public Surface getSurface() {
return mSurface;
}
@Override
public Rect getSurfaceFrame() {
return mSurfaceFrame;
}
};
}
4.5 看看Surface源码
源码如下所示
- Surface实现了Parcelable接口,因为它需要在进程间以及本地方法间传输。Surface中创建了Canvas对象,用于执行具体的绘制操作。
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
50public class Surface implements Parcelable {
final Object mLock = new Object(); // protects the native state
private final Canvas mCanvas = new CompatibleCanvas();
public Canvas lockCanvas(Rect inOutDirty)
throws Surface.OutOfResourcesException, IllegalArgumentException {
synchronized (mLock) {
checkNotReleasedLocked();
if (mLockedObject != 0) {
throw new IllegalArgumentException("Surface was already locked");
}
mLockedObject = nativeLockCanvas(mNativeObject, mCanvas, inOutDirty);
return mCanvas;
}
}
public void unlockCanvasAndPost(Canvas canvas) {
synchronized (mLock) {
checkNotReleasedLocked();
if (mHwuiContext != null) {
mHwuiContext.unlockAndPost(canvas);
} else {
unlockSwCanvasAndPost(canvas);
}
}
}
private void unlockSwCanvasAndPost(Canvas canvas) {
if (canvas != mCanvas) {
throw new IllegalArgumentException("canvas object must be the same instance that "
+ "was previously returned by lockCanvas");
}
if (mNativeObject != mLockedObject) {
Log.w(TAG, "WARNING: Surface's mNativeObject (0x" +
Long.toHexString(mNativeObject) + ") != mLockedObject (0x" +
Long.toHexString(mLockedObject) +")");
}
if (mLockedObject == 0) {
throw new IllegalStateException("Surface was not locked");
}
try {
nativeUnlockCanvasAndPost(mLockedObject, canvas);
} finally {
nativeRelease(mLockedObject);
mLockedObject = 0;
}
}
}
05.SurfaceView总结
5.1 使用场景
- SurfaceView主要用于游戏、视频等复杂视觉效果的场景,利用双缓冲机制,在子线程中执行复杂的绘制操作,可以防止阻塞UI线程。
- 我们在使用SurfaceView时一般都要实现Runnable接口和SurfaceHolder的Callback接口,并开启子线程进行具体的绘制操作
5.2 为何使用SurfaceView
- 一是,如果屏幕刷新频繁,onDraw方法会被频繁的调用,onDraw方法执行的时间过长,会导致掉帧,出现页面卡顿。而SurfaceView采用了双缓冲技术,提高了绘制的速度,可以缓解这一现象。
- 二是,view的onDraw方法是运行在主线程中的,会轻微阻塞主线程,对于需要频繁刷新页面的场景,而且onDraw方法中执行的操作比较耗时,会导致主线程阻塞,用户事件的响应受到影响,也就是响应速度下降,影响了用户的体验。而SurfaceView可以在自线程中更新UI,不会阻塞主线程,提高了响应速度。