这个问题在 Xcode 10.2.1 和 iOS 12 中没有出现。它在 Xcode 11.1 和 iOS 13 中开始出现
我的应用程序录制视频,当应用程序进入后台时,我停止运行捕获会话并删除预览层。当应用程序返回前台时,我重新启动捕获会话并将预览图层添加回:
let captureSession = AVCaptureSession()
var previewLayer: AVCaptureVideoPreviewLayer?
var movieFileOutput = AVCaptureMovieFileOutput()
// *** I initially didn't remove the preview layer in this example but I did remove it in the other 2 examples below ***
@objc fileprivate func stopCaptureSession() {
DispatchQueue.main.async {
[weak self] in
if self?.captureSession.isRunning == true {
self?.captureSession.stopRunning()
}
}
}
@objc func restartCaptureSession() {
DispatchQueue.main.async {
[weak self] in
if self?.captureSession.isRunning == false {
self?.captureSession.startRunning()
}
}
}
发生的情况是,当我转到后台并返回预览层时,用户界面完全冻结。但是在进入后台之前,如果我在
if self?.captureSession.isRunning == true
行上放置一个断点,并在 if self?.captureSession.isRunning == false
行上放置另一个断点,一旦触发断点,预览层和 ui 就可以正常工作。
经过进一步研究,我发现了这个问题,并在评论中@HotLicks 说:
Obviously, it's likely that the breakpoint gives time for some async activity to complete before the above code starts mucking with things. However, it's also the case that 0.03 seconds is an awfully short repeat interval for a timer, and it may simply be the case that the breakpoint allows the UI setup to proceed before the timer ties up the CPU.
我做了更多研究,苹果说:
startRunning() 方法是一个阻塞调用,可能需要一些时间, 因此,您应该在串行队列上执行会话设置,以便 主队列不会被阻塞(这会保持 UI 响应)。看 AVCam-iOS:使用 AVFoundation 捕获图像和电影 实现示例。
使用@HotLicks的评论和Apple的信息,我切换到使用
DispatchQueue.main.sync
,然后使用Dispatch Group
,从后台返回后,预览层和ui仍然冻结。但是,一旦我像第一个示例中那样添加断点并触发它们,预览层和 ui 就可以正常工作了。
我做错了什么?
更新
我从调试模式切换到发布模式,仍然不起作用。
我也尝试切换到使用
DispatchQueue.global(qos: .background).async
和计时器DispatchQueue.main.asyncAfter(deadline: .now() + 1.5)
,就像@MohyG建议的那样,但这没有什么区别。
在没有断点的情况下进一步检查,后台通知工作正常,但当应用程序进入 fg 时,前台通知不会被调用。由于某种原因,只有当我第一次在
stopCaptureSession()
函数中放置断点时,才会触发 fg 通知。
问题是前台通知仅在我上面描述的断点处触发。
我尝试了DispatchQueue.main.sync:
@objc fileprivate func stopCaptureSession() {
if captureSession.isRunning { // adding a breakpoint here is the only thing that triggers the foreground notification when the the app comes back
DispatchQueue.global(qos: .default).async {
[weak self] in
DispatchQueue.main.sync {
self?.captureSession.stopRunning()
}
DispatchQueue.main.async {
self?.previewLayer?.removeFromSuperlayer()
self?.previewLayer = nil
}
}
}
}
@objc func restartCaptureSession() {
if !captureSession.isRunning {
DispatchQueue.global(qos: .default).async {
[weak self] in
DispatchQueue.main.sync {
self?.captureSession.startRunning()
}
DispatchQueue.main.asyncAfter(deadline: .now() + 15) {
self?.previewLayer = AVCaptureVideoPreviewLayer(session: self!.captureSession)
self?.previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
guard let previewLayer = self?.previewLayer else { return }
previewLayer.frame = self!.containerViewForPreviewLayer.bounds
self?.containerViewForPreviewLayer.layer.insertSublayer(previewLayer, at: 0)
}
}
}
}
我尝试了调度组:
@objc fileprivate func stopCaptureSession() {
let group = DispatchGroup()
if captureSession.isRunning { // adding a breakpoint here is the only thing that triggers the foreground notification when the the app comes back
group.enter()
DispatchQueue.global(qos: .default).async {
[weak self] in
self?.captureSession.stopRunning()
group.leave()
group.notify(queue: .main) {
self?.previewLayer?.removeFromSuperlayer()
self?.previewLayer = nil
}
}
}
}
@objc func restartCaptureSession() {
let group = DispatchGroup()
if !captureSession.isRunning {
group.enter()
DispatchQueue.global(qos: .default).async {
[weak self] in
self?.captureSession.startRunning()
group.leave()
group.notify(queue: .main) {
self?.previewLayer = AVCaptureVideoPreviewLayer(session: self!.captureSession)
self?.previewLayer?.videoGravity = AVLayerVideoGravity.resizeAspectFill
guard let previewLayer = self?.previewLayer else { return }
previewLayer.frame = self!.containerViewForPreviewLayer.bounds
self?.containerViewForPreviewLayer.layer.insertSublayer(previewLayer, at: 0)
}
}
}
}
如果需要,这里是其余代码:
NotificationCenter.default.addObserver(self, selector: #selector(appHasEnteredBackground),
name: UIApplication.willResignActiveNotification,
object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground),
name: UIApplication.willEnterForegroundNotification,
object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(sessionWasInterrupted),
name: .AVCaptureSessionWasInterrupted,
object: captureSession)
NotificationCenter.default.addObserver(self, selector: #selector(sessionInterruptionEnded),
name: .AVCaptureSessionInterruptionEnded,
object: captureSession)
NotificationCenter.default.addObserver(self, selector: #selector(sessionRuntimeError),
name: .AVCaptureSessionRuntimeError,
object: captureSession)
func stopMovieShowControls() {
if movieFileOutput.isRecording {
movieFileOutput.stopRecording()
}
recordButton.isHidden = false
saveButton.isHidden = false
}
@objc fileprivate func appWillEnterForeground() {
restartCaptureSession()
}
@objc fileprivate func appHasEnteredBackground() {
stopMovieShowControls()
imagePicker.dismiss(animated: false, completion: nil)
stopCaptureSession()
}
@objc func sessionRuntimeError(notification: NSNotification) {
guard let error = notification.userInfo?[AVCaptureSessionErrorKey] as? AVError else { return }
stopMovieRecordigShowControls()
if error.code == .mediaServicesWereReset {
if !captureSession.isRunning {
DispatchQueue.main.async { [weak self] in
self?.captureSession.startRunning()
}
} else {
restartCaptureSession()
}
} else {
restartCaptureSession()
}
}
@objc func sessionWasInterrupted(notification: NSNotification) {
if let userInfoValue = notification.userInfo?[AVCaptureSessionInterruptionReasonKey] as AnyObject?,
let reasonIntegerValue = userInfoValue.integerValue,
let reason = AVCaptureSession.InterruptionReason(rawValue: reasonIntegerValue) {
switch reason {
case .videoDeviceNotAvailableInBackground:
stopMovieShowControls()
case .audioDeviceInUseByAnotherClient, .videoDeviceInUseByAnotherClient:
stopMovieShowControls()
case .videoDeviceNotAvailableWithMultipleForegroundApps:
print("2. The toggleButton was pressed")
case .videoDeviceNotAvailableDueToSystemPressure:
// no documentation
break
@unknown default:
break
}
}
}
@objc func sessionInterruptionEnded(notification: NSNotification) {
restartCaptureSession()
stopMovieShowControls()
}
你尝试过吗
DispatchQueue.global(qos: .background).async
?
基本上从我得到的情况来看,您需要在 self?.captureSession.startRunning()
和 self?.captureSession.stopRunning()
之前造成延迟。
解决您的问题的一个快速解决方案是使用手动延迟,如下所示:
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
但不建议
您可以尝试一下,看看它是否可以解决您的问题,如果是这样,您需要在
AppDelegate
中处理应用程序转换状态
基本上,当您转换到后台和前台时,您需要以某种方式管理触发
captureSession
的开始/停止 AppDelegate
:
func applicationDidEnterBackground(_ application: UIApplication) {}
和
func applicationDidBecomeActive(_ application: UIApplication) {}
我发现了这个错误,这是一个非常奇怪的错误。
按钮图像的色调为白色。我想要一个模糊的背景,而不是使用常规的黑色背景,所以我使用了这个:
func addBackgroundFrostToButton(_ backgroundBlur: UIVisualEffectView, vibrancy: UIVisualEffectView, button: UIButton, width: CGFloat?, height: CGFloat?){
backgroundBlur.frame = button.bounds
vibrancy.frame = button.bounds
backgroundBlur.contentView.addSubview(vibrancy)
button.insertSubview(backgroundBlur, at: 0)
if let width = width {
backgroundBlur.frame.size.width += width
}
if let height = height {
backgroundBlur.frame.size.height += height
}
backgroundBlur.center = CGPoint(x: button.bounds.midX, y: button.bounds.midY)
}
我打电话给
viewDidLayoutSubview()
,比如:
lazy var cancelButto: UIButton = {
let button = UIButton(type: .system)
//....
return button
}()
let cancelButtoBackgroundBlur: UIVisualEffectView = {
let blur = UIVisualEffectView(effect: UIBlurEffect(style: .dark))
//...
return blur
}()
let cancelButtoVibrancy: UIVisualEffectView = {
let vibrancyEffect = UIVibrancyEffect(blurEffect: UIBlurEffect(style: .extraLight))
// ...
return vibrancyView
}()
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
// I did this with 4 buttons, this is just one of them
addBackgroundFrostToButton(cancelButtoBackgroundBlur,
vibrancy: cancelButtoVibrancy,
button: cancelButto,
width: 10, height: 2.5)
}
一旦我注释掉上面的代码,
foreground notification
就开始毫无问题地触发,我不再需要断点了。
由于
viewDidLayoutSubviews()
可以被多次调用,所以 UIVisualEffectView
和 UIVibrancyEffect
不断地相互复合,并且由于某些 非常奇怪的原因,它影响了 foreground notification
。
为了解决这个问题,我简单地创建了一个
Bool
来检查是否将模糊添加到按钮。一旦我这样做了,我就不再有任何问题了。
var wasBlurAdded = false
override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews()
if !wasBlurAdded {
addBackgroundFrostToButton(cancelButtoBackgroundBlur,
vibrancy: cancelButtoVibrancy,
button: cancelButto,
width: 10, height: 2.5)
wasBlurAdded = true
}
}
我不知道为什么或如何影响
foreground notification observer
,但就像我说的,这是一个非常奇怪的错误。
我建议创建您自己的
DispatchQueue
并将其设为串行队列(默认)。这样您就可以确保没有任何并发访问捕获会话。在队列中执行所有会话访问;您可以在队列上使用 async
块,这样您调用它的线程就不会阻塞。
DispatchQueue.global(qos: .background).async {
self.avCaptureSession.startRunning()
}