先上栈,这个 crash 是我们目前开发产品的 top5 crash
对于死在 ojbc _ msgSend 的函数(不仅仅是 msgSend, objc_retain 等一切没有创建栈帧的都需要注意),请先检查 crash 上报的寄存器信息
一般来说,lr 肯定不等于第一个栈。 目前的 crash上报功能,丢失了最顶层的栈。因为 objc_msgSend 并没有创建栈帧。
这样,我们就得根据 lr,来计算真实的最后一个栈了。
栈帧介绍
栈帧,保存当前函数返回地址,以及上级栈帧地址。 这样,通过枚举栈帧,即可得到函数的调用栈。
在模块列表查找,lr 是那个模块的
找到了,计算绝对偏移,找出对应函数地址。(函数绝对偏移 = lr - 模块基址)同样,反过来就可以在本机或者 IDA 查找函数了)
libAVFAudio.dylib`AVAudioSessionPropertyListener(void, unsigned int, unsigned int, void const) + 1796
好了,终于找到调用 objc_msgSend 的点了。
运行程序,找到野掉的对象到底是个什么
AVAudioSessionRouteDescription (这里需要根据 selector 再次核实上一步骤的调用点是否正确。crash 时 selector 存放在 x1 寄存器, 有时候上报平台会打印出 x-selector detect, 对比下 selector 是否一致,一致则说明上一步得到的地址没有问题)
这个对象是哪里来的
需要调试神器 lzmalloc 命令(话说,这个命令实在是太好用了。) lzmalloc 为我们自己开发的调试器辅助命令,用于打印对象分配以及释放点的堆栈信息。 下面为 lzmalloc 结果
到这里为止,首先排查了自己代码内部对于 AVAudioSessionRouteDescription 确定不存在过度释放的问题,不得已,只有逆向了。(最蛋疼的步骤了)
首先确定野掉的 AVAudioSessionRouteDescription 来源于 libAVFAudio.dylib`AVAudioSessionPropertyListener(void, unsigned int, unsigned int, void const) + 1768
而此行是调用函数 -[AVAudioSession privateConfigureRouteDescription:]
而 privateConfigureRouteDescription 从lzmalloc 结论来看,内部是调用 +[AVAudioSessionRouteDescription privateCreateOrConfigure:withRawDescription:]
首先逆向 +[AVAudioSessionRouteDescription privateCreateOrConfigure:withRawDescription:]
函数逻辑大概如下
test config
if(!change) return orgDes;
else release(orgDes); alloc newDes。 newDes retain autorelease
从这个逻辑,可以看出来,如果是 new 出来的对象,那是绝对不可能野的。 所以,对象只可能是返回了 orgDes。
逆向 -[AVAudioSession privateConfigureRouteDescription:]
函数逻辑大概如下
lock
{
get orgDes
newdes = call privateCreateOrConfigure:withRawDescription:
return newdes
}
发现问题了吗? 如果 newdes = orgdes 呢。 而函数返回后,刚好另一个线程执行了 privateCreateOrConfigure:withRawDescription: 而这个时候,config 又恰好变动呢。 orgDes 会被释放!! 哎,这个锁算是白加了。
问题原因可能猜到了。但是如何修改呢?
hook -[AVAudioSession privateConfigureRouteDescription:] 内部调用原函数之后加上 retain autorelease? 似乎挺理想,但是仔细想想,还是没什么用啊,照样阻止不了其他线程 privateCreateOrConfigure:withRawDescription:的调用。当然这个可以很大降低概率,因为间隔代码很少。
so,换种思路,根据之前动态调试的结果 privateCreateOrConfigure:withRawDescription: 触发时机,有两个,一个是系统耳机插拔通知的时候,另一个就是我们自己调用 audiosession.currentroute 的时候。 而系统通知只在 audio 线程调用。所以呢,既然如此,那我们自己干脆不调用了,在系统通知的时候,在回调里面保存最新的。 当需要访问 audiosession.currentroute 直接返回我们保存的值。 这样,冲突不就没了
修改外发 很幸运,已经消灭了这个问题。
总结下
最近发现不少苹果的内存问题。 不知道为什么苹果自己代码很多都不使用 arc,也许这样做很 cool!!
不过,连苹果这么牛这么自信的开发,都弄出了这么多难缠的问题。我们还是不要向他学习,老老实实的用好 ARC 吧。
本文系腾讯Bugly独家内容,转载请在文章开头显眼处注明注明作者和出处“腾讯Bugly(http://bugly.qq.com)”
腾讯Bugly 最专业的质量跟踪平台
精神哥、小萝莉,为您定期分享应用崩溃解决方案