Why Your Swift Camera App Crashes on iPhone and How to Fix It Properly
你是不是也遇到过这种情况:兴致勃勃地写好了一个相机App,在模拟器上跑得欢,结果一跑到真机上——啪,直接闪退。日志里还只有一行让你摸不着头脑的崩溃信息,心里那个郁闷啊。
别急,这篇文章就是来帮你理清头绪的。我会结合真实的开发经验和具体的代码示例,把那些让开发者抓狂的相机崩溃问题一个个掰开揉碎讲清楚。
权限问题:最容易被忽视的”拦路虎”
还记得我第一次写相机App时,死活搞不明白为什么一打开就崩溃。后来才发现,原来我连相机的权限都没问用户要。这就像你要进别人的家,连敲门都不敲,被人轰出来也是正常的。
在Swift中,我们需要在Info.plist文件中正确配置权限描述,然后在代码中主动请求权限:
import AVFoundation
class CameraViewController: UIViewController {
private let captureSession = AVCaptureSession()
private var isSessionRunning = false
override func viewDidLoad() {
super.viewDidLoad()
setupCamera()
}
private func setupCamera() {
// 第一步:检查权限
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
// 权限已授权,直接启动
startCamera()
case .notDetermined:
// 权限未决定,请求授权
AVCaptureDevice.requestAccess(for: .video) { [weak self] granted in
if granted {
DispatchQueue.main.async {
self?.startCamera()
}
} else {
self?.showPermissionDeniedAlert()
}
}
case .denied, .restricted:
// 权限被拒绝或受限,引导用户去设置
showPermissionDeniedAlert()
@unknown default:
showPermissionDeniedAlert()
}
}
private func startCamera() {
guard !isSessionRunning else { return }
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
self?.configureCaptureSession()
self?.startSession()
}
}
private func showPermissionDeniedAlert() {
let alert = UIAlertController(
title: "需要相机权限",
message: "请在设置中允许相机访问,以便使用相机功能。",
preferredStyle: .alert
)
alert.addAction(UIAlertAction(title: "去设置", style: .default) { _ in
if let url = URL(string: UIApplication.openSettingsURLString) {
UIApplication.shared.open(url)
}
})
alert.addAction(UIAlertAction(title: "取消", style: .cancel))
present(alert, animated: true)
}
}
同时,别忘了在Info.plist中添加这两个键值:
<key>NSCameraUsageDescription</key>
<string>我们需要相机权限来拍摄照片和视频</string>
<key>NSMicrophoneUsageDescription</key>
<string>我们需要麦克风权限来录制视频时的音频</string>
相机会话管理:多线程环境下的”踩地雷”
相机会话(AVCaptureSession)是个敏感的角色。它最忌讳的就是在错误的线程上被操作。很多人写代码时图省事,直接在主线程上配置会话,结果不是卡死就是崩溃。
正确的做法是:配置会话用后台线程,更新UI用主线程。
class CameraManager: NSObject {
private let session = AVCaptureSession()
private var photoOutput: AVCapturePhotoOutput?
private var previewLayer: AVCaptureVideoPreviewLayer?
private weak var delegate: CameraDelegate?
// 使用 DispatchQueue 来确保线程安全
func configureCaptureSession(with device: AVCaptureDevice) {
// 在后台线程配置会话
DispatchQueue.global(qos: .userInitiated).async { [weak self] in
guard let self = self else { return }
// 开始配置,阻止自动调整
self.session.beginConfiguration()
// 设置会话预设(根据需求调整质量)
self.session.sessionPreset = .high
// 添加输入
do {
let input = try AVCaptureDeviceInput(device: device)
if self.session.canAddInput(input) {
self.session.addInput(input)
}
} catch {
print("添加输入失败: \(error)")
// 配置失败时,必须调用 commitConfiguration
self.session.commitConfiguration()
return
}
// 添加照片输出
let photoOutput = AVCapturePhotoOutput()
if self.session.canAddOutput(photoOutput) {
self.session.addOutput(photoOutput)
// 确保使用 RAW 格式支持(如果需要)
photoOutput.isHighResolutionPhotoEnabled = true
}
self.photoOutput = photoOutput
// 提交配置
self.session.commitConfiguration()
// 配置完成,通知主线程
DispatchQueue.main.async {
self.delegate?.cameraManager(self, didConfigureSession: true)
}
}
}
func startRunning() {
guard !session.isRunning else { return }
// startRunning 必须在主线程调用!
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
// 双重检查,防止并发问题
guard !self.session.isRunning else { return }
self.session.startRunning()
// 检查是否成功启动
if self.session.isRunning {
print("相机会话启动成功")
} else {
print("相机会话启动失败")
}
}
}
func stopRunning() {
guard session.isRunning else { return }
DispatchQueue.main.async { [weak self] in
self?.session.stopRunning()
print("相机会话已停止")
}
}
}
protocol CameraDelegate: AnyObject {
func cameraManager(_ manager: CameraManager, didConfigureSession success: Bool)
}
内存管理:那些悄悄溜走的”内存小偷”
相机App是内存消耗大户。如果你没有正确地管理内存,App很快就会因为内存警告而被系统杀掉。更糟的是,内存泄漏会让问题雪上加霜。
下面这个例子展示了如何正确地管理相机资源:
class CameraViewController: UIViewController {
// 使用 weak 引用避免循环引用
private weak var captureSession: AVCaptureSession?
private var photoCaptureDelegate: PhotoCaptureDelegate?
private var deviceInput: AVCaptureDeviceInput?
// 使用 @autoreleasepool 来处理大量图像数据
override func viewDidLoad() {
super.viewDidLoad()
setupCamera()
}
deinit {
// 析构函数中清理资源
releaseCameraResources()
print("CameraViewController 被释放")
}
private func setupCamera() {
let session = AVCaptureSession()
self.captureSession = session
// 使用 try-catch 来避免潜在的崩溃
if let device = AVCaptureDevice.default(.builtInWideAngleCamera,
for: .video,
position: .back) {
do {
let input = try AVCaptureDeviceInput(device: device)
if session.canAddInput(input) {
session.addInput(input)
self.deviceInput = input
}
} catch {
print("创建设备输入失败: \(error)")
}
}
// 创建照片输出
let photoOutput = AVCapturePhotoOutput()
if session.canAddOutput(photoOutput) {
session.addOutput(photoOutput)
}
// 设置代理时使用 weak,避免循环引用
let delegate = PhotoCaptureDelegate { [weak self] image, error in
guard let self = self else { return }
self.handlePhotoCapture(image: image, error: error)
}
self.photoCaptureDelegate = delegate
// 准备预览层
let previewLayer = AVCaptureVideoPreviewLayer(session: session)
previewLayer.frame = view.cameraPreview.bounds
previewLayer.videoGravity = .resizeAspectFill
view.cameraPreview.layer.addSublayer(previewLayer)
// 在后台线程启动会话
DispatchQueue.global(qos: .userInitiated).async {
session.startRunning()
}
}
private func releaseCameraResources() {
// 停止并释放会话
captureSession?.stopRunning()
captureSession = nil
// 释放输入
deviceInput = nil
// 释放代理
photoCaptureDelegate = nil
// 强制释放预览层
view.cameraPreview.layer.sublayers?.forEach {
if let layer = $0 as? AVCaptureVideoPreviewLayer {
layer.removeFromSuperlayer()
}
}
print("相机资源已释放")
}
private func handlePhotoCapture(image: UIImage?, error: Error?) {
if let error = error {
print("拍摄照片失败: \(error)")
return
}
guard let image = image else { return }
// 使用autoreleasepool处理大量数据
autoreleasepool {
// 在这里处理图像数据
processImage(image)
}
}
private func processImage(_ image: UIImage) {
// 图像处理逻辑
}
}
// 使用class而非struct来避免值类型拷贝开销
class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
private let completionHandler: (UIImage?, Error?) -> Void
init(completionHandler: @escaping (UIImage?, Error?) -> Void) {
self.completionHandler = completionHandler
}
func photoOutput(_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?) {
if let error = error {
completionHandler(nil, error)
return
}
guard let imageData = photo.fileDataRepresentation(),
let image = UIImage(data: imageData) else {
completionHandler(nil, nil)
return
}
completionHandler(image, nil)
}
}
线程安全问题:并发环境下的”多米诺骨牌”
相机操作中,线程安全是个大坑。很多人没有在正确的线程上调用API,导致各种奇奇怪怪的崩溃。
class ThreadSafeCameraController {
private let sessionQueue = DispatchQueue(label: "com.camera.sessionQueue",
attributes: .concurrent)
private var session: AVCaptureSession?
private var isConfigured = false
// 使用锁来保护共享状态
private let configLock = NSLock()
func configureCamera() {
configLock.lock()
defer { configLock.unlock() }
guard !isConfigured else { return }
let session = AVCaptureSession()
session.sessionPreset = .photo
// 配置输入输出...
self.session = session
isConfigured = true
}
func startCamera() {
sessionQueue.async(flags: .barrier) { [weak self] in
guard let self = self, let session = self.session else { return }
if !session.isRunning {
session.startRunning()
}
}
}
func takePhoto(completion: @escaping (UIImage?, Error?) -> Void) {
// 所有相机操作都在sessionQueue上执行
sessionQueue.async { [weak self] in
guard let self = self,
let session = self.session,
session.isRunning else {
completion(nil, NSError(domain: "Camera",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "相机未启动"]))
return
}
let photoOutput = session.outputs.first as? AVCapturePhotoOutput
let settings = AVCapturePhotoSettings()
photoOutput?.capturePhoto(with: settings, delegate: PhotoCaptureHandler(completion: completion))
}
}
}
class PhotoCaptureHandler: NSObject, AVCapturePhotoCaptureDelegate {
private let completion: (UIImage?, Error?) -> Void
init(completion: @escaping (UIImage?, Error?) -> Void) {
self.completion = completion
}
func photoOutput(_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?) {
if let error = error {
completion(nil, error)
return
}
guard let imageData = photo.fileDataRepresentation(),
let image = UIImage(data: imageData) else {
completion(nil, nil)
return
}
completion(image, nil)
}
}
设备兼容性问题:不同iPhone的”差异化待遇”
从iPhone X到最新的iPhone 15 Pro Max,不同设备的相机硬件差异很大。如果不妥善处理兼容性问题,某些设备上的特定操作就会导致崩溃。
class DeviceCompatibileCamera {
static func getBackCamera() -> AVCaptureDevice? {
// iPhone X及以后使用Dual Wide Camera
if #available(iOS 12.0, *) {
if let dualWide = AVCaptureDevice.DiscoverySession(
deviceTypes: [.builtInDualWideCamera],
mediaType: .video,
position: .back
).devices.first {
return dualWide
}
}
// 回退到普通后置摄像头
return AVCaptureDevice.DiscoverySession(
deviceTypes: [.builtInWideAngleCamera],
mediaType: .video,
position: .back
).devices.first
}
static func getFrontCamera() -> AVCaptureDevice? {
// 检查是否有TrueDepth相机
if #available(iOS 11.0, *) {
if let trueDepth = AVCaptureDevice.DiscoverySession(
deviceTypes: [.builtInTrueDepthCamera],
mediaType: .video,
position: .front
).devices.first {
return trueDepth
}
}
// 回退到普通前置摄像头
return AVCaptureDevice.DiscoverySession(
deviceTypes: [.builtInWideAngleCamera],
mediaType: .video,
position: .front
).devices.first
}
static func supportsPhotoHDR(_ device: AVCaptureDevice) -> Bool {
var supportsHDR = false
var error: NSError?
device.lockForConfiguration(&error)
if error == nil {
if #available(iOS 10.0, *) {
supportsHDR = device.isPhotoHDRSupported
}
device.unlockForConfiguration()
}
return supportsHDR
}
static func configureDevice(_ device: AVCaptureDevice,
exposureMode: AVCaptureDevice.ExposureMode = .continuousAutoExposure) {
var error: NSError?
device.lockForConfiguration(&error)
if error == nil {
device.exposureMode = exposureMode
device.focusMode = .continuousAutoFocus
// iPhone X及以后支持连续自动对焦
if #available(iOS 11.0, *) {
device.videoZoomFactor = 1.0
}
device.unlockForConfiguration()
} else {
print("配置设备失败: \(error?.localizedDescription ?? "未知错误")")
}
}
}
视频录制崩溃:那些隐藏的”定时炸弹”
视频录制比拍照更复杂,涉及的资源也更多。以下是视频录制中常见的崩溃原因及解决方案:
class VideoRecorder: NSObject {
private var videoOutput: AVCaptureMovieFileOutput?
private var recordingStartTime: CMTime?
private var isRecording = false
private weak var delegate: VideoRecorderDelegate?
func startRecording(to outputURL: URL) {
guard !isRecording else { return }
// 确保在正确的线程上调用
DispatchQueue.main.async { [weak self] in
guard let self = self, let output = self.videoOutput else { return }
// 检查是否有足够的存储空间
guard self.hasEnoughStorage() else {
self.delegate?.recorder(self, didFailWithError:
NSError(domain: "Storage", code: -1,
userInfo: [NSLocalizedDescriptionKey: "存储空间不足"]))
return
}
// 记录开始时间
self.recordingStartTime = output.currentTime
output.startRecording(to: outputURL, recordingDelegate: self)
self.isRecording = true
}
}
func stopRecording() {
guard isRecording else { return }
DispatchQueue.main.async { [weak self] in
self?.videoOutput?.stopRecording()
self?.isRecording = false
}
}
private func hasEnoughStorage() -> Bool {
do {
let attributes = try FileManager.default.attributesOfFileSystem(forPath: NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!)
let freeSpace = attributes[.systemFreeSize] as? UInt64 ?? 0
// 至少需要50MB可用空间
return freeSpace > 50 * 1024 * 1024
} catch {
return false
}
}
}
extension VideoRecorder: AVCaptureFileOutputRecordingDelegate {
func fileOutput(_ output: AVCaptureFileOutput,
didStartRecordingTo fileURL: URL,
from connections: [AVCaptureConnection]) {
delegate?.recorderDidStartRecording(self)
}
func fileOutput(_ output: AVCaptureFileOutput,
didFinishRecordingTo outputFileURL: URL,
from connections: [AVCaptureConnection],
error: Error?) {
isRecording = false
if let error = error {
delegate?.recorder(self, didFailWithError: error)
return
}
delegate?.recorder(self, didFinishRecordingTo: outputFileURL)
}
}
protocol VideoRecorderDelegate: AnyObject {
func recorderDidStartRecording(_ recorder: VideoRecorder)
func recorder(_ recorder: VideoRecorder, didFinishRecordingTo url: URL)
func recorder(_ recorder: VideoRecorder, didFailWithError error: Error)
}
性能优化:让相机App跑得更快更稳
如果你的相机App在录制高分辨率视频时经常卡顿或崩溃,可能需要考虑性能优化:
class OptimizedCameraSession {
private let session = AVCaptureSession()
private var photoOutput: AVCapturePhotoOutput?
private var videoOutput: AVCaptureMovieFileOutput?
func setupForHighPerformance() {
session.beginConfiguration()
// 使用适当的预设
session.sessionPreset = .high
// 配置视频输出
videoOutput = AVCaptureMovieFileOutput()
if session.canAddOutput(videoOutput!) {
session.addOutput(videoOutput!)
// 优化视频编码
if let connection = videoOutput!.connection(with: .video) {
connection.videoMaxDuration = 30 * 60 // 最多30分钟
connection.videoZoomFactor = 1.0
}
}
// 配置照片输出
photoOutput = AVCapturePhotoOutput()
if session.canAddOutput(photoOutput!) {
session.addOutput(photoOutput!)
// 启用RAW格式支持
if #available(iOS 11.0, *) {
photoOutput!.isLivePhotoCaptureEnabled = false
}
}
session.commitConfiguration()
}
func capturePhotoWithSettings(_ settings: AVCapturePhotoSettings) {
// 检查照片格式支持
if photoOutput!.isPhotoFormatSupported(settings.photoFormat) {
// 设置最佳质量
settings.flashMode = .off
settings.isAutoStillImageStabilizationEnabled = true
photoOutput!.capturePhoto(with: settings, delegate: PhotoCaptureDelegate.shared)
}
}
}
class PhotoCaptureDelegate: NSObject, AVCapturePhotoCaptureDelegate {
static let shared = PhotoCaptureDelegate()
func photoOutput(_ output: AVCapturePhotoOutput,
didFinishProcessingPhoto photo: AVCapturePhoto,
error: Error?) {
if let error = error {
print("照片捕获错误: \(error)")
return
}
// 处理照片数据
if let imageData = photo.fileDataRepresentation() {
// 使用 DispatchQueue 异步处理,避免阻塞相机线程
DispatchQueue.global(qos: .userInitiated).async {
let image = UIImage(data: imageData)
// 处理图像...
}
}
}
}
调试技巧:当崩溃信息不够明确时
有时候,崩溃日志可能很模糊。以下是一些实用的调试技巧:
- 使用Activity Monitor查看内存使用:
func checkMemoryUsage() {
let processInfo = ProcessInfo.processInfo
let memoryStatus = processInfo.systemMemoryInfo
print("物理内存总量: \(memoryStatus.physicalMemory / 1024 / 1024) MB")
print("可用内存: \(memoryStatus.freeMemory / 1024 / 1024) MB")
print("已用内存: \(memoryStatus.wiredMemory / 1024 / 1024) MB")
}
启用僵尸对象检测: 在Scheme的Diagnose选项中启用”Zombie Objects”,可以帮助发现访问已释放对象的崩溃。
使用Thread Sanitizer检测线程问题: 在Scheme的Run选项中启用Thread Sanitizer,可以检测数据竞争和线程安全问题。
使用Instruments的Allocations和Leak模板: 定期检查内存分配和泄漏情况。
总结:避免崩溃的几个核心原则
- 始终检查权限:在使用相机前,确保已获得用户授权。
- 在正确的线程上操作:会话配置在后台线程,UI更新在主线程。
- 正确处理内存:使用weak引用避免循环引用,及时释放资源。
- 注意设备兼容性:不同iPhone型号的相机硬件差异很大。
- 错误处理要完善:使用try-catch和guard语句来优雅地处理错误。
- 定期检查资源状态:如存储空间、电池电量等。
记住,相机开发是iOS开发中比较复杂的领域之一。遇到问题时,不要急躁,一步步排查,大部分崩溃都是有规律可循的。希望这篇文章能帮你少走一些弯路!