排查Compose Multiplatform中AVFoundation二维码扫描器停止工作问题
问题解决:Compose Multiplatform中AVFoundation相机扫描器多次扫描后停止工作
问题描述
我在Compose Multiplatform项目中集成了基于AVFoundation的相机扫描器,该扫描器在随机扫描5-6次后便停止扫描,但实时预览仍保持正常运行。请问该如何解决此问题?
原代码
@OptIn(ExperimentalForeignApi::class) @Composable actual fun QRCameraView( modifier: Modifier, onScanResult: (String) -> Unit ) { val captureSession = AVCaptureSession() val captureDevice = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo).firstOrNull { device -> (device as AVCaptureDevice).position == AVCaptureDevicePositionBack }!! as AVCaptureDevice val input = AVCaptureDeviceInput.deviceInputWithDevice(captureDevice, null) as AVCaptureDeviceInput captureSession.addInput(input) //Initialize an AVCaptureMetadataOutput object and set it as the output device to the capture session. val metadataOutput = AVCaptureMetadataOutput() if (captureSession.canAddOutput(metadataOutput)) { //Set delegate and use default dispatch queue to execute the call back // fixed with https://youtrack.jetbrains.com/issue/KT-45755/iOS-delegate-protocol-is-empty captureSession.addOutput(metadataOutput) metadataOutput.setMetadataObjectsDelegate(objectsDelegate = object : NSObject(), AVCaptureMetadataOutputObjectsDelegateProtocol { override fun captureOutput( output: AVCaptureOutput, didOutputMetadataObjects: List<*>, fromConnection: AVCaptureConnection, ) { if (didOutputMetadataObjects.isNotEmpty()) { val readableObject = didOutputMetadataObjects[0] as? AVMetadataMachineReadableCodeObject if (readableObject?.type == AVMetadataObjectTypeQRCode) { val code = readableObject?.stringValue onScanResult(code ?: "") } return } } }, queue = dispatch_get_main_queue()) metadataOutput.metadataObjectTypes = listOf(AVMetadataObjectTypeQRCode) } val cameraPreviewLayer = remember { AVCaptureVideoPreviewLayer( session = captureSession ) } UIKitView( modifier = modifier, background = Color.Black, factory = { val container = UIView() container.layer.addSublayer(cameraPreviewLayer) cameraPreviewLayer.videoGravity = AVLayerVideoGravityResizeAspectFill CoroutineScope(Dispatchers.IO).launch { captureSession.startRunning() } container }, onResize = { container: UIView, rect: CValue<CGRect> -> CATransaction.begin() CATransaction.setValue(true, kCATransactionDisableActions) container.layer.setFrame(rect) cameraPreviewLayer.setFrame(rect) CATransaction.commit() }, ) }
问题分析与解决方案
核心问题
你的代码存在三个关键问题导致扫描停止:
captureSession未被持久持有:Compose重组时会重新创建AVCaptureSession实例,但预览层绑定的是旧实例,导致元数据输出代理失效。- 元数据代理对象被GC回收:匿名代理对象没有强引用持有,iOS的ARC机制会在几次扫描后回收该对象,后续扫描事件无法触发回调。
- 会话启动的协程作用域不稳定:使用临时
CoroutineScope启动会话,作用域结束后可能影响会话的稳定运行。
修复后的代码
@OptIn(ExperimentalForeignApi::class) @Composable actual fun QRCameraView( modifier: Modifier, onScanResult: (String) -> Unit ) { // 用remember持有会话,避免重组时重建 val captureSession = remember { AVCaptureSession() } // 用remember持有元数据代理,防止被GC回收 val metadataDelegate = remember { object : NSObject(), AVCaptureMetadataOutputObjectsDelegateProtocol { override fun captureOutput( output: AVCaptureOutput, didOutputMetadataObjects: List<*>, fromConnection: AVCaptureConnection, ) { didOutputMetadataObjects.firstOrNull()?.let { obj -> val qrCode = obj as? AVMetadataMachineReadableCodeObject if (qrCode?.type == AVMetadataObjectTypeQRCode) { qrCode.stringValue?.let { onScanResult(it) } } } } } } LaunchedEffect(Unit) { // 初始化逻辑放在LaunchedEffect中,确保只执行一次 val captureDevice = AVCaptureDevice.devicesWithMediaType(AVMediaTypeVideo) .firstOrNull { (it as AVCaptureDevice).position == AVCaptureDevicePositionBack } ?: return@LaunchedEffect runCatching { val input = AVCaptureDeviceInput.deviceInputWithDevice(captureDevice, null) as AVCaptureDeviceInput if (captureSession.canAddInput(input)) { captureSession.addInput(input) val metadataOutput = AVCaptureMetadataOutput() if (captureSession.canAddOutput(metadataOutput)) { captureSession.addOutput(metadataOutput) metadataOutput.setMetadataObjectsDelegate( objectsDelegate = metadataDelegate, queue = dispatch_get_main_queue() ) metadataOutput.metadataObjectTypes = listOf(AVMetadataObjectTypeQRCode) } } } } val cameraPreviewLayer = remember { AVCaptureVideoPreviewLayer(session = captureSession).apply { videoGravity = AVLayerVideoGravityResizeAspectFill } } // 用LaunchedEffect启动会话,保证作用域稳定 LaunchedEffect(captureSession) { captureSession.startRunning() } UIKitView( modifier = modifier, background = Color.Black, factory = { UIView().apply { layer.addSublayer(cameraPreviewLayer) } }, onResize = { container, rect -> CATransaction.begin() CATransaction.setValue(true, kCATransactionDisableActions) container.layer.frame = rect cameraPreviewLayer.frame = rect CATransaction.commit() }, ) }
关键修复点说明
remember持久化核心对象:captureSession和metadataDelegate用remember包裹,确保Compose重组时不会被重新创建,同时避免代理对象被GC回收。LaunchedEffect执行初始化:将设备输入、元数据输出的初始化逻辑放在LaunchedEffect(Unit)中,确保只执行一次,避免重复添加输入输出导致异常。- 稳定协程作用域启动会话:用
LaunchedEffect替代临时CoroutineScope启动captureSession.startRunning(),保证会话运行在稳定的作用域中。 - 优化回调逻辑:简化元数据回调的空值处理,让代码更简洁可靠。
内容的提问来源于stack exchange,提问作者Murodjon Abdukholikov
相关产品推荐
相关产品推荐

