如何避免ML-Kit条码扫描触发函数重复调用的竞态问题?
问题描述
在Flutter项目中使用Google ML-Kit条码扫描插件和默认相机插件时,扫描二维码跳转页面后返回,导航流程会被再次触发。日志排查发现,相机图像流的_processCameraImage()方法会快速调用parseImageForQRCode(),导致_isBusy变量无法及时更新,无法起到互斥作用,引发竞态问题。
相关代码(camera_screen.dart)
class CameraComponent extends StatefulWidget { const CameraComponent({Key? key}) : super(key: key); @override State<CameraComponent> createState() => _CameraComponentState(); } class _CameraComponentState extends State<CameraComponent> with WidgetsBindingObserver { CameraCubit get _cubit => context.read<CameraCubit>(); CameraController? _controller; final BarcodeScanner _barcodeScanner = BarcodeScanner(); bool _canProcess = true; bool _isBusy = false; CameraDescription? _camera; final ArtworkRepository _artworkRepository = getIt<ArtworkRepository>(); Future<CameraDescription> getCamera() async { // Obtain a list of the available cameras on the device. final List<CameraDescription> cameras = await availableCameras(); // Get a specific camera from the list of available cameras. return cameras.first; } void _navigateToArtworkDetailsById({required int artworkId}) { context.navigator.pushNamed( ArtworkViewScreen.path, arguments: <String, dynamic>{ 'artworkId': artworkId, 'source': ArtworkViewSourceScreen.QRScan.name }, ); } @override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _cubit.init(); startCameraSetup(); } Future<void> startCameraSetup() async { _canProcess = true; _camera = await getCamera(); //todo:sp handle camera being null if (_camera == null) { log('camera was null!'); displayErrorDialog('Camera Error', 'Looks like the camera is busy or unavailable! Please try again later.'); return; } _controller = CameraController( _camera!, ResolutionPreset.high, enableAudio: false, ); _controller?.initialize().then((_) { if (!mounted) { return; } _controller?.startImageStream(_processCameraImage); setState(() {}); }); } Future _processCameraImage(CameraImage image) async { final WriteBuffer allBytes = WriteBuffer(); for (final Plane plane in image.planes) { allBytes.putUint8List(plane.bytes); } final Uint8List bytes = allBytes.done().buffer.asUint8List(); final Size imageSize = Size(image.width.toDouble(), image.height.toDouble()); final InputImageRotation? imageRotation = InputImageRotationValue.fromRawValue(_camera!.sensorOrientation); if (imageRotation == null) { return; } final InputImageFormat? inputImageFormat = InputImageFormatValue.fromRawValue(image.format.raw); if (inputImageFormat == null) { return; } final List<InputImagePlaneMetadata> planeData = image.planes.map( (Plane plane) { return InputImagePlaneMetadata( bytesPerRow: plane.bytesPerRow, height: plane.height, width: plane.width, ); }, ).toList(); final InputImageData inputImageData = InputImageData( size: imageSize, imageRotation: imageRotation, inputImageFormat: inputImageFormat, planeData: planeData, ); final InputImage inputImage = InputImage.fromBytes(bytes: bytes, inputImageData: inputImageData); parseImageForQRCode(inputImage); } Future parseImageForQRCode(InputImage inputImage) async { if (!_canProcess) { return; } if (_isBusy) { return; } setState(() { _isBusy = true; }); final List<Barcode> barcodes = await _barcodeScanner.processImage(inputImage); if (barcodes.isNotEmpty) { final Barcode qrcode = barcodes[0]; if (qrcode.rawValue != null) { try { final int artworkId = int.parse(qrcode.rawValue!.split('_')[1]); _controller!.stopImageStream(); Future.delayed(const Duration(milliseconds: 100), () async { _navigateToArtworkDetailsById( artworkId: artworkId, ); await _artworkRepository.scanArtwork(id: artworkId); }); } catch (e) { print(e); displayErrorDialog('Oops', 'Looks like something went wrong! Please make sure you\'re using a valid QR code & try again.'); } } } _isBusy = false; if (mounted) { setState(() {}); } } void displayErrorDialog(String title, String msg) { showDialog<void>( context: context, builder: (BuildContext ctx) => AlertDialog( title: Text( title, textAlign: TextAlign.center, ), content: Text(msg), actions: [ TextButton( onPressed: () { Navigator.of(ctx).pop(); }, child: const Text('Ok'), ), ], ), ); } @override void dispose() { cleanup(); WidgetsBinding.instance.removeObserver(this); super.dispose(); } Future cleanup() async { _canProcess = false; _isBusy = false; _barcodeScanner.close(); if (_controller != null && _controller!.value.isInitialized) { try { await _controller?.stopImageStream(); } catch (e) { print(e); } } await _controller?.dispose(); _controller = null; } @override void didChangeAppLifecycleState(AppLifecycleState state) { log(state.name); // final CameraController? cameraController = _controller; // // App state changed before we got the chance to initialize. // if (cameraController == null || !cameraController.value.isInitialized) { // return; // } if (state == AppLifecycleState.inactive) { log('inactive being called'); cleanup(); } else if (state == AppLifecycleState.resumed) { log('on resume called'); startCameraSetup(); } } @override Widget build(BuildContext context) { return Scaffold( body: BlocConsumer<CameraCubit, CameraState>( listener: (BuildContext context, CameraState state) { state.maybeWhen( initCamera: (List<CameraDescription> cameras) {}, orElse: () {}, ); }, builder: (BuildContext context, CameraState state) { return state.maybeWhen( initCamera: (_) { return Scaffold( appBar: AppBar( centerTitle: true, title: Text( 'Take a picture', style: montserratRegular15.copyWith(color: AppColors.black), ), ), body: Stack( children: [ if (_controller?.value.isInitialized == true) CameraPreview(_controller!) else const Center( child: CircularProgressIndicator( color: AppColors.red, ), ), Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.only( bottom: 130, ), child: Text( 'Scan a QR code or take a photo of\nthe artwork', textAlign: TextAlign.center, style: montserratRegular15.copyWith( color: AppColors.white, ), ), ), ), Align( alignment: Alignment.bottomCenter, child: Padding( padding: const EdgeInsets.only(bottom: 30), child: GestureDetector( onTap: () async { // Ensure that the camera is initialized. if (!_controller!.value.isInitialized) { displayErrorDialog('Camera Error', 'Looks like the camera is busy or unavailable. Please try again later.'); return; } await _controller!.stopImageStream(); final XFile image = await _controller!.takePicture(); final Uint8List bytes = await image.readAsBytes(); final String base64Image = base64Encode(bytes); final int artworkId = await _cubit.imageSearchByBase64( base64Image: base64Image, ); if (_cubit.state is ArtworkIdFound$) { _navigateToArtworkDetailsById( artworkId: artworkId, ); _cubit.init(); } }, child: Image.asset( 'assets/icons/shutter_icon.png', height: 72, width: 72, ), ), ), ), Align( child: Padding( padding: const EdgeInsets.all(50), child: Image.asset('assets/icons/b_logo_grey.png'), ), ), ], ), ); }, loading: () => Column( mainAxisAlignment: MainAxisAlignment.center, children: const <Widget>[ Center( child: CircularProgressIndicator( color: AppColors.red, ), ), SizedBox( height: 10, ), Text( 'Performing image recognition...', style: montserratRegular15, ) ], ), initial: () => const Center( child: CircularProgressIndicator( color: AppColors.red, ), ), artworkIdError: () { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ const Text( 'Failed to recognize artwork!', style: montserratRegular15, ), const SizedBox( height: 20, ), Padding( padding: const EdgeInsets.symmetric(horizontal: 100), child: AppButton( onTap: () { _cubit.init(); setState(() {}); }, text: 'Retry', ), ), ], ); }, orElse: () { return Column( mainAxisAlignment: MainAxisAlignment.center, children: const <Widget>[ Center( child: CircularProgressIndicator( color: AppColors.red, ), ), SizedBox( height: 10, ), Text( 'Performing image recognition...', style: montserratRegular15, ) ], ); }, ); }, ), ); } }
推荐解决方案
方案1:同步更新_isBusy变量,避免异步setState延迟
原代码中setState(() => _isBusy = true)是异步执行的,导致在setState完成前,新的parseImageForQRCode()调用仍会认为_isBusy为false。修改为同步更新变量后再触发setState:
Future parseImageForQRCode(InputImage inputImage) async { if (!_canProcess || _isBusy) { return; } // 先同步设置_isBusy,立即生效阻挡后续调用 _isBusy = true; if (mounted) { setState(() {}); // 仅更新UI,变量已同步修改 } try { final List<Barcode> barcodes = await _barcodeScanner.processImage(inputImage); if (barcodes.isNotEmpty) { final Barcode qrcode = barcodes[0]; if (qrcode.rawValue != null) { try { final int artworkId = int.parse(qrcode.rawValue!.split('_')[1]); await _controller!.stopImageStream(); _canProcess = false; // 直接禁止后续处理,直到返回后重启 await _artworkRepository.scanArtwork(id: artworkId); _navigateToArtworkDetailsById(artworkId: artworkId); } catch (e) { print(e); displayErrorDialog('Oops', '请确保使用有效的二维码,重试一次'); } } } } catch (e) { print(e); } finally { // 仅在未处理成功二维码时重置_isBusy if (_canProcess) { _isBusy = false; if (mounted) { setState(() {}); } } } }
方案2:使用信号量严格控制并发执行
通过package:async库的Semaphore,限制同时只有一个parseImageForQRCode()函数执行,从根源避免竞态:
- 添加依赖:在
pubspec.yaml中加入
dependencies: async: ^2.11.0
- 修改代码:
import 'package:async/async.dart'; class _CameraComponentState extends State<CameraComponent> with WidgetsBindingObserver { // ... 其他变量 final Semaphore _semaphore = Semaphore(1); // 限制并发数为1 Future parseImageForQRCode(InputImage inputImage) async { if (!_canProcess) { return; } // 尝试获取信号量,无等待直接返回(避免阻塞) if (!await _semaphore.acquire(timeout: Duration.zero)) { return; } try { if (!_canProcess) return; // 双重检查防止状态变化 final List<Barcode> barcodes = await _barcodeScanner.processImage(inputImage); if (barcodes.isNotEmpty) { final Barcode qrcode = barcodes[0]; if (qrcode.rawValue != null) { try { final int artworkId = int.parse(qrcode.rawValue!.split('_')[1]); await _controller!.stopImageStream(); _canProcess = false; await _artworkRepository.scanArtwork(id: artworkId); _navigateToArtworkDetailsById(artworkId: artworkId); } catch (e) { print(e); displayErrorDialog('Oops', '请确保使用有效的二维码,重试一次'); } } } } finally { _semaphore.release(); // 释放信号量 } } }
方案3:扫描成功后彻底终止图像流并禁止处理
扫描到有效二维码后,不仅停止图像流,还直接设置_canProcess = false,确保所有后续的图像帧处理请求直接返回。在页面返回后,通过didChangeAppLifecycleState的resumed状态重新初始化相机时,再将_canProcess设为true。
内容的提问来源于stack exchange,提问作者Stelios Papamichail
相关产品推荐
相关产品推荐

