Flutter Camera插件在iPhone XR设备上无法初始化控制器
Flutter相机组件在iOS设备初始化失败/延迟问题
问题描述
相机组件在Android设备上可正常初始化,但在iPhone XR(iOS系统)上要么初始化失败,要么存在严重延迟。
相机组件代码
import 'dart:async'; import 'package:camera/camera.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import '../images/cached_image.dart'; import '../models/image.dart'; import '../theme/extensions/colors.dart'; import 'camera_controls.dart'; class Camera extends StatefulWidget { const Camera({ super.key, }); @override State<Camera> createState() => _CameraState(); } class _CameraState extends State<Camera> with WidgetsBindingObserver, TickerProviderStateMixin { CameraController? _controller; XFile? _imageFile; final bool _enableAudio = false; double _minAvailableZoom = 1; double _maxAvailableZoom = 1; double _currentScale = 1; double _baseScale = 1; // Counting pointers (number of user fingers on screen) int _pointers = 0; var _cameras = <CameraDescription>[]; bool get _controllerIsAvailable => _controller != null; bool get _isInitialized => _controllerIsAvailable && _controller!.value.isInitialized; bool get _flashIsOff => _controllerIsAvailable && _controller!.value.flashMode == FlashMode.off; bool get _isBackCamera => _controllerIsAvailable && _controller!.description.lensDirection == CameraLensDirection.back; @override void initState() { super.initState(); WidgetsBinding.instance.addPostFrameCallback((_) async { SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( statusBarColor: Colors.black, ), ); await _startCamera(); }); } @override void dispose() { WidgetsBinding.instance.addPostFrameCallback((_) async { await _controller?.dispose(); SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle( statusBarColor: Colors.transparent, ), ); }); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { final cameraController = _controller; // App state changed before we got the chance to initialize. if (_controllerIsAvailable && !cameraController!.value.isInitialized) { return; } WidgetsBinding.instance.addPostFrameCallback((_) async { if (state == AppLifecycleState.inactive) { await cameraController?.dispose(); } else if (state == AppLifecycleState.resumed) { await _initializeCameraController(cameraController!.description); } }); } @override Widget build(BuildContext context) => Stack( children: [ if (_isInitialized) Positioned.fill( child: AspectRatio( aspectRatio: _controller!.value.aspectRatio, child: Listener( onPointerDown: (_) => _pointers++, onPointerUp: (_) => _pointers--, child: CameraPreview( _controller!, child: LayoutBuilder( builder: (context, constraints) => GestureDetector( behavior: HitTestBehavior.opaque, onScaleStart: _handleScaleStart, onScaleUpdate: _handleScaleUpdate, onTapDown: (details) async => onViewFinderTap( details, constraints, ), ), ), ), ), ), ), if (_imageFile != null) Positioned.fill( child: CachedImage( image: LocalImageVm(url: _imageFile!.path), ), ), Align( alignment: Alignment.topCenter, child: ColoredBox( color: Colors.black, child: Row( children: [ const SizedBox(width: 8), if (_imageFile == null || _isBackCamera) IconButton( color: context.colorsTheme.background, icon: Icon( _flashIsOff ? Icons.flash_off : Icons.flash_on, ), onPressed: () async { await _toggleFlashlight(); }, ), ], ), ), ), Align( alignment: Alignment.bottomCenter, child: CameraControls( onPressedResetImage: _imageFile != null ? _resetImageFile : () async => Navigator.of(context).pop(), onPressedTakePicture: _imageFile == null ? _onPressedTakePicture : null, onPressedSwitchCamera: _imageFile != null ? () async { Navigator.of(context).pop(_imageFile); } : () async { await onNewCameraSelected(); }, ), ), ], ); Future<void> _startCamera() async { _cameras = await availableCameras(); if (_cameras.isNotEmpty) { await _initializeCameraController(_cameras[0]); } } void _handleScaleStart(ScaleStartDetails details) { _baseScale = _currentScale; } Future<void> _handleScaleUpdate(ScaleUpdateDetails details) async { // When there are not exactly two fingers on screen don't scale if (_pointers != 2) { return; } _currentScale = (_baseScale * details.scale) .clamp(_minAvailableZoom, _maxAvailableZoom); await _controller!.setZoomLevel(_currentScale); } Future<void> _resetImageFile() async { setState(() { _imageFile = null; }); } Future<void> _toggleFlashlight() async { try { await _controller!.setFlashMode( _controller!.value.flashMode == FlashMode.off ? FlashMode.torch : FlashMode.off, ); } on CameraException catch (e) { _showCameraException(e); } } Future<void> onViewFinderTap( TapDownDetails details, BoxConstraints constraints, ) async { final cameraController = _controller; final offset = Offset( details.localPosition.dx / constraints.maxWidth, details.localPosition.dy / constraints.maxHeight, ); await cameraController!.setExposurePoint(offset); await cameraController.setFocusPoint(offset); } Future<void> onNewCameraSelected() async { final description = !_isBackCamera ? _cameras[0] : _cameras[1]; return _controller!.setDescription(description); } Future<void> _initializeCameraController( CameraDescription cameraDescription, ) async { await _controller?.dispose(); _controller = CameraController( cameraDescription, ResolutionPreset.max, enableAudio: _enableAudio, imageFormatGroup: ImageFormatGroup.jpeg, ); await _controller!.setFlashMode(FlashMode.off); _controller!.addListener(() { if (mounted) { setState(() {}); } if (_controller!.value.hasError) { throw Exception('Camera error ${_controller!.value.errorDescription}'); } }); try { await _controller!.initialize(); await _controller! .getMaxZoomLevel() .then((value) => _maxAvailableZoom = value); await _controller! .getMinZoomLevel() .then((value) => _minAvailableZoom = value); } on CameraException catch (e) { _showCameraException(e); } if (mounted) { setState(() {}); } } Future<void> _onPressedTakePicture() async { final file = await takePicture(); if (mounted) { setState(() { _imageFile = file; }); } // await onPausePreview(); } Future<void> setFlashMode(FlashMode mode) async { try { await _controller!.setFlashMode(mode); } on CameraException catch (e) { _showCameraException(e); } } Future<XFile?> takePicture() async { final cameraController = _controller; if (!cameraController!.value.isInitialized) { throw Exception('Error: select a camera first.'); } if (cameraController.value.isTakingPicture) { // A capture is already pending, do nothing. return null; } try { final file = await cameraController.takePicture(); return file; } on CameraException catch (e) { _showCameraException(e); } return null; } void _showCameraException(CameraException e) { throw Exception('Error: ${e.code}\n${e.description}'); } }
已尝试方案
试过多种初始化方案,添加了_isInitialized变量检查,但iOS端控制器仍无法正常初始化。
flutter doctor 输出
Doctor summary (to see all details, run flutter doctor -v): [✓] Flutter (Channel stable, 3.22.2, on macOS 14.5 23F79 darwin-arm64, locale en-UA) [✓] Android toolchain - develop for Android devices (Android SDK version 35.0.0) [✓] Xcode - develop for iOS and macOS (Xcode 15.4) [✓] Chrome - develop for the web [✓] Android Studio (version 2024.1) [✓] VS Code (version 1.90.2) [✓] Connected device (4 available) ! Error: Browsing on the local area network for Hindenburg. Ensure the device is unlocked and attached with a cable or associated with the same local area network as this Mac. The device must be opted into Developer Mode to connect wirelessly. (code -27) [✓] Network resources
修复建议
1. 检查iOS权限配置
iOS要求必须在Info.plist中添加相机权限描述,否则相机无法正常初始化:
- 添加
NSCameraUsageDescription:描述应用使用相机的原因,比如"需要访问相机以拍摄照片" - 如果启用了音频(即使当前
_enableAudio为false),还需添加NSMicrophoneUsageDescription
2. 降低分辨率预设
ResolutionPreset.max在iOS设备上可能因分辨率过高导致初始化延迟或失败,建议换成ResolutionPreset.high或ResolutionPreset.medium,减少系统资源占用:
_controller = CameraController( cameraDescription, ResolutionPreset.high, // 替换max为high enableAudio: _enableAudio, imageFormatGroup: ImageFormatGroup.jpeg, );
3. 优化初始化流程
- 移除
addPostFrameCallback嵌套,直接在initState中异步处理,同时确保组件已挂载:
@override void initState() { super.initState(); WidgetsBinding.instance.addObserver(this); _initCamera(); } Future<void> _initCamera() async { await SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle(statusBarColor: Colors.black), ); if (!mounted) return; await _startCamera(); }
- 初始化前先检查相机权限,避免权限不足导致初始化失败:
Future<void> _startCamera() async { // 先检查权限 final status = await Permission.camera.request(); if (status != PermissionStatus.granted) { // 提示用户授权 if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('需要相机权限才能使用相机功能')), ); } return; } _cameras = await availableCameras(); if (_cameras.isNotEmpty && mounted) { await _initializeCameraController(_cameras[0]); } }
4. 优化App生命周期处理
当前didChangeAppLifecycleState中,resumed时直接重新初始化控制器可能导致重复操作,优化逻辑:
@override void didChangeAppLifecycleState(AppLifecycleState state) { final cameraController = _controller; if (cameraController == null) return; if (state == AppLifecycleState.inactive) { cameraController.dispose(); } else if (state == AppLifecycleState.resumed) { if (!cameraController.value.isInitialized) { _initializeCameraController(cameraController.description); } } }
5. 修复相机切换逻辑
iOS上使用setDescription切换相机可能存在兼容性问题,建议重新初始化控制器:
Future<void> onNewCameraSelected() async { final newCamera = !_isBackCamera ? _cameras[0] : _cameras[1]; await _initializeCameraController(newCamera); }
6. 优化错误处理
当前_showCameraException直接抛出异常会中断初始化流程,改为友好提示:
void _showCameraException(CameraException e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('相机初始化失败:${e.description}')), ); } debugPrint('Camera error: ${e.code} - ${e.description}'); }
7. 完善内存管理
dispose时先移除监听器,再释放控制器:
@override void dispose() { WidgetsBinding.instance.removeObserver(this); _controller?.removeListener(() {}); _controller?.dispose(); SystemChrome.setSystemUIOverlayStyle( const SystemUiOverlayStyle(statusBarColor: Colors.transparent), ); super.dispose(); }
内容的提问来源于stack exchange,提问作者Oleksandr Krynytskyi
相关产品推荐
相关产品推荐

