3. PreviewView在Compose中的封装

3.1 原理概述

CameraX 的预览输出需要通过 Preview.SurfaceProvider 接口将相机帧数据投递到一个可绘制的 Surface 上。在传统 View 系统中,PreviewView 是一个集成了 SurfaceProvider 与生命周期感知的专用 View,能自动处理 Surface 创建与销毁。然而 Jetpack Compose 是声明式 UI 框架,无法直接使用 View 对象。因此需借助 AndroidView 组件将 PreviewView 嵌入 Compose 层级,并通过 LocalLifecycleOwner 传递生命周期信号,确保相机资源在正确的时机绑定与释放。

核心挑战:是让 Compose 的重组与 CameraX 的生命周期事件同步,避免因 recomposition 导致 Surface 重复创建或因生命周期错位引发内存泄漏。
Compose 中 PreviewView 封装架构流 Compose UI @Composable AndroidView Compose → View PreviewView SurfaceProvider CameraX Preview.UseCase LifecycleOwner LocalLifecycleOwner 生命周期回调 onStart / onResume / onPause / onStop Surface 创建/销毁 surfaceProvider 回调 ProcessCameraProvider bindToLifecycle

3.2 实现流程

  1. 获取 CameraX 实例及 UseCase:通过 ProcessCameraProvider.getInstance(context) 获取异步实例,构造 Preview 对象并设置 SurfaceProvider。
  2. 将 SurfaceProvider 传入 PreviewView:在 AndroidView 的 factory 中创建 PreviewView,调用 previewView.surfaceProvider 获得该 View 提供的 SurfaceProvider。
  3. 绑定生命周期:使用 LocalLifecycleOwner.current 将 CameraX 的 bindToLifecycle 与 Compose 所在的生命周期关联。
  4. 在 AndroidView 的 update 中处理重组:如果 lensFacing 等配置变化,通过 update 参数重新绑定或切换 UseCase。
  5. 清理资源:当 Composable 离开组合时,通过 DisposableEffect 或 AndroidView 的 onReset 解绑相机,避免泄漏。

3.3 关键代码实现

以下完整封装示例演示如何在 Compose 中使用 CameraX 预览:

@Composable
fun CameraPreviewScreen(
    modifier: Modifier = Modifier,
    lensFacing: Int = CameraSelector.LENS_FACING_BACK
) {
    val context = LocalContext.current
    val lifecycleOwner = LocalLifecycleOwner.current
    val cameraProviderFuture = remember { ProcessCameraProvider.getInstance(context) }

    // 使用 remember 避免重组时重复创建 Preview 对象
    val preview = remember {
        Preview.Builder()
            .setTargetRotation(context.resources.configuration.orientation)
            .build()
    }

    // 用 AndroidView 嵌入 PreviewView
    AndroidView(
        modifier = modifier.fillMaxSize(),
        factory = { ctx ->
            val previewView = PreviewView(ctx)
            previewView.setBackgroundColor(Color.BLACK.toArgb())

            // 将 PreviewView 的 SurfaceProvider 设置给 Preview UseCase
            preview.surfaceProvider = previewView.surfaceProvider

            // 绑定相机生命周期
            val cameraSelector = CameraSelector.Builder()
                .requireLensFacing(lensFacing)
                .build()

            cameraProviderFuture.addListener({
                val cameraProvider = cameraProviderFuture.get()
                try {
                    cameraProvider.unbindAll()
                    cameraProvider.bindToLifecycle(
                        lifecycleOwner,
                        cameraSelector,
                        preview
                    )
                } catch (e: Exception) {
                    Log.e("CameraPreview", "Failed to bind camera", e)
                }
            }, ContextCompat.getMainExecutor(ctx))

            previewView
        },
        update = { previewView ->
            // 当 lensFacing 变化时重新绑定
            val cameraSelector = CameraSelector.Builder()
                .requireLensFacing(lensFacing)
                .build()
            cameraProviderFuture.addListener({
                val cameraProvider = cameraProviderFuture.get()
                try {
                    cameraProvider.unbindAll()
                    cameraProvider.bindToLifecycle(
                        lifecycleOwner,
                        cameraSelector,
                        preview
                    )
                } catch (e: Exception) {
                    Log.e("CameraPreview", "Failed to rebind camera", e)
                }
            }, ContextCompat.getMainExecutor(previewView.context))
        }
    )
}

3.4 配置说明

配置项说明示例值/文件
CameraX 依赖 需在 build.gradle 中添加 CameraX 核心及生命扩展库
implementation "androidx.camera:camera-core:1.4.1"
implementation "androidx.camera:camera-lifecycle:1.4.1"
implementation "androidx.camera:camera-view:1.4.1"
权限声明 AndroidManifest 中声明相机权限,Android 10+ 需动态申请
<uses-permission android:name="android.permission.CAMERA"/>
ProGuard 规则(可选) 保持 CameraX 内部类不被混淆
-keep class androidx.camera.** { *; }
Activity 配置 确保 Activity 使用 Compose 主题且未锁屏方向
android:screenOrientation="fullSensor"

3.5 调试与常见错误

调试技巧:在 PreviewView 的 surfaceProvider 设置前后打印日志,确认 Surface 是否被及时创建。

常见错误及日志分析:

  • 预览黑屏:最可能原因是 preview.surfaceProvider 在 bindToLifecycle 之前未设置,或设置时机过晚。日志中若出现 SurfaceProvider not set 警告,则需检查赋值顺序。
  • Surface 已销毁(-19):表示 CameraX 试图往已释放的 Surface 推帧。常见于 Activity 重建但 CameraProvider 未重新绑定。解决方案:在 update 块中重新执行 bindToLifecycle。
  • IllegalStateException: Camera is already closed:通常由多次 unbindAll 或生命周期回调冲突导致。确保在 DisposableEffect 中只解绑当前 UseCase 而非全部。
// 日志示例:Surface 正常创建
D/CameraPreview: Surface created: Size(1920,1080) format=34
D/CameraPreview: Binding camera to lifecycle...
D/CameraPreview: Camera bound successfully

// 日志示例:Surface 未设置
W/CameraX: Preview.SurfaceProvider is null. Preview will not produce frames.

3.6 性能与稳定性优化

  1. 减少不必要的重组:使用 remember 缓存 Preview 和 CameraProviderFuture,避免每次重组都创建新实例。若需切换镜头,通过 update 块更新绑定而不是重建整个 View。
  2. 避免多次 unbindAll:在 update 中调用 unbindAll 会断开所有 UseCase,若后续有 ImageAnalysis 等其他 UseCase 也会失效。推荐只 unbind 当前 Preview UseCase。
  3. 生命周期对齐:使用 DisposableEffect 监听 Compose 组合销毁,显式调用 cameraProvider.unbind(preview) 释放相机资源。
  4. Surface 复用:避免在 factory 中每次创建新 PreviewView 时都重新生成 SurfaceProvider。若 Compose 树销毁重建,旧的 Surface 会被回收,CameraX 会自动处理。
// 使用 DisposableEffect 确保资源释放
DisposableEffect(lifecycleOwner) {
    onDispose {
        cameraProviderFuture.get().unbind(preview)
    }
}
注意:在 Compose 中使用 LocalLifecycleOwner.current 必须确保当前组合正在拥有生命周期的组件内(如 Activity 或 Fragment)。若在 Dialog 或非 Activity 场景中使用,需手动提供合适的 LifecycleOwner。