先给结论:iOS 上做人脸采集时界面卡死,绝大多数不是设备性能不够,而是三件事之一——相机权限还没拿到就启动了采集会话、采集回调里把耗时活放到了主线程、或者会话配置不当导致内存持续上涨后被系统终止。

新手遇到相机卡死的第一反应往往是换设备重试,试出来"有的机型卡有的不卡"就更困惑了。其实卡顿集中在三个固定位置:权限检查、回调线程、会话参数。这三处的表现都像"卡死",但定位方式完全不同,按固定顺序排查就能分开。
本文基于 iOS 系统相机框架的通用行为说明,用三段可以放进工程里的最小代码讲清:启动前该检查什么、回调里该怎么分发、以及哪些配置会让内存一路涨上去。每段都给出预期现象和失败分支。
先看结论
- 先查权限:权限没授权就启动会话,界面会停在等待状态,看起来就是相机卡死。
- 再查线程:采集回调不在主线程,把图像处理放到主线程必然掉帧。
- 最后查会话:分辨率和帧率配置过高会让内存持续上涨,最终被系统回收。
相机框架的调用顺序记不清时,翻 iOS 教程 里按功能分组的章节最快。
一、权限没拿到就启动会话
把问题定死:应用一进页面就启动采集,界面停在黑屏,控制台没有报错。
import AVFoundation
func checkCameraPermission() -> Bool {
switch AVCaptureDevice.authorizationStatus(for: .video) {
case .authorized:
return true
case .notDetermined:
AVCaptureDevice.requestAccess(for: .video) { granted in
print("用户选择结果:", granted)
}
return false
default:
print("权限被拒绝,需要引导用户去设置页开启")
return false
}
}
这段做的是在启动任何采集配置之前先查授权状态。三种状态要分开处理:已授权直接继续;未询问要先弹窗,弹窗是异步的,本次调用应当直接返回不启动;已拒绝要引导用户去设置页。
正确顺序是"先检查、再配置、最后启动":
func startCapture() {
guard checkCameraPermission() else {
print("权限未就绪,本次不启动会话")
return
}
let session = AVCaptureSession()
session.beginConfiguration()
session.sessionPreset = .medium
session.commitConfiguration()
session.startRunning()
print("会话已启动")
}
这段做的是用 guard 拦住权限未就绪的情况。适用边界要讲清:请求授权的回调不在主线程,拿到结果后如果要更新界面,必须切回主线程再改。
二、回调不在主线程,重活压在主线程
第二类相机卡死来自线程错配。采集回调默认不在主线程,如果直接在回调里做图像处理或者刷新界面,表现就是掉帧甚至无响应。
class CaptureDelegate: NSObject, AVCaptureVideoDataOutputSampleBufferDelegate {
func captureOutput(_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection) {
DispatchQueue.main.async {
// 在这里做逐帧处理,界面会被压住
self.handle(buffer: sampleBuffer)
}
}
func handle(buffer: CMSampleBuffer) {
print("处理一帧")
}
}
这段做的是把每一帧的处理都丢回主线程。预期现象是界面滚动发涩、按钮点击延迟,帧率明显下降。
正确的做法是把耗时部分留在采集队列里,只把最终结果交回主线程:
func captureOutput(_ output: AVCaptureOutput,
didOutput sampleBuffer: CMSampleBuffer,
from connection: AVCaptureConnection) {
let result = self.handle(buffer: sampleBuffer)
DispatchQueue.main.async {
self.updateUI(with: result)
}
}
这段做的是先在当前队列完成计算,再只把界面更新切回主线程。判断标准是:主线程只做界面更新,任何逐帧计算都不该出现在它上面。

