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

Xamarin iOS中CaptureSession关联ViewController无法关闭问题

Xamarin iOS 相机预览界面冻结问题修复

问题现象

在Xamarin iOS项目中,通过CameraHelper.ScanId弹出带AVCaptureSession的ScanIdViewController拍照,完成后弹出PreviewImageViewController预览照片。确认照片后预览控制器可正常关闭,但ScanIdViewController的相机界面冻结,按钮失去响应,无法返回原视图。

相关代码

CameraHelper 中的 ScanId 方法

public override Task<ImageData> ScanId(PictureQuality quality)
{
    _quality = quality;                  
    _scanIdViewController = new ScanIdViewController();
    _scanIdViewController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
    _scanIdViewController.ImagePicked += CustomCam_ImagePicked;
    UIWindow window = UIApplication.SharedApplication.KeyWindow;
    var rootController = window.RootViewController;
    

    while (rootController.PresentedViewController != null)
        rootController = rootController.PresentedViewController;
    rootController.PresentViewController(_scanIdViewController, true, null);

    _imageDataPickedCompletionSource = new TaskCompletionSource<ImageData> { };
    return _imageDataPickedCompletionSource.Task;

}

private void CustomCam_ImagePicked(object sender, EventArgs e)
{
    if (sender is ScanIdViewController scanIdController && scanIdController.Image is { })
    {
        using (NSData imageData = scanIdController.Image.AsPNG())
        {
            byte[] data = new byte[imageData.Length];
            Marshal.Copy(imageData.Bytes, data, 0, Convert.ToInt32(imageData.Length));
            var scaledBytes = GetScaledImage(data);
            _imageDataPickedCompletionSource.SetResult(new ImageData(scaledBytes, null, RotationNeeded(GetPicInfo(data)), true));
        };
    }
}

ScanIdViewController 代码

public class ScanIdViewController: UIViewController
{
    public event EventHandler ImagePicked;
    public UIImage Image { get; private set; }
    private PhotoCaptureDelegate _delegate;
    private AVCaptureSession _captureSession;
    private CGRect _outlineRect;
    IExceptionHandler _exceptionHandler = DependencyContainer.Current.Resolve<IExceptionHandler>();
    public override void ViewDidLoad()
    {
        base.ViewDidLoad();

        // 创建AVCaptureSession
        _captureSession = new AVCaptureSession() { SessionPreset = AVCaptureSession.PresetPhoto };

        // 设置会话为高质量预设
        _captureSession.SessionPreset = AVCaptureSession.PresetHigh;

        // 获取后置摄像头
        var backCamera = AVCaptureDevice.GetDefaultDevice(AVCaptureDeviceType.BuiltInWideAngleCamera, AVMediaTypes.Video, AVCaptureDevicePosition.Back);

        // 创建摄像头输入
        var input = AVCaptureDeviceInput.FromDevice(backCamera);

        // 添加输入到会话
        _captureSession.AddInput(input);

        // 创建预览图层
        var previewLayer = new AVCaptureVideoPreviewLayer(_captureSession);
        SetupPreviewLayer(previewLayer);
        var capturePhotoOutput = new AVCapturePhotoOutput();


        _delegate = new PhotoCaptureDelegate();
        _delegate.ImagePicked += PhotoCaptureDelegate_ImagePicked;
        _captureSession.AddOutput(capturePhotoOutput);

        // 创建拍照按钮
        var captureButton = new UIButton(UIButtonType.System);
        captureButton.Frame = new CGRect(0, 0, 50, 50);
        captureButton.Center = new CGPoint(View.Bounds.GetMidX(), View.Bounds.GetMaxY() - 50);
        captureButton.BackgroundColor = UIColor.White;
        captureButton.Layer.CornerRadius = 25;
        captureButton.TouchUpInside += (sender, e) =>
        {
            var photoSettings = AVCapturePhotoSettings.Create();
            capturePhotoOutput.CapturePhoto(photoSettings, _delegate);
        };
        View.AddSubview(captureButton);

        _captureSession.StartRunning();
    }

