一. 背景
由于项目中使用了自定义相机功能,该功能也带来了这个崩溃,该崩溃偶现,具体崩溃堆栈如下:
二. 分析和治理
这个问题首先定位到崩溃的函数,简化后的代码如下,该代码的主要功能是检查AVCaptureSession的session的输出对象数组首先是否包含了imageOutput,如果包含了,就直接设置输出图片格式和代理对象,如果没有检测当前session是否能加这个imageOutput,如果可以直接加,并设置输出图片格式和代理对象,不行就错误提示。
从崩溃堆栈看崩溃的代码在给self.imageOutPut.capturePhoto(with: outputSettings, delegate: self),图片输出类设置图片样式和代理方法这里,
然后我们再结合崩溃的原因:
reason: '*** -[AVCapturePhotoOutput capturePhotoWithSettings:delegate:] No active and enabled video connection
根据这个崩溃理由分析,出现崩溃原因有:
AVCaptureSession未正确配置或者未正在运行session.addOutput(output)没有正确添加
因为代码里面添加了是否添加output的判断,因此这里最大的可能是AVCaptureSession此时未正常启动。
基于这点考虑我在函数前面首先检测了下session.isRunning是否为true,如果session正在运行,就走下面的逻辑,如果没有运行,就去startRunningSession。
这个方案上线后,该崩溃减少了很多,但依然会偶现。所以需要进一步排查原因。
因为这个类及功能是另外同事写的,所以在了解了该功能相关的代码逻辑以及进一步深入了解了AVCaptureSession、AVCaptureDevice等类的作用后。我将怀疑定位到了如下这段代码,
if self.session.canSetSessionPreset(AVCaptureSession.Preset.hd4K3840x2160) && isiPhoneX {// iphonex之后机型才使用4K
self.session.sessionPreset = AVCaptureSession.Preset.hd4K3840x2160
} else if self.session.canSetSessionPreset(AVCaptureSession.Preset.hd1920x1080) {
self.session.sessionPreset = AVCaptureSession.Preset.hd1920x1080
} else if self.session.canSetSessionPreset(AVCaptureSession.Preset.hd1280x720) {
self.session.sessionPreset = AVCaptureSession.Preset.hd1280x720
}
这段代码的主要逻辑是会话session判断是否支持某个级别输出格式预设,如果可以则设置为该格式。
AVCaptureSession 有一个默认的 sessionPreset。如果你没有设置 sessionPreset,会使用默认值 AVCaptureSessionPresetHigh
这里只判断了AVCaptureSession是否支持这个预设,但并没有判断AVCaptureDevice是否也一起支持,因此这里可能会出现两种一个支持,一个不支持的情况,所以最好两者兼容,因此修改后的代码如下。
if self.session.canSetSessionPreset(AVCaptureSession.Preset.hd4K3840x2160) && self.deviceIsSupportSessionPreset(AVCaptureSession.Preset.hd4K3840x2160) && isiPhoneX {// iphonex之后机型才使用4K
self.session.sessionPreset = AVCaptureSession.Preset.hd4K3840x2160
} else if self.session.canSetSessionPreset(AVCaptureSession.Preset.hd1920x1080),
self.deviceIsSupportSessionPreset(AVCaptureSession.Preset.hd1920x1080) {
self.session.sessionPreset = AVCaptureSession.Preset.hd1920x1080
} else if self.session.canSetSessionPreset(AVCaptureSession.Preset.hd1280x720),
self.deviceIsSupportSessionPreset(AVCaptureSession.Preset.hd1280x720) {
self.session.sessionPreset = AVCaptureSession.Preset.hd1280x720
} else if self.session.canSetSessionPreset(AVCaptureSession.Preset.high),
self.deviceIsSupportSessionPreset(AVCaptureSession.Preset.high) {
self.session.sessionPreset = AVCaptureSession.Preset.high
} else if self.session.canSetSessionPreset(AVCaptureSession.Preset.medium),
self.deviceIsSupportSessionPreset(AVCaptureSession.Preset.medium) {
self.session.sessionPreset = AVCaptureSession.Preset.medium
} else if self.session.canSetSessionPreset(AVCaptureSession.Preset.low),
self.deviceIsSupportSessionPreset(AVCaptureSession.Preset.low) {
self.session.sessionPreset = AVCaptureSession.Preset.medium
} else {
print("当前设备相机没有合适的支持图片格式")
return
}
/// 设备 是否 支持 当前present
private func deviceIsSupportSessionPreset(_ preset: AVCaptureSession.Preset) -> Bool {
if let tmpDevice = self.device,
tmpDevice.supportsSessionPreset(preset) {
return true
}
return false
}
然后加上相关的降级代码和日志和错误上报的埋点代码。
经上线验证后,发现这个问题彻底解决。
三. 总结
从上面分析和尝试治理,我们可以看出这个问题解决方案有两点:
- 当自定义相机开始拍照的时候,先判断
device是否连接,如果未连接,则直接返回;接着判断session是否isRuning,如果isRuning为false,重新运行session,优化后的内容大致如下。
// 拍照
@objc
func startTakePhoto() {
/// 判断 设备 是否 连接,未连接,则直接返回
guard self.device?.isConnected == true else {
return
}
/// 判断 session是否running,未运行则先去运行
guard self.session.isRunning == true else {
self.startRunning()
return
}
let outputSettings = AVCapturePhotoSettings.init(format: [AVVideoCodecKey: AVVideoCodecType.jpeg])
if self.session.outputs.contains(self.ImageOutPut) {
self.ImageOutPut.capturePhoto(with: outputSettings, delegate: self)
} else {
if self.session.canAddOutput(self.ImageOutPut) {
self.session.addOutput(self.ImageOutPut)
self.ImageOutPut.capturePhoto(with: outputSettings, delegate: self)
} else {
print("当前设备相机不可用")
return
}
}
}
- 判断
session.sessionPreset的设置格式,是否device也支持,保证两者同时支持。
if self.session.canSetSessionPreset(AVCaptureSession.Preset.hd4K3840x2160) && self.deviceIsSupportSessionPreset(AVCaptureSession.Preset.hd4K3840x2160) && isiPhoneX {// iphonex之后机型才使用4K
self.session.sessionPreset = AVCaptureSession.Preset.hd4K3840x2160
} else if self.session.canSetSessionPreset(AVCaptureSession.Preset.hd1920x1080),
self.deviceIsSupportSessionPreset(AVCaptureSession.Preset.hd1920x1080) {
self.session.sessionPreset = AVCaptureSession.Preset.hd1920x1080
} else if self.session.canSetSessionPreset(AVCaptureSession.Preset.hd1280x720),
self.deviceIsSupportSessionPreset(AVCaptureSession.Preset.hd1280x720) {
self.session.sessionPreset = AVCaptureSession.Preset.hd1280x720
} else if self.session.canSetSessionPreset(AVCaptureSession.Preset.high),
self.deviceIsSupportSessionPreset(AVCaptureSession.Preset.high) {
self.session.sessionPreset = AVCaptureSession.Preset.high
} else if self.session.canSetSessionPreset(AVCaptureSession.Preset.medium),
self.deviceIsSupportSessionPreset(AVCaptureSession.Preset.medium) {
self.session.sessionPreset = AVCaptureSession.Preset.medium
} else if self.session.canSetSessionPreset(AVCaptureSession.Preset.low),
self.deviceIsSupportSessionPreset(AVCaptureSession.Preset.low) {
self.session.sessionPreset = AVCaptureSession.Preset.medium
} else {
print("当前设备相机没有合适的支持图片格式")
return
}
/// 设备 是否 支持 当前present
private func deviceIsSupportSessionPreset(_ preset: AVCaptureSession.Preset) -> Bool {
if let tmpDevice = self.device,
tmpDevice.supportsSessionPreset(preset) {
return true
}
return false
}
四. 推荐
这是我的另外两篇崩溃治理,有兴趣的也可以看下: