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 getHolder() SurfaceHolder getSurface() Surface lockCanvas() Canvas unlockCanvasAndPost() 屏幕显示 Callback surfaceCreated() surfaceChanged() surfaceDestroyed() addCallback() 管理生命周期 图例: 视图层 控制器 缓冲区 画布 输出 回调

从这张图可以看得很清楚:SurfaceView通过SurfaceHolder暴露接口,SurfaceHolder管理Surface和Callback,Canvas是从Surface锁出来的临时画布,画完之后通过unlockCanvasAndPost提交到屏幕。

2.7 实战中的几个要点

最后,分享几个我在项目中踩过的坑:

  1. 线程安全:lockCanvas()和unlockCanvasAndPost()必须在同一个线程调用。我见过有人在不同线程里调用,结果画面闪烁得厉害。
  2. 不要持有Canvas引用:Canvas用完之后必须unlock,不要把它存起来下次用。每次lock出来的Canvas可能都不一样。
  3. Surface销毁后别操作:在surfaceDestroyed()之后,任何对Surface的操作都会抛异常。所以一定要用标志位控制线程。
  4. 性能优化:如果只需要更新画面的一小部分,用带Rect参数的lockCanvas(),能省不少性能。

总结一下:SurfaceHolder是入口,Surface是缓冲区,Canvas是画笔,lock/unlock是操作流程,Callback是生命周期监听。把这几个概念理清了,SurfaceView的视频渲染就入门了。


公众号:蓝海资料掘金营,微信deep3321