    private void PhotoCaptureDelegate_ImagePicked(object sender, EventArgs e)
    {
        if (sender is PhotoCaptureDelegate del && del.Image is { })
        {
            var bounds = UIScreen.MainScreen.Bounds;
            var aspectX = del.Image.Size.Width / bounds.Width;
            var aspectY = del.Image.Size.Height / bounds.Height;
            var cutImage = del.Image.CropImage((int)(_outlineRect.X * aspectX), (int)(_outlineRect.Y * aspectY), (int)(_outlineRect.Width * aspectX), (int)(_outlineRect.Height * aspectY));

            var previewImageView = new UIImageView(new CGRect(0, 0, UIScreen.MainScreen.Bounds.Width, UIScreen.MainScreen.Bounds.Height - 100)) { Image = cutImage };
            var previewController = new PreviewImageViewController(previewImageView);
            previewController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
            previewController.ImageAccepted += PreviewController_ImageAccepted;

            UIWindow window = UIApplication.SharedApplication.KeyWindow;
            var rootController = window.RootViewController;

            while (rootController.PresentedViewController != null)
                rootController = rootController.PresentedViewController;
            rootController.PresentViewController(previewController, true, null);
        }
    }

    private void PreviewController_ImageAccepted(object sender, EventArgs e)
    {
        Image = _delegate.Image;
        ImagePicked?.Invoke(this, EventArgs.Empty);
        if (_captureSession.Running)
        {
            Device.BeginInvokeOnMainThread(() => _captureSession.StopRunning());
            Device.BeginInvokeOnMainThread(() => DismissModalViewController(true));
        }
    }


    private void SetupPreviewLayer(AVCaptureVideoPreviewLayer avCapturePreviewLayer)
    {
        avCapturePreviewLayer.VideoGravity = AVLayerVideoGravity.ResizeAspectFill;
        avCapturePreviewLayer.Connection.VideoOrientation = AVCaptureVideoOrientation.Portrait;
        avCapturePreviewLayer.Frame = UIScreen.MainScreen.Bounds;
        View.Layer.InsertSublayer(avCapturePreviewLayer, 0);
        var overlay = CreateImageOverlay();
        View.AddSubview(overlay);
    }
}

PreviewImageViewController 代码

public class PreviewImageViewController : UIViewController
{
    private UIImageView _previewImageView;
    public event EventHandler ImageAccepted;

    public PreviewImageViewController(UIImageView previewImageView)
    {
        _previewImageView = previewImageView;
    }

    public override void ViewDidLoad()
    {
        base.ViewDidLoad();
        SetUpPreviewImageView();

    }

    private void SetUpPreviewImageView()
    {
        _previewImageView.ContentMode = UIViewContentMode.ScaleAspectFit;


        UIButton acceptButton = new UIButton();
        acceptButton.SetTitle("使用照片", UIControlState.Normal);
        acceptButton.SetTitleColor(UIColor.White, UIControlState.Normal);
        acceptButton.BackgroundColor = UIColor.Black;
        acceptButton.Frame = new CGRect(0, View.Frame.Height - 100, View.Frame.Width / 2, 100);
        acceptButton.TouchUpInside += (sender, e) =>
        {
            ImageAccepted?.Invoke(this, EventArgs.Empty);
            DismissModalViewController(true);
        };

        UIButton retakeButton = new UIButton();
        retakeButton.Frame = new CGRect(View.Frame.Width / 2, View.Frame.Height - 100, View.Frame.Width / 2, 100);
        retakeButton.SetTitle("重新拍摄", UIControlState.Normal);
        retakeButton.SetTitleColor(UIColor.White, UIControlState.Normal);
        retakeButton.BackgroundColor = UIColor.Black;

        retakeButton.TouchUpInside += (sender, e) =>
        {
            DismissModalViewController(true);
        };
        View.AddSubview(_previewImageView);
        View.Add(acceptButton);
        View.Add(retakeButton);
    }
}

