You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

排查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()
        },
    )
}

问题分析与解决方案

核心问题

你的代码存在三个关键问题导致扫描停止:

  1. captureSession未被持久持有:Compose重组时会重新创建AVCaptureSession实例,但预览层绑定的是旧实例,导致元数据输出代理失效。
  2. 元数据代理对象被GC回收:匿名代理对象没有强引用持有,iOS的ARC机制会在几次扫描后回收该对象,后续扫描事件无法触发回调。
  3. 会话启动的协程作用域不稳定:使用临时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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.22 23:12:32