三、会话配置过高导致内存上涨
第三类相机卡死最隐蔽:一切正常,跑几分钟后卡住甚至被系统回收。原因通常是分辨率与帧率配置过高,加上每一帧的缓冲没有被及时释放。
let session = AVCaptureSession()
session.beginConfiguration()
session.sessionPreset = .high
if session.canSetSessionPreset(.high) {
session.sessionPreset = .high
} else {
session.sessionPreset = .medium
}
session.commitConfiguration()
这段做的是在设置前先询问设备是否支持该档位。直接写死高档位在老设备上会失败或退化,判断这一步不能省。
性能取舍要讲清:人脸采集并不需要最高档分辨率,中等档位配合较低的帧率通常就够用,还能显著降低内存压力。需要更高画质时再按机型分级开启。
let output = AVCaptureVideoDataOutput()
output.alwaysDiscardsLateVideoFrames = true
output.videoSettings = [
kCVPixelBufferPixelFormatTypeKey as String: kCVPixelFormatType_32BGRA
]
这段做的是打开丢弃迟到帧的开关,并指定像素格式。这个开关很关键:处理跟不上采集速度时,迟到帧会被丢掉而不是堆积在内存里。关掉它,内存就会随着运行时间一路涨上去。
⚠️ 注意:真机验证之前,模拟器上的相机相关功能不可用,很多"卡死"在模拟器上根本复现不出来。排查必须在真机上进行。
四、常见失败分支与验证清单
4.1 权限弹窗只出现一次
系统授权弹窗每个应用只会主动弹一次,用户拒绝之后再次调用也不会再弹。修复方式是检测到拒绝状态时主动提示用户去设置页手动开启,并在设置页返回后重新检查状态。
4.2 前后台切换后卡住
应用进入后台时会话没有停止,回到前台后状态错乱。修复方式是在进入后台的通知里停止会话,回到前台时重新启动,并处理启动失败的情况。
4.3 界面在真机上卡、模拟器上正常
模拟器的相机链路与真机差异很大,不能用它下结论。修复方式是把全部验证放到真机上,至少覆盖一台较老的机型。
4.4 验证清单
- 检查:授权状态是否为已授权,弹窗回调是否在主线程之外
- 运行:在真机上连续采集三分钟,观察内存曲线
- 预期:内存曲线平稳,帧率维持在设定值附近
- 失败:内存持续上涨或帧率下滑,说明配置过高或缓冲没释放
- 修复:降低会话档位,打开丢弃迟到帧的开关
- 验证:换一台较老机型重复一次,结果一致才算稳定
上面这些步骤做的是按权限、线程、配置三个方向逐一排除。预期结果(未在本机执行)是三处都修正后连续运行不再出现界面无响应;如果仍有卡顿,把内存曲线和帧率数据一起记录下来再定位。

总结
相机卡死集中在三个位置:权限未就绪就启动、回调线程错配、会话配置过高导致内存上涨。按这个顺序排查,比反复换设备试要快得多。
三条要点带走:
- 启动前必查授权状态,未询问时要先弹窗且本次不启动;
- 采集回调里只把界面更新切回主线程,逐帧计算留在采集队列;
- 会话档位按机型分级,并打开丢弃迟到帧的开关。
下一步建议把这三处检查封装成一个启动函数,统一处理失败分支。语法细节可以对照 Swift 教程 里的异步与队列章节再确认一遍。
常见问题
Q:为什么应用一打开相机就卡住不动?
A:先查授权状态。未授权就启动会话会停在等待状态,看起来和卡死一样。把权限检查放在启动之前。
Q:相机卡死只在部分机型出现是什么原因?
A:多数是会话档位过高。老设备不支持高档位,加上缓冲堆积就会卡住。按机型分级设置档位。
Q:模拟器上正常,真机上卡死怎么查?
A:模拟器的相机链路与真机不同,不能用它下结论。排查要全部放到真机上,并至少覆盖一台较老机型。
Q:采集回调里能不能直接更新界面?
A:不能。回调不在主线程,直接更新界面会造成线程错配。应当把计算留在采集队列,只把结果交回主线程。
Q:跑一段时间后才卡住是怎么回事?
A:通常是内存持续上涨。检查是否关闭了丢弃迟到帧的开关,以及每一帧的缓冲是否及时释放。
延伸学习
想把这套排查方法练熟,可以按这个顺序来:
- 想补一遍语言基础,Swift 微课 可以跟一遍;
- 读一篇相关笔记 Debug 是什么意思,把定位思路理清;
- 想随手验证一段代码逻辑,用 在线代码实例 跑一遍最快。

TRAE-AI编程