问题根源

  1. 模态控制器层级错误:在PhotoCaptureDelegate_ImagePicked中,通过遍历窗口根控制器呈现预览界面,导致预览控制器与ScanIdViewController的层级关联断裂,后续Dismiss操作无法正确回退到相机界面上下文。
  2. AVCaptureSession 资源未清理:停止会话后未移除输入输出资源,也未释放会话实例,导致相机资源被占用,界面冻结。
  3. 事件引用泄漏:CameraHelper中对ScanIdViewController.ImagePicked的强引用未移除,导致控制器无法被GC回收,引发界面无响应。
  4. 废弃API使用:DismissModalViewController是iOS旧版API,存在兼容性问题,无法保证正确的模态回退逻辑。

修复方案

1. 修正预览控制器呈现逻辑

在ScanIdViewController.PhotoCaptureDelegate_ImagePicked中,直接使用当前控制器呈现预览界面,替代根控制器遍历:

private void PhotoCaptureDelegate_ImagePicked(object sender, EventArgs e)
{
    if (sender is PhotoCaptureDelegate del && del.Image is { })
    {
        var bounds = UIScreen.MainScreen.Bounds;
        var aspectX = del.Image.Size.Width / bounds.Width;
        var aspectY = del.Image.Size.Height / bounds.Height;
        var cutImage = del.Image.CropImage((int)(_outlineRect.X * aspectX), (int)(_outlineRect.Y * aspectY), (int)(_outlineRect.Width * aspectX), (int)(_outlineRect.Height * aspectY));

        var previewImageView = new UIImageView(new CGRect(0, 0, UIScreen.MainScreen.Bounds.Width, UIScreen.MainScreen.Bounds.Height - 100)) { Image = cutImage };
        var previewController = new PreviewImageViewController(previewImageView);
        previewController.ModalPresentationStyle = UIModalPresentationStyle.FullScreen;
        previewController.ImageAccepted += PreviewController_ImageAccepted;

        // 直接用当前控制器呈现预览界面,保证层级关联
        this.PresentViewController(previewController, true, null);
    }
}

2. 优化会话停止与资源清理

替换废弃API,按顺序清理会话资源并回退界面:

private async void PreviewController_ImageAccepted(object sender, EventArgs e)
{
    Image = _delegate.Image;
    ImagePicked?.Invoke(this, EventArgs.Empty);
    
    if (_captureSession.Running)
    {
        await Device.InvokeOnMainThreadAsync(() => 
        {
            _captureSession.StopRunning();
            // 清理会话输入输出资源
            foreach (var input in _captureSession.Inputs.ToList())
            {
                _captureSession.RemoveInput(input);
            }
            foreach (var output in _captureSession.Outputs.ToList())
            {
                _captureSession.RemoveOutput(output);
            }
            _captureSession.Dispose();
            _captureSession = null;
        });
        
        // 使用新版API回退界面
        await this.DismissViewControllerAsync(true);
    }
}

3. 清理事件引用避免泄漏

在CameraHelper.CustomCam_ImagePicked中移除事件绑定:

private void CustomCam_ImagePicked(object sender, EventArgs e)
{
    if (sender is ScanIdViewController scanIdController && scanIdController.Image is { })
    {
        using (NSData imageData = scanIdController.Image.AsPNG())
        {
            byte[] data = new byte[imageData.Length];
            Marshal.Copy(imageData.Bytes, data, 0, Convert.ToInt32(imageData.Length));
            var scaledBytes = GetScaledImage(data);
            _imageDataPickedCompletionSource.SetResult(new ImageData(scaledBytes, null, RotationNeeded(GetPicInfo(data)), true));
        };
        
        // 移除事件绑定,释放控制器引用
        scanIdController.ImagePicked -= CustomCam_ImagePicked;
        _scanIdViewController = null;
    }
}

4. 更新Preview控制器的Dismiss方法

将PreviewImageViewController中的DismissModalViewController替换为新版API:

acceptButton.TouchUpInside += async (sender, e) =>
{
    ImageAccepted?.Invoke(this, EventArgs.Empty);
    await DismissViewControllerAsync(true);
};

retakeButton.TouchUpInside += async (sender, e) =>
{
    await DismissViewControllerAsync(true);
};

内容的提问来源于stack exchange,提问作者Kriska

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 19:07:02