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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 18:25:54