第2章 CameraX配置初始化:ProcessCameraProvider、Future与Compose协程的桥接

2.1 原理:从异步Future到声明式协程

CameraX核心初始化入口为ProcessCameraProvider,它是一个单例工厂,通过getInstance(context)返回一个ListenableFuture<ProcessCameraProvider>。Future是Google Guava的异步回调模型,而Compose/Android推荐使用Kotlin协程管理生命周期。本节核心任务是将Future“桥接”到协程作用域,使开发者能够在Composable中同步地获取ProcessCameraProvider实例,进而绑定LifecycleOwner、UseCase和PreviewView。

关键原则:ProcessCameraProvider的初始化必须发生在协程作用域内,且需感知Activity/Fragment的LifecycleOwner,否则会出现CameraInfoUnavailableException或空指针。
为什么用Future而不是直接返回对象?CameraX需要异步绑定底层Camera HAL服务,若在主线程阻塞会造成ANR。Future提供非阻塞等待机制,配合协程的suspendCancellableCoroutine可无缝转换为挂起函数。

2.2 实现流程

  1. 添加依赖:在build.gradle中引入CameraX核心库与协程库。
  2. 创建Compose的LifecycleOwner:通常在LocalLifecycleOwner.current获取。
  3. 协程作用域:使用rememberCoroutineScope()获得与Compose生命周期绑定的协程。
  4. 桥接Future→协程:使用suspendCancellableCoroutine包装ProcessCameraProvider.getInstance()。
  5. 绑定CameraProvider:获取到Provider后调用provider.bindToLifecycle()。
  6. 错误处理:捕获ExecutionException、InterruptedException,转换为Compose状态。
常见陷阱:忘记在DisposableEffect或LaunchedEffect中取消协程,导致内存泄漏;未检查LifecycleOwner状态导致IllegalStateException。

2.3 关键代码:完整桥接实现

以下示例展示在Composable中安全初始化CameraX并绑定预览用例,同时处理Future与协程的转换。

// build.gradle (Module :app)
dependencies {
    // CameraX core
    implementation "androidx.camera:camera-core:1.3.1"
    implementation "androidx.camera:camera-camera2:1.3.1"
    implementation "androidx.camera:camera-lifecycle:1.3.1"
    implementation "androidx.camera:camera-view:1.3.1"

    // Coroutines
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3"
    implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.7.0"
}
// CameraInitializer.kt
import androidx.camera.core.Preview
import androidx.camera.lifecycle.ProcessCameraProvider
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.LifecycleOwner
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

// 1. 将ListenableFuture转换为挂起函数
suspend fun awaitCameraProvider(context: android.content.Context): ProcessCameraProvider {
    return suspendCancellableCoroutine { continuation ->
        val future: ListenableFuture<ProcessCameraProvider> =
            ProcessCameraProvider.getInstance(context)
        // 注册回调
        future.addListener({
            if (continuation.isActive) {
                try {
                    continuation.resume(future.get())
                } catch (e: Exception) {
                    continuation.resumeWithException(e)
                }
            }
        }, Dispatchers.Main.immediate)
        // 取消时终止Future
        continuation.invokeOnCancellation {
            future.cancel(false)
        }
    }
}

// 2. Composable入口
@Composable
fun CameraXPreview(
    lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
    modifier: Modifier = Modifier
) {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()

    // 保存provider状态
    val cameraProvider = remember { mutableStateOf<ProcessCameraProvider?>(null) }
    val errorMessage = remember { mutableStateOf<String?>(null) }

    // 生命周期内初始化
    LaunchedEffect(lifecycleOwner) {
        try {
            val provider = awaitCameraProvider(context)
            cameraProvider.value = provider
        } catch (e: Exception) {
            errorMessage.value = "Camera init failed: ${e.localizedMessage}"
        }
    }

    // 预览View绑定
    AndroidView(
        factory = { ctx ->
            PreviewView(ctx).apply {
                implementationMode = PreviewView.ImplementationMode.COMPATIBLE
            }
        },
        modifier = modifier
    ) { previewView ->
        val provider = cameraProvider.value
        if (provider != null) {
            bindPreview(provider, lifecycleOwner, previewView)
        }
    }

    // 错误提示
    errorMessage.value?.let {
        Text(text = it, color = Color.Red, style = MaterialTheme.typography.bodySmall)
    }
}

