Skip to content

Koe 仍会卡死在开始录音(#77 后续):inputNode / installTapOnBus / prepare 在主线程被 CoreAudio HAL 阻塞 #92

@skhe

Description

@skhe

问题描述

#77 修好之后,我在同一台机器上又复现了「Koe 卡死 / 进程存活 / CPU 持续占用」的问题,但这次卡死的位置和 #77 不太一样:之前是卡在 audioEngine.start()#77 的 PR 把这一步包进了后台队列 + 3s 超时;这次卡在更早AVAudioEngine.inputNode / outputFormatForBus: / installTapOnBus: / prepare 一段,仍然运行在主线程上。

结果和 #77 完全一样:Koe 进程不崩,但一直占 CPU,菜单栏点不开,热键触发没有反应,只能强杀重启。

环境

  • Koe 版本:1.0.14 (15)/Applications/Koe.app/Contents/MacOS/Koe
  • Bundle ID:nz.owo.koe
  • macOS:26.3 (25D125)
  • 芯片:Apple Silicon(arm64)
  • 形态:菜单栏应用(LSUIElement = true

进程采样

sample <pid> 3 抓到主线程 2243/2243 个采样全部卡在同一条调用链,完整路径如下(摘关键几层):

main
 └─ -[NSApplication run]
    └─ _DPSNextEvent
       └─ __CFRunLoopDoTimer
          └─ __NSFireTimer
             └─ __33-[SPHotkeyMonitor startHoldTimer]_block_invoke
                └─ -[SPHotkeyMonitor holdTimerFired]
                   └─ -[SPAppDelegate hotkeyMonitorDidDetectHoldStart]
                      └─ -[SPAudioCaptureManager startCaptureWithAudioCallback:] + 180
                         └─ -[AVAudioEngine inputNode]
                            └─ AVAudioEngineImpl::UpdateInputNode(bool)
                               └─ -[AVAudioNode inputFormatForBus:]
                                  └─ AVAudioIONodeImpl::GetInputFormat
                                     └─ AVAudioIOUnit::GetHWFormat
                                        └─ _dispatch_lane_barrier_sync_invoke_and_complete
                                           └─ AVAudioIOUnit_OSX::_GetHWFormat
                                              └─ AVAEHalUtil::GetSubDevices
                                                 └─ AudioObjectGetPropertyData_mac_imp
                                                    └─ HAL_HardwarePlugIn_ObjectGetPropertyData
                                                       └─ HALC_Object_GetPropertyData_DAI32
                                                          └─ mach_msg → mach_msg2_trap   (100% 样本)

进程运行时间约 12.5 h,ps 显示累计 CPU 时间 225:08,状态 R,RSS ~37MB。也就是说主线程从某个时刻开始就一直卡在 AU 内部 GetHWFormatcoreaudiod 的 mach IPC 上。

根因分析

-[SPAudioCaptureManager startCaptureWithAudioCallback:] 当前实现里这几行都在主线程上跑:

self.audioEngine = [[AVAudioEngine alloc] init];
AVAudioInputNode *inputNode = self.audioEngine.inputNode;   // ← 主线程在这里被卡死

if (self.pendingDeviceID != kAudioObjectUnknown) {
    AudioUnitSetProperty(inputNode.audioUnit, kAudioOutputUnitProperty_CurrentDevice, ...);
}

AVAudioFormat *hardwareFormat = [inputNode outputFormatForBus:0];
...
[inputNode installTapOnBus:0 bufferSize:4096 format:hardwareFormat block:^...];
[self.audioEngine prepare];

上面这几个调用看起来像是单纯读写 Obj-C 属性,但它们内部全部都会 dispatch_syncAVAudioIOUnit 的串行队列,再通过 mach_msg 同步发请求给 coreaudiod。蓝牙耳机路由切换、聚合设备/空间音频子设备枚举、Audio device list changed 等瞬态场景下,这个 reply 会无限延迟(#77HALC_ProxyIOContext::StartAndWaitForState returned error 35 是同一个现象的另一个表现),于是整个主线程被钉死。

#77 的那版修复只把最后一步 [engine startAndReturnError:] 丢到后台 + 3s 超时,前面的 inputNode / installTapOnBus: / prepare 还是在主线程,所以遇到 HAL 同样的毛病时,代码永远走不到已经有超时保护的那一段。

关键证据(和 #77 对比)

#77 卡死位置 本次卡死位置
主线程 frame -[AVAudioEngine startAndReturnError:] -[AVAudioEngine inputNode]
CoreAudio 层 HALC_ProxyIOContext::_StartIO / StartAndWaitForState AVAudioIOUnit::GetHWFormatAVAEHalUtil::GetSubDevices
现有保护是否生效 是(3s 超时) 否(还没走到 start)
用户侧表现 进程存活 / CPU 30%+ / 无法恢复 进程存活 / CPU 30%+ / 无法恢复

修复方案

最小侵入的改法:把 startCaptureWithAudioCallback:整段接触 CoreAudio 的逻辑(engine 构造、inputNode 查询、AudioUnitSetProperty 切换输入设备、outputFormatForBus:installTapOnBus:preparestartAndReturnError:)都挪进现有的后台 dispatch_async,并复用 kEngineStartTimeoutSec(3s)的那把信号量闸门。主线程等待超时即返回 NO,由 SPAppDelegate.startAudioCaptureWithRetry 已有的 500ms 重试兜底。

额外处理一下「主线程超时但后台后来又成功启动了 engine」的情况:在共享锁里写一个 mainAborted 标志,后台发现 mainAborted == YES 就对迟到启动的 engine 调 stop,避免留下孤儿录音会话。

这样无论 HAL 卡在哪一步,主线程最多被阻塞 3 秒,然后走统一的错误/重试路径,不再出现「进程活着但整 app 假死」。

PR 已经发在 #93,只改 KoeApp/Koe/Audio/SPAudioCaptureManager.m,没有接口级别变动。

其他

如果需要,我可以再补一份完整的 sample 输出(~1800 行)以及复现时的 log show --predicate 'subsystem == "com.apple.coreaudio"' 片段。

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions