背景
之前曾经遇到过一个AVAudioPlayer
的多线程crash,这里记录一下。
crash堆栈如下所示1
2
3
4
5
6
7
8
9
10Crashed: com.apple.main-thread
0 libobjc.A.dylib 0x1834fc910 objc_msgSend + 16
1 AVFAudio 0x189c814f4 -[AVAudioPlayer(AVAudioPlayerPriv) finishedPlaying:] + 92
2 Foundation 0x184d7a0ec __NSThreadPerformPerform + 340
3 CoreFoundation 0x1842d7404 CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION + 24
4 CoreFoundation 0x1842d6c2c __CFRunLoopDoSources0 + 276
5 CoreFoundation 0x1842d479c __CFRunLoopRun + 1204
6 CoreFoundation 0x1841f4da8 CFRunLoopRunSpecific + 552
7 GraphicsServices 0x1861d9020 GSEventRunModal + 100
8 UIKit 0x18e211758 UIApplicationMain + 236
通过查询,发现也有其他同学遇到了这个问题:
- Crash : AVAudioPlayer(AVAudioPlayerPriv) finishedPlaying
- Crash with AVAudioPlayer
- Some iOS audio crash
也尝试了一些网上的解决方案,发现并不奏效:
分析
观察堆栈,应该是在finishedPlaying:
方法中访问了非法地址,而_objc_msgSend
则大概率预示着这是一个多线程问题。AVAudioPlayer
播放声音是异步的,另起一个播放线程,并不在主线程。当声音播放完全之后,再切换到主线程调用AVAudioPlayer的finishedPlaying:
方法,从而调用delegate
的方法。而当从播放线程切换到主线程的过程当中,AVAudioPlayer
会被引用,并不会被释放。这样当AVAudioPlayer
的代理被释放后,AVAudioPlayer
还是有可能调用了 delegate的方法。
通过google,也可以发现类似的解释1
2
3
4The cause of this problem was that the AVAudioPlayer object was trying
to invoke [avAudioPlayer.delegate audioPlayerDidFinishPlaying] but the
avAudioPlayer.delegate ref was no longer valid because it had been
deallocated when releasing the avAudioPlayer in my code
通过查看AVAudioPlayer.h
头文件,发现其代理是weak的,同时该crash几乎通吃所有的系统版本和机型,这让人十分费解1
2
3
4
5
6
7
8
9
10// AVAudioPlayer.h
@interface AVAudioPlayer : NSObject {
@private
id _impl;
}
// the delegate will be sent messages from the AVAudioPlayerDelegate protocol
@property(weak, nullable) id<AVAudioPlayerDelegate> delegate;
@end
调试
由于finishedPlaying:
并没有暴露为共有方法,为了能看下堆栈,所以hook了finishedPlaying:
方法
通过调试,发现原AVAudioPlayer有一个内部实现类AudioPlayerImpl
,这个内部实现类还有一个成员变量_delegate
进而详细查看了AudioPlayerImpl
的头文件:AudioPlayerImpl的头文件
通过验证,发现这两个代理都指向同一个对象:1
2
3
4
5 (lldb) po self.delegate
<ThreadViewController: 0x7ff3334025e0>
Printing description of self->_impl->_delegate:
<ThreadViewController: 0x7ff3334025e0>
我们不由地怀疑这个内部类AudioPlayerImpl
的成员变量_delegate
有问题,初步推断crash是这样产生的:
假设AVAudioPlayer
的delegate
为A,内部类AudioPlayerImpl
的成员变量_delegate
为B
正常设计上,外部代理释放的时候,weak的A会自动变为nil,同时也应该将B设置为nil,并且要保证原子性。但是可能有些情况下A和B的变化并没有保持绝对同步,导致A变为nil的时候,B仍然指向一个非法的地址,如果此时访问B,便会crash。
解决办法
朝着这个怀疑点,尝试修复一下,原理很简单:
在执行方法finishedPlaying
之前,先判断AVAudioPlayer
的代理delegate
是否为nil
- 如果是nil,将
AudioPlayerImpl
的成员变量_delegate
也设置为nil - 否则,强持有
AVAudioPlayer
的代理delegate
,保证finishedPlaying
方法执行期间不被释放;
代码如下:
1 | @implementation AVAudioPlayer (Extension) |
验证
通过上线验证后,该修复方法的确生效了