4. Surface创建与销毁的边界条件
Surface 的生命周期管理是 Android 图形子系统中最容易出错的环节之一。本章深入剖析 Surface 的可用性判断、重复创建防范以及典型泄漏场景,帮助开发者在复杂生命周期场景中建立边界控制能力。
4.1 核心问题:为什么需要边界管理
Surface 本身是 生产者-消费者模型 中的共享缓冲区。当应用侧持有的 Surface 对象与 SurfaceFlinger 侧的 BufferQueue 解除绑定后,任何写入操作将导致未定义行为甚至崩溃。边界管理要解决三个本质问题:
- 可用性边界:Surface 对象在哪个时间窗口内是有效的?
- 创建边界:如何避免在回调风暴中反复创建 Surface?
- 销毁边界:如何确保 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 从 创建等待 到 安全销毁 的完整状态机,重点标注了各阶段的边界判断点:
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。