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

如何避免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()函数执行,从根源避免竞态:

  1. 添加依赖:在pubspec.yaml中加入
dependencies:
  async: ^2.11.0
  1. 修改代码:
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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 07:35:19