固定大小环形数组索引合法仍触发EXC_BAD_ACCESS崩溃排查
环形缓冲区固定数组添加越界防护后仍运行崩溃问题
问题现象
- 基于固定大小数组实现环形缓冲区循环更新逻辑,即使添加了索引越界防护,仍然触发运行时崩溃
- 初始测试数组大小为1000时可稳定复现崩溃,500及以下未触发;后续测试推翻该结论,数组大小为500时也会出现崩溃
- 初步怀疑崩溃由编译器优化导致,寻求对应排查、解决思路
核心业务代码
缓冲区定义实现
struct ZeroCrossing { // 第二个采样点的整数索引(过零点发生在两个采样点之间) var index: UInt // 当前过零点和上一个过零点之间的最高振幅峰值(可正可负) let previousPeak: Float // 两个采样点索引之间的插值过零点位置 let indexWithOffset: Double } class CrossingBuffer { private var array: [ZeroCrossing] private let size: Int private var nextWriteIndex = 0 private var full: Bool { nextWriteIndex >= size } init(size: Int) { self.size = size array = [ZeroCrossing](repeating: ZeroCrossing(index: 0, previousPeak: 0, indexWithOffset: 0), count: size) array.reserveCapacity(size) } public func write(_ val: ZeroCrossing) { array[nextWriteIndex % size] = val nextWriteIndex += 1 } public func getAfterIndex(_ refIndex: Double) -> [ZeroCrossing]? { if !full { return nil } var subArray = [ZeroCrossing]() let lastElementIndex = nextWriteIndex - 1 for i in 0...size - 1 { // 崩溃发生在该行 let thisCrossing = array[(lastElementIndex - i) % size] if thisCrossing.indexWithOffset > refIndex { subArray.append(thisCrossing) } else { break } } return subArray.reversed() } public func reset() { array = [ZeroCrossing](repeating: ZeroCrossing(index: 0, previousPeak: 0, indexWithOffset: 0), count: size) nextWriteIndex = 0 } }
崩溃回溯信息
* thread #1, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x1016dc008) frame #0: 0x000000018bf5c090 libswiftCore.dylib`swift_retain + 60 frame #1: 0x000000018bf9c704 libswiftCore.dylib`swift_bridgeObjectRetain + 56 * frame #2: 0x0000000100838e50 CrossingBuffer.getAfterIndex(refIndex=431975.76999999583, self=0x00000002800a86f0) at CrossingBuffer.swift:97:31 frame #3: 0x00000001007cd704 correlate(self=0x000000010130e5c0) at PitchEngine.swift:146:52 frame #4: 0x00000001007a155c correlate(self=0x0000000283b91200) at TunerEngine.swift:57:39 frame #5: 0x00000001007a1bd8 @objc correlate() at <compiler-generated>:0
调试异常表现
LLDB调试控制台中,使用崩溃行完全相同的索引表达式可正常访问对应元素,调试输出如下:
(lldb) print (lastElementIndex - i) % size (Int) $R4 = 838 (lldb) print array.count (Int) $R5 = 1000 (lldb) print array[(lastElementIndex - i) % size] (ZeroCrossing) $R6 = (index = 438691, previousPeak = 0.0251232013, indexWithOffset = 438690.12000000477)
复现关联代码
测试发现仅当数组大小大于638时会触发崩溃,且需要麦克风拾取到噪声时才会复现:
import Foundation import AVKit final class AudioTap { private var audioEngine = AVAudioEngine() private var windowIndex: UInt = 0 private var lastPeak: Float = 0 private var lastSample: Sample? public var minPeakSize: Float = 0.005 // 调整crossingBuffer的大小会改变崩溃触发行为 private var crossingBuffer = CrossingBuffer(size: 2000) private var lastSeekIndex: Double = 0.0 private var timer: Timer? init() { installTunerTap() audioEngine.prepare() do { try audioEngine.start() } catch let error as NSError { print("AVAudioEngine启动错误: \(error.domain), \(error)") } timer = Timer.scheduledTimer( timeInterval: 0.01, target: self, selector: #selector(getNext), userInfo: nil, repeats: true) } private func installTunerTap() { let inputNode = audioEngine.inputNode inputNode.installTap( onBus: 0, bufferSize: 1000, format: nil, block: { buffer, when in let sampleCount = Int(buffer.frameLength) var sampleIndex = 0 while (sampleIndex < sampleCount) { if let val = buffer.floatChannelData?.pointee[sampleIndex]{ let sample = Sample(index: self.windowIndex, val: val) self.update(sample: sample) } self.windowIndex += 1 sampleIndex += 1 } }) } private func update(sample: Sample) { lastPeak = abs(sample.val) > abs(lastPeak) ? sample.val : lastPeak if let last = lastSample { if last.val * sample.val < 0 && abs(lastPeak) > minPeakSize { // 检测到过零点 let offset = Double(sample.index) + Double(round((sample.val/(last.val - sample.val)) * 100) / 100) let crossing = ZeroCrossing( index: sample.index, previousPeak: lastPeak, indexWithOffset: offset ) crossingBuffer.write(crossing) lastPeak = 0 } } lastSample = sample } @objc func getNext() { if let arr = crossingBuffer.getAfterIndex(lastSeekIndex) { if let s = arr.last { lastSeekIndex = s.indexWithOffset } } } }
问题根因与解决方案
根因
该崩溃和编译器优化无关,是多线程并发读写无保护导致的数据竞争、内存损坏:
write(_:)方法运行在AVAudioEngine的音频实时渲染线程,getAfterIndex(_:)运行在主线程Timer回调中,两个线程并发读写同一个array存储属性和nextWriteIndex变量,没有任何同步保护- Swift的Array虽然是值类型,但作为类的存储属性时会被多个线程共享引用,并发读写会破坏数组内部的内存结构,崩溃点出现在
swift_retain是典型的对象引用计数被并发写坏后的表现 - LLDB中可以正常访问是因为程序暂停在崩溃点时,并发写入已经停止,不存在竞态;数组大小阈值、需要麦克风输入才触发崩溃的特征,完全符合竞态条件的概率性触发规律
- 索引计算逻辑本身不存在越界问题:缓冲区写满后
lastElementIndex >= size - 1,i取值范围为0...size-1,(lastElementIndex - i) % size的计算结果始终在0..<size合法区间内
解决方案
- 快速验证:临时将所有
CrossingBuffer的方法调用都派发到主线程执行,如果崩溃消失即可确认是数据竞争问题 - 常规实现:创建独立的串行DispatchQueue,所有对缓冲区的读写操作都派发到该队列执行,避免并发访问。注意音频实时线程不能使用会阻塞的同步派发,写入操作建议用异步派发避免阻塞音频渲染
- 高性能场景:针对音频实时处理需求,实现基于原子变量的单生产者单消费者无锁环形缓冲区,避免线程同步带来的延迟开销
- 冗余代码清理:初始化方法中
array.reserveCapacity(size)是多余操作,数组以count:size初始化时已经分配了足够容量,不需要重复调用
内容的提问来源于stack exchange,提问作者simonas
相关产品推荐
相关产品推荐