// 3. 绑定Preview UseCase
private fun bindPreview(
    provider: ProcessCameraProvider,
    lifecycleOwner: LifecycleOwner,
    previewView: PreviewView
) {
    // 解绑之前绑定的用例
    provider.unbindAll()

    val preview = Preview.Builder()
        .build()
        .also { it.setSurfaceProvider(previewView.surfaceProvider) }

    // 绑定到生命周期(自动处理暂停/恢复/销毁)
    try {
        provider.bindToLifecycle(
            lifecycleOwner,
            CameraSelector.DEFAULT_BACK_CAMERA,
            preview
        )
    } catch (e: Exception) {
        // 通常由权限或Camera2异常引起
        Log.e("CameraXInit", "bindToLifecycle failed", e)
    }
}

2.4 配置与日志分析

CameraX初始化阶段会输出大量日志,开启adb logcat -s CameraX可观察关键流程。典型成功日志:

D/CameraX: CameraX initialized successfully.
D/CameraX: Available cameras: [0: back, 1: front]
D/CameraX: Bound 1 use cases: [Preview]

常见失败日志与排查:

日志原因解决方案
CameraInfoUnavailableException: No available camera 未授予CAMERA权限或设备无摄像头 在初始化前检查权限(ContextCompat.checkSelfPermission)
IllegalStateException: LifecycleOwner is destroyed 在Activity/Fragment已销毁后调用bindToLifecycle 使用LaunchedEffect确保生命周期活跃,或监听Lifecycle.Event.ON_DESTROY
ExecutionException: java.lang.SecurityException 权限在运行时被撤销 在try-catch中捕获并重定向到权限请求
日志过滤建议:adb logcat -s "CameraX*","Preview*",同时关注ProcessCameraProvider.getInstance执行时间(理想低于200ms)。若超过500ms,检查HAL层或系统相机占用。

2.5 性能与稳定性优化

  • 延迟初始化:ProcessCameraProvider.getInstance()是重量级操作,建议在LaunchedEffect中异步执行,不要阻塞Compose首次绘制。
  • 重用Provider:ProcessCameraProvider是单例,不应在每次重新组合时重新获取。上面代码使用remember存储,但注意LaunchedEffect只执行一次(除非key变化)。
  • 取消协程:suspendCancellableCoroutine的invokeOnCancellation必须取消Future,否则后台线程会继续等待,造成资源泄露。
  • UseCase解绑:每次重新绑定前调用provider.unbindAll(),否则多个Preview会冲突导致崩溃。
  • 生命周期感知:bindToLifecycle会自动处理暂停/恢复,但若手动管理,需在ON_DESTROY时调用provider.shutdown()(一般由Android组件负责)。

2.6 本章内联结构图:Future→协程桥接流程

ProcessCameraProvider初始化与协程桥接流程 ProcessCameraProvider初始化与Compose协程桥接流程 App启动 / Composable ProcessCameraProvider.getInstance ListenableFuture<Provider> suspendCancellableCoroutine / awaitCameraProvider() provider.bindToLifecycle(…Preview…) 协程作用域 LaunchedEffect / rememberCoroutineScope

2.7 调试与验证

  • 模拟慢初始化:延迟getInstance()完成,测试UI是否卡顿(理想状态下应无ANR)。
  • 生命周期测试:旋转设备或按Home键,观察bindToLifecycle是否自动解绑/重新绑定。打印日志:
Lifecycle.Event.ON_START -> CameraX resuming
Lifecycle.Event.ON_STOP  -> CameraX pausing
Lifecycle.Event.ON_DESTROY -> CameraX unbinding all use cases
重要:如果使用LaunchedEffect(null)(key为null)会导致每次重组都重新执行,应传入稳定key如lifecycleOwner。错误示例:LaunchedEffect(Unit)——因Unit恒定,重新组合不会触发,但若传null会每次都触发,造成频繁初始化。