4. Surface创建与销毁的边界条件

Surface 的生命周期管理是 Android 图形子系统中最容易出错的环节之一。本章深入剖析 Surface 的可用性判断、重复创建防范以及典型泄漏场景,帮助开发者在复杂生命周期场景中建立边界控制能力。

4.1 核心问题:为什么需要边界管理

Surface 本身是 生产者-消费者模型 中的共享缓冲区。当应用侧持有的 Surface 对象与 SurfaceFlinger 侧的 BufferQueue 解除绑定后,任何写入操作将导致未定义行为甚至崩溃。边界管理要解决三个本质问题:

  • 可用性边界:Surface 对象在哪个时间窗口内是有效的?
  • 创建边界:如何避免在回调风暴中反复创建 Surface?
  • 销毁边界:如何确保 Native 层的资源被及时释放?
课堂提醒:Surface 的可用性不仅取决于 Java 层的 isValid(),更取决于 Native 层的状态机。理解这两层映射关系是边界管理的前提。

4.2 Surface 可用性判断原理

Surface 在 Native 层由 sp<Surface> 表示,内部持有 IGraphicBufferProducer 的 Binder 代理。当 SurfaceFlinger 端断开连接后,Binder 代理进入死状态,此时任何 lockCanvas() 或 queueBuffer() 操作都将失败。

判断维度 Java 层 API Native 层状态 典型返回值
有效性 isValid() mIsValid 标志位 true / false
可写入 lockCanvas() BufferQueue 连接状态 Canvas / null
已销毁 — mDestroyed 标志位 true / false
生产就绪 isConsumerConnected() Consumer 连接计数 true / false

4.2.1 最佳实践:组合判断

单一 isValid() 判断不足以应对所有场景。推荐使用三层守卫模式:

// 三层守卫:Surface 可用性检查
boolean isSurfaceUsable(Surface surface) {
    if (surface == null) {
        return false;
    }
    if (!surface.isValid()) {
        return false;
    }
    // 检查 Canvas 是否可获取,这是最终的可用性探测
    Canvas canvas = null;
    try {
        canvas = surface.lockCanvas(null);
        if (canvas != null) {
            surface.unlockCanvasAndPost(canvas);
        }
    } catch (Surface.OutOfResourcesException e) {
        return false;
    } catch (IllegalStateException e) {
        return false;
    }
    return true;
}
常见误区:很多开发者只依赖 surfaceCreated() 回调作为“可用”信号。实际上,SurfaceView 的创建回调与硬件层的 BufferQueue 激活存在时间差。在上述时间窗口内调用 lockCanvas() 可能得到 null 或抛出异常。

4.3 多次创建避免策略

在异步架构中(如 Camera2、MediaCodec 输出、自定义 SurfaceView),surfaceCreated() 回调可能因 Activity 重建、Fragment 状态变化而被多次调用。重复创建 Native Surface 将导致 内存碎片 和 句柄泄漏。

4.3.1 防重复创建模式

使用 AtomicBoolean 作为守卫:

public class SurfaceGuard {
    private final AtomicBoolean isSurfaceCreated = new AtomicBoolean(false);
    private final Object lock = new Object();
    private Surface surface;

    public boolean tryAcquireSurface(Surface newSurface) {
        // 双重检查锁定
        if (isSurfaceCreated.get()) {
            // 已有有效 Surface,释放新传入的 Surface
            if (newSurface != null && newSurface != surface) {
                newSurface.release();
            }
            return false;
        }
        synchronized (lock) {
            if (isSurfaceCreated.get()) {
                return false;
            }
            surface = newSurface;
            isSurfaceCreated.set(true);
            return true;
        }
    }

    public Surface getSurface() {
        return surface;
    }

    public void release() {
        synchronized (lock) {
            if (isSurfaceCreated.compareAndSet(true, false)) {
                if (surface != null) {
                    surface.release();
                    surface = null;
                }
            }
        }
    }
}

4.3.2 生命周期感知式容器

在 ViewModel 或 LifecycleOwner 中使用 LiveData 持有 Surface 引用,避免因 Fragment 重建导致重复创建:

public class SurfaceViewModel extends ViewModel {
    private final MutableLiveData<Surface> surfaceLiveData = new MutableLiveData<>();

    public void onSurfaceCreated(Surface surface) {
        Surface current = surfaceLiveData.getValue();
        if (current != null && current != surface && current.isValid()) {
            // 已有有效 Surface,跳过
            surface.release();
            return;
        }
        surfaceLiveData.setValue(surface);
    }

    public LiveData<Surface> getSurfaceLiveData() {
        return surfaceLiveData;
    }

    @Override
    protected void onCleared() {
        Surface surface = surfaceLiveData.getValue();
        if (surface != null && surface.isValid()) {
            surface.release();
        }
        super.onCleared();
    }
}

4.4 泄漏场景深度剖析

