2. SurfaceView核心API:SurfaceHolder、Surface、Canvas、lockCanvas()与unlockCanvasAndPost()、SurfaceHolder.Callback回调详解
说实话,很多做Android视频渲染的朋友,一开始都被SurfaceView这套API绕晕过。我当年第一次接触时,也是对着文档看了半天,总觉得这几个类之间的关系像一团乱麻。今天咱们就把它们彻底捋清楚。
2.1 核心角色:SurfaceHolder
SurfaceHolder,你可以把它理解成SurfaceView的"管家"。它不直接干活,但所有重要的操作都得经过它。
我习惯把SurfaceHolder比作一个遥控器——你不需要知道电视机内部怎么工作的,按遥控器上的按钮就行了。SurfaceHolder就是那个遥控器,Surface就是那台电视机。
关键点:SurfaceHolder是SurfaceView内部Surface的控制器。你通过它来获取Surface、管理回调、控制画布。
获取SurfaceHolder的方式很简单:
SurfaceHolder holder = surfaceView.getHolder();
嗯,就这一行。但拿到之后,能干的事情可就多了。
2.2 真正的画板:Surface
Surface才是真正干活的那个。它代表着一块屏幕外的缓冲区,你可以往上面画东西。我遇到过不少新手,以为SurfaceView本身能直接画,其实不是——你得通过Surface来操作。
Surface有几个重要特性:
- 独立于UI线程:你可以在子线程里往Surface上画东西,不会阻塞主线程
- 双缓冲机制:内部维护了两个缓冲区,一个用于绘制,一个用于显示
- 与WindowManager关联:Surface最终会被合成到系统显示系统中
我的经验:Surface的创建和销毁是由系统控制的,你没法手动new一个Surface出来用。所以一定要通过SurfaceHolder来获取。
2.3 绘图工具:Canvas
Canvas就像一支画笔。你拿到Canvas之后,就可以在上面画矩形、画圆、画文字、画图片。但注意——这个Canvas是从Surface的缓冲区里"锁定"出来的。
说白了,Canvas和Surface的关系是这样的:
- Surface是画板,Canvas是画笔
- 你从Surface上"借"出Canvas,画完再"还"回去
- 借和还的过程,就是lockCanvas()和unlockCanvasAndPost()
2.4 核心操作:lockCanvas()与unlockCanvasAndPost()
这两个方法,是SurfaceView绘制的灵魂。我刚开始用的时候,总觉得它们像一对双胞胎,必须成对出现。
lockCanvas():从Surface的缓冲区中锁定一块区域,返回一个Canvas对象。这块区域就是你能画的地方。
unlockCanvasAndPost():把画好的内容提交到屏幕上。注意,这个方法名已经说得很清楚了——先解锁(unlock),再提交(Post)。
SurfaceHolder holder = surfaceView.getHolder();
Canvas canvas = holder.lockCanvas();
if (canvas != null) {
try {
// 在这里画东西
canvas.drawColor(Color.BLUE);
canvas.drawCircle(100, 100, 50, paint);
} finally {
holder.unlockCanvasAndPost(canvas);
}
}
注意:lockCanvas()可能返回null!比如Surface还没创建好,或者已经被销毁了。所以一定要判空。我曾经因为没判空,在低端机上崩溃了好几次,后来学乖了。
还有一个细节:lockCanvas()默认锁定的是整个Surface区域。如果你只想更新一小块,可以用带Rect参数的版本:
Rect dirtyRect = new Rect(10, 10, 100, 100);
Canvas canvas = holder.lockCanvas(dirtyRect);
这样做的好处是性能更好——系统只需要把那块脏区域提交上去,不用刷新整个屏幕。
2.5 生命周期管理:SurfaceHolder.Callback
这个回调接口,是SurfaceView的生命周期监听器。它有三个方法:
| 方法 | 触发时机 | 说明 |
|---|---|---|
| surfaceCreated() | Surface首次创建时 | 可以在这里启动绘制线程 |
| surfaceChanged() | Surface尺寸或格式变化时 | 比如横竖屏切换,需要重新调整绘制参数 |
| surfaceDestroyed() | Surface即将销毁时 | 必须在这里停止绘制线程,否则会崩溃 |
我见过最典型的错误,就是在surfaceDestroyed()里没有停止线程。结果Activity退出了,子线程还在往Surface上画,直接报"Surface already destroyed"的异常。
holder.addCallback(new SurfaceHolder.Callback() {
@Override
public void surfaceCreated(SurfaceHolder holder) {
// 启动绘制线程
isDrawing = true;
drawingThread = new DrawingThread(holder);
drawingThread.start();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
// 更新绘制参数
updateViewport(width, height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// 停止绘制线程
isDrawing = false;
drawingThread.join();
}
});
避坑指南:surfaceDestroyed()里不要直接调用lockCanvas()!因为Surface已经不可用了。我曾经在回调里尝试做最后的绘制,结果每次都报错。正确的做法是:设置一个标志位让线程退出,然后在线程内部判断。
2.6 知识体系总览
说了这么多,咱们用一张图来总结一下这些API之间的关系:
从这张图可以看得很清楚:SurfaceView通过SurfaceHolder暴露接口,SurfaceHolder管理Surface和Callback,Canvas是从Surface锁出来的临时画布,画完之后通过unlockCanvasAndPost提交到屏幕。
2.7 实战中的几个要点
最后,分享几个我在项目中踩过的坑:
- 线程安全:lockCanvas()和unlockCanvasAndPost()必须在同一个线程调用。我见过有人在不同线程里调用,结果画面闪烁得厉害。
- 不要持有Canvas引用:Canvas用完之后必须unlock,不要把它存起来下次用。每次lock出来的Canvas可能都不一样。
- Surface销毁后别操作:在surfaceDestroyed()之后,任何对Surface的操作都会抛异常。所以一定要用标志位控制线程。
- 性能优化:如果只需要更新画面的一小部分,用带Rect参数的lockCanvas(),能省不少性能。
总结一下:SurfaceHolder是入口,Surface是缓冲区,Canvas是画笔,lock/unlock是操作流程,Callback是生命周期监听。把这几个概念理清了,SurfaceView的视频渲染就入门了。