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

SwiftUI中UIImagePickerController关闭后相机仍被占用问题求助

问题根因
  • UIImagePickerController被声明为VideoRecordingView的存储属性,且被Coordinator强持有,dismiss后实例无法释放,内部AVCapture会话持续运行,导致相机占用指示灯常亮
  • Coordinator初始化时添加的通知中心观察者未做移除操作,且通知闭包强持有了parent实例,造成循环引用,进一步导致Coordinator和UIImagePickerController整体内存泄漏
  • 切后台时仅执行了stopVideoCapture,你注释了dismiss代码,此时UIImagePickerController仍处于活跃状态,返回前台后会自动恢复相机访问
  • dismiss选择器时未手动清空UIImagePickerController的delegate、sourceType等属性,系统内部可能仍保留相机资源引用
修复方案
  1. 不要把UIImagePickerController作为结构体存储属性,改为在makeUIViewController方法内局部创建,避免不必要的强持有
  2. 修复Coordinator循环引用问题,通知闭包添加弱引用标记,deinit时移除通知观察者
  3. 恢复切后台时的dismiss逻辑,确保后台时picker被正常关闭
  4. dismiss前主动清理picker的相关属性,强制释放相机资源
修复后完整代码
import SwiftUI
import AVFoundation
import Photos

struct VideoRecordingView: UIViewControllerRepresentable {
    @Binding var videoURL: URL?
    @Environment(\.viewController) private var viewControllerHolder: UIViewController?

    init(videoURL: Binding<URL?>) {
        self._videoURL = videoURL
    }

    func makeUIViewController(context: Context) -> UIImagePickerController {
        // 改为局部创建,不做存储属性
        let imagePickerController = UIImagePickerController()
        imagePickerController.allowsEditing = true
        imagePickerController.sourceType = .camera
        imagePickerController.mediaTypes = ["public.movie"]
        imagePickerController.videoMaximumDuration = .infinity
        if #available(iOS 14.0, *) {
            imagePickerController.videoQuality = .typeHigh
        }
        imagePickerController.delegate = context.coordinator
        return imagePickerController
    }

    func updateUIViewController(_ uiViewController: UIImagePickerController, context: Context) {}

    func makeCoordinator() -> Coordinator {
        Coordinator(self)
    }

    final class Coordinator: NSObject, UIImagePickerControllerDelegate, UINavigationControllerDelegate {
        weak var parent: VideoRecordingView? // 改为弱引用避免循环持有
        private let notificationObserver: NSObjectProtocol?

        init(_ parent: VideoRecordingView) {
            self.parent = parent
            // 保存观察者引用方便后续移除
            notificationObserver = NotificationCenter.default.addObserver(
                forName: UIApplication.willResignActiveNotification,
                object: nil,
                queue: .main
            ) { [weak self] notification in
                guard let self = self, let picker = self.parent?.viewControllerHolder?.presentedViewController as? UIImagePickerController else { return }
                picker.stopVideoCapture()
                // 恢复dismiss逻辑,后台时直接关闭picker
                self.parent?.viewControllerHolder?.dismiss(animated: true)
            }
        }

        deinit {
            // 移除通知观察者
            if let observer = notificationObserver {
                NotificationCenter.default.removeObserver(observer)
            }
        }

        func requestAuthorizationToPhotos(completionHandler: @escaping (Bool) -> Void) {
            guard PHPhotoLibrary.authorizationStatus() != .authorized else {
                completionHandler(true)
                return
            }
            PHPhotoLibrary.requestAuthorization { status in
                completionHandler(status == .authorized)
            }
        }

        func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
            picker.stopVideoCapture()
            cleanUpPicker(picker)
            parent?.viewControllerHolder?.dismiss(animated: true)
        }

        func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
            picker.stopVideoCapture()

            if let videoURL = info[.mediaURL] as? URL {
                DispatchQueue.global(qos: .background).async { [weak self] in
                    let fileURL = videoURL.copyFileToTempDirectory()
                    DispatchQueue.main.async {
                        self?.parent?.videoURL = fileURL
                    }

                    self?.requestAuthorizationToPhotos { granted in
                        if granted, UIVideoAtPathIsCompatibleWithSavedPhotosAlbum(videoURL.path) {
                            UISaveVideoAtPathToSavedPhotosAlbum(videoURL.path, nil, nil, nil)
                        }
                    }
                }
            }

            cleanUpPicker(picker)
            parent?.viewControllerHolder?.dismiss(animated: true)
        }

        // 新增picker资源清理方法
        private func cleanUpPicker(_ picker: UIImagePickerController) {
            picker.delegate = nil
            picker.sourceType = .photoLibrary // 切换源类型强制释放相机资源
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.25 01:36:02