Surface 泄漏是 Android 中 最难定位的内存泄漏 类型之一,因为泄漏对象通常不在堆内,而是占用 图形内存(Graphic Buffer) 和 文件描述符。

4.4.1 典型泄漏场景总览

  • 场景 A:场景切换未释放 —— Activity A 启动 Activity B,A 的 SurfaceView 被销毁但输出的 Camera 或 MediaCodec 线程仍持有 Surface 引用。
  • 场景 B:匿名 Binder 保活 —— 创建 Surface 时传入的匿名 SurfaceControl 未释放,导致 SurfaceFlinger 端 Binder 引用无法回收。
  • 场景 C:Handler 延迟消息 —— 在子线程中将 Surface 传给 Handler.postDelayed 的回调,Activity 销毁后回调仍然触发写入。
    验证代码:
// 泄漏版本:Handler 持有外部 Surface 引用
Handler handler = new Handler(Looper.getMainLooper());
Surface leakingSurface = surfaceView.getHolder().getSurface();
handler.postDelayed(() -> {
    // 即使 Activity 已销毁,这段代码仍会执行
    try {
        Canvas canvas = leakingSurface.lockCanvas(null);
        if (canvas != null) {
            canvas.drawColor(Color.RED);
            leakingSurface.unlockCanvasAndPost(canvas);
        }
    } catch (Exception e) {
        // 捕获异常但 Surface 已泄漏
    }
}, 5000);

4.4.2 泄漏检测实战

通过 dumpsys SurfaceFlinger 查看 BufferQueue 生产者计数:

adb shell dumpsys SurfaceFlinger | grep -A 20 "Layer" | grep -E "producer|count|buffer"

# 正常输出示例:
#   producer  count = 1
#   buffer-count  = 0

# 泄漏输出示例(count > 期望值):
#   producer  count = 3
#   buffer-count  = 2
实战技巧:使用 strace 跟踪 Surface 相关系统调用:
# 跟踪 SurfaceFlinger 的 ioctl 调用
adb shell strace -p $(pidof surfaceflinger) -e trace=ioctl,ioctl 2>&1 | grep -E "BINDER_IOCTL|SYNC_IOC_MERGE"

4.5 边界状态机:创建与销毁的完整流程

以下 SVG 图展示了 Surface 从 创建等待 到 安全销毁 的完整状态机,重点标注了各阶段的边界判断点:

Surface 创建与销毁边界状态机 Surface 创建与销毁边界状态机 等待创建 surfaceCreated() 未收到 可用 isValid()=true, 可写入 正在销毁 surfaceDestroyed() 已调用 泄漏状态 持有引用但已失效 安全销毁 资源完整释放 强制销毁 系统回收或异常退出 surfaceCreated() 成功 surfaceDestroyed() 进程异常 / OOM 未释放引用 正确释放资源 新 Surface 创建 系统回收 边界管理关键点: 1. 可用阶段(绿)是唯一的“安全写入窗口”,需在 surfaceDestroyed() 前停止所有写入线程。 2. 正在销毁阶段(红)应立刻切断所有外部引用,防止进入泄漏状态。 3. 泄漏状态(紫)将持有 Graphic Buffer 直到进程死亡,是 OOM 的主要诱因。 4. 安全销毁(青)后应通过 AtomicBoolean 或 Lifecycle 防止重复创建。

4.6 面试级边界问题诊断

症状 根因 排查手段 修复方案
Surface already released 多线程并发写入导致 double-free 添加 ThreadSanitizer 检测 使用同步锁保护 release()
BufferQueue has been abandoned Consumer 端断开而 Producer 继续写入 检查 SF dumpsys 的 abandoned 计数 监听 onFrameAvailable 回调的断开信号
graphic memory 持续增长 Surface 在 Handler/LiveData 中延迟释放 使用 Memory Profiler 查看 Graphic Buffer 改用 WeakReference 或 lifecycle-aware 容器
ANR 发生在 lockCanvas() 多线程竞争导致死锁 抓取 traces.txt 分析锁等待 避免在 UI 线程调用 lockCanvas()

4.7 性能与稳定性优化建议

  • 使用 tryAcquire 模式:在 Surface 创建回调中引入 CAS 操作,防止线程同步造成的重复创建。
  • 分代释放策略:对短期内重复创建销毁的场景(如横竖屏切换),采用 2 秒延迟释放,避免频繁创建/销毁的开销。
  • 文件描述符监控:定期检查 /proc/self/fd 下 SurfaceFlinger 相关 fd 数量,超过阈值时主动 dump 调用栈。
  • Hook 关键系统调用:在开发阶段使用 AOP 对 Surface#lockCanvas() 和 Surface#release() 进行入参、线程、时间戳的日志记录。
  • 异步销毁优化:将 surfaceDestroyed() 回调中的释放逻辑放入 HandlerThread,避免阻塞主线程导致 ANR。