You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

iOS音频录制应用中无对应结束通知的音频中断场景及健壮处理方案咨询

iOS音频录制应用中无对应结束通知的音频中断场景及健壮处理方案咨询

哥们儿,这个坑我做音频录制App的时候踩过好几次,太懂那种等着结束通知却永远等不到的憋屈了!结合你用AVAudioRecorder做纯录制的场景,我给你捋清楚哪些情况会出现这种「单边中断」,再给你一套亲测有效的健壮处理方案。

一、只会触发.began中断通知、无对应.ended的常见场景

  • 其他App主动抢占音频会话:就是你遇到的情况——你的App正在录制,用户切到Safari、Spotify这类App主动播放/录制音频,抢占了音频会话资源。系统只会给你的App发.began中断通知,因为其他App是主动持有会话权限的,除非那个App主动释放资源,否则系统不会替它发结束通知给你。
  • 系统级音频资源被高优先级App剥夺:比如你的App在后台录制时,用户打开了语音备忘录、通话类这类需要独占音频会话的高优先级App,你的会话会被强制挂起,但不会收到结束通知——直到高优先级App主动释放资源。
  • 后台状态下系统回收音频资源:你的App在后台挂着录制时,系统因为低内存、电源管理等原因,回收了你的音频会话相关资源,但App进程还存活,这时候也不会收到中断结束通知。
  • 音频路由变更伴随会话抢占:比如你的App正在录制,用户切换到蓝牙耳机/ AirPlay设备,同时有其他App在该设备上播放音频,你的会话被抢占后也不会收到结束通知。

二、针对AVAudioRecorder的健壮处理方案

核心思路就是:绝对不要只依赖中断结束通知,要建立多维度的状态监听+主动检查机制,下面是具体落地的步骤:

1. 放弃单一依赖,多维度监听关键事件

不要把宝全压在AVAudioSessionInterruptionNotification上,同时监听这几个关键事件:

  • App前后台切换事件:UIApplication.didBecomeActiveNotification(回到前台激活时检查)、UIApplication.willEnterForegroundNotification(进入前台前预检查)
  • 其他App音频播放状态:用KVO监听AVAudioSession.sharedInstance().isOtherAudioPlaying属性
  • 音频路由变化:AVAudioSession.routeChangeNotification(路由变化时检查会话状态)

2. 中断回调只做“暂停+状态记录”,不等待结束通知

收到.began中断时,立刻暂停录制、保存临时数据,同时记录用户之前的录制状态(比如用一个布尔变量wasRecordingBeforeInterruption标记),不要傻等.ended

@objc func handleInterruption(_ notification: Notification) {
    guard let userInfo = notification.userInfo,
          let typeValue = userInfo[AVAudioSessionInterruptionTypeKey] as? UInt,
          let type = AVAudioSession.InterruptionType(rawValue: typeValue) else {
        return
    }
    
    if type == .began {
        // 记录之前的录制状态
        wasRecordingBeforeInterruption = recorder?.isRecording ?? false
        // 立刻暂停录制,保存临时文件
        if recorder?.isRecording == true {
            recorder?.pause()
            saveTemporaryRecordingFragment() // 自定义的保存临时片段方法
        }
        // 更新UI提示用户
        updateRecordingUI(isRecording: false, isPaused: true)
    }
    // 这里完全不处理.ended事件,把恢复逻辑交给其他回调
}

3. 利用App前后台切换,主动检查会话状态

当App从后台回到前台时,必须主动检查音频会话的可用性,而不是等通知:

@objc func appDidBecomeActive(_ notification: Notification) {
    let session = AVAudioSession.sharedInstance()
    guard session.responds(to: #selector(getter: AVAudioSession.isOtherAudioPlaying)) else { return }
    
    if session.isOtherAudioPlaying {
        // 还有其他App占着音频资源,提示用户
        showAlert(title: "录制提示", message: "当前有其他应用在使用音频,暂时无法恢复录制")
        updateRecordingUI(isRecording: false, isPaused: true)
    } else if wasRecordingBeforeInterruption {
        // 用户之前在录制,弹出询问框确认是否恢复
        showResumeConfirmationAlert()
    }
}

4. 实时监听其他App的音频状态变化

用KVO监听isOtherAudioPlaying属性,能实时捕捉到其他App抢占/释放音频资源的场景(比如你遇到的切到Safari播音频的情况):

// 在初始化时注册KVO
override func viewDidLoad() {
    super.viewDidLoad()
    do {
        try AVAudioSession.sharedInstance().addObserver(
            self,
            forKeyPath: #keyPath(AVAudioSession.isOtherAudioPlaying),
            options: [.new, .old],
            context: nil
        )
    } catch {
        print("注册音频状态监听失败:\(error.localizedDescription)")
    }
}

// KVO回调处理
override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {
    guard keyPath == #keyPath(AVAudioSession.isOtherAudioPlaying) else {
        super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
        return
    }
    
    let session = AVAudioSession.sharedInstance()
    if session.isOtherAudioPlaying {
        // 其他App开始播放,立刻暂停录制
        if recorder?.isRecording == true {
            wasRecordingBeforeInterruption = true
            recorder?.pause()
            saveTemporaryRecordingFragment()
            updateRecordingUI(isRecording: false, isPaused: true)
            showAlert(title: "录制暂停", message: "其他应用正在播放音频,录制已暂停")
        }
    } else if wasRecordingBeforeInterruption {
        // 其他App释放了资源,询问是否恢复录制
        showResumeConfirmationAlert()
    }
}

// 记得在deinit里移除KVO观察者
deinit {
    AVAudioSession.sharedInstance().removeObserver(
        self,
        forKeyPath: #keyPath(AVAudioSession.isOtherAudioPlaying)
    )
    NotificationCenter.default.removeObserver(self)
}

5. 恢复录制前主动激活音频会话

不管是用户确认恢复,还是自动尝试恢复,都要主动激活会话,不要假设会话已经可用:

private func resumeRecordingIfPossible() {
    guard wasRecordingBeforeInterruption else { return }
    
    let session = AVAudioSession.sharedInstance()
    do {
        // 尝试激活音频会话
        try session.setActive(true, options: .notifyOthersOnDeactivation)
        // 激活成功,恢复录制
        recorder?.record()
        wasRecordingBeforeInterruption = false
        updateRecordingUI(isRecording: true, isPaused: false)
    } catch {
        // 激活失败,提示用户
        showAlert(title: "恢复失败", message: "无法重新获取音频权限,请关闭其他音频应用后重试")
        wasRecordingBeforeInterruption = false
    }
}

三、额外注意事项

  • 维护清晰的状态机:给你的App定义明确的音频状态(未录制、录制中、暂停中、中断中),每个状态下的处理逻辑要清晰,避免状态混乱。
  • 避免重复激活会话:可以加一个布尔变量isActivatingSession做锁,防止多个回调同时触发激活请求,导致冲突。
  • 用户体验优先:不要自动恢复录制,一定要询问用户——毕竟用户切换到其他App可能已经不想继续录制了。
  • 适配iOS版本isOtherAudioPlaying在iOS 8及以上可用,如果你需要适配更低版本,可以用AVAudioSessionRouteDescriptionoutputs属性判断是否有其他音频输出。

这样处理之后,你遇到的切到Safari播音频的场景会被实时捕捉到,其他单边中断的情况也能被覆盖到,你的录制App的音频处理逻辑会健壮很多!

火山引擎 最新活动