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); } }
问题根源
- 模态控制器层级错误:在
PhotoCaptureDelegate_ImagePicked中,通过遍历窗口根控制器呈现预览界面,导致预览控制器与ScanIdViewController的层级关联断裂,后续Dismiss操作无法正确回退到相机界面上下文。 - AVCaptureSession 资源未清理:停止会话后未移除输入输出资源,也未释放会话实例,导致相机资源被占用,界面冻结。
- 事件引用泄漏:
CameraHelper中对ScanIdViewController.ImagePicked的强引用未移除,导致控制器无法被GC回收,引发界面无响应。 - 废弃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
相关产品推荐
相关产品推荐

