作者:李偉
先上棧,這個 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 吧。