第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 实现流程
- 添加依赖:在build.gradle中引入CameraX核心库与协程库。
- 创建Compose的LifecycleOwner:通常在
LocalLifecycleOwner.current获取。 - 协程作用域:使用
rememberCoroutineScope()获得与Compose生命周期绑定的协程。 - 桥接Future→协程:使用
suspendCancellableCoroutine包装ProcessCameraProvider.getInstance()。 - 绑定CameraProvider:获取到Provider后调用
provider.bindToLifecycle()。 - 错误处理:捕获
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→协程桥接流程
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会每次都触发,造成频繁初始化。