iOS Crash 治理--[AVCapturePhotoOutput capturePhotoWithSettings:delegate:]

724 阅读3分钟

一. 背景

由于项目中使用了自定义相机功能,该功能也带来了这个崩溃,该崩溃偶现,具体崩溃堆栈如下:

二. 分析和治理

这个问题首先定位到崩溃的函数,简化后的代码如下,该代码的主要功能是检查AVCaptureSessionsession的输出对象数组首先是否包含了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

这个方案上线后,该崩溃减少了很多,但依然会偶现。所以需要进一步排查原因。

因为这个类及功能是另外同事写的,所以在了解了该功能相关的代码逻辑以及进一步深入了解了AVCaptureSessionAVCaptureDevice等类的作用后。我将怀疑定位到了如下这段代码,

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,如果isRuningfalse,重新运行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
}

四. 推荐

这是我的另外两篇崩溃治理,有兴趣的也可以看下:

货拉拉iOS疑难Crash治理-系统键盘语音

货拉拉iOS疑难Crash治理-TTS problem iOS 17