多線程導致的iOS閃退分析

前段時間做了一個APP,在測試的時候遇到了很奇怪的閃退情況。

這個APP是有關聲音處理的:設備一邊錄音,一邊對聲音進行處理。所以需要2個線程,一個線程將錄音保存下來,另一個處理保存下來的聲音。測試的時候,會在1~10min之內,不定時、無預兆的出現閃退的情況,報的錯也各不一樣,有的是

1)”NSGenericException ‘Collection was mutated while being enumerated”

或者:

2)”pointer being freed was not allocated”

還有的閃退信息提示內存用的太多:

3)”crash due to memory pressure”

更有的打出了天書:

4)”First throw call stack:

(0x30170ecb 0x3a907ce7 0x301709b9 0x4a177 0x40a97 0x30b5bab5 0x3013bf0f 0x3013bb2b 0x30139eb3 0x300a4729 0x300a450b 0x350036d3 0x32a05871 0x4f4b9 0x3ae05ab7)”

而時間要求又比較急,真是有點焦頭爛額。經過1天多的研究,這些問題被一一搞定。

首先,第3條,明顯是生成了內存而沒有釋放。咦,Objective-C不是有ARC嗎,自動處理內存啊,好長時間都沒有因爲內存煩惱了。但是聲音的處理都是比較底層的,用的都是C語言,對這部分的內存,ARC就愛莫能助了。處理起來也不麻煩了,先用Instruments定位一下,哪邊泄漏的內存,然後對所有malloc出來的內存塊,用完了都free掉。再試一下,內存的增加果然慢下來了。

第1條,”Collection was mutated while being enumerated”,意思是,一個對象(一般是NSArray什麼的)在被訪問的時候,這個對象發生了變化,導致程序掛掉了。在我的程序裏,這個對象就是存儲聲音數據的東西,暫且叫它data。第1個線程會源源不斷的向data裏寫入新數據,並將舊數據刪掉。而第2個線程,則會定時讀取data的內容並做處理。因爲這兩個線程每次操作data的時間都比較短,所以它們同時操作的情況不是很常見,所以一般程序也能堅持個幾分鐘。然而它們一旦同時對data進行操作/訪問,程序就掛了。

解決方法也很簡單,Objective-C裏有一個語法,專門處理這樣的事:@synchronized(參數){代碼塊}

當兩個@synchronized代碼塊的參數相同的時候,這兩個代碼塊是不能同時操作的。對於參數,我們經常使用self來做參數。例如:

線程1:

@synchronized(self){

//寫data

}

線程2:

@synchronized(self){

//讀data

}

當線程1在寫data時,線程2只能等着。這樣就避免了同時多線程同時讀/寫global數據塊會出現的閃退問題。

第2條,”pointer being freed was not allocated”,也是因爲多線程同時寫data的問題,但有一些特殊。data裏只包含了最近一段時間的聲音數據,當data存儲的太多了,就會先將舊數據刪了,再將新數據存起來。問題是線程1的調用次數非常快,達到1秒鐘50次。有時候上一次調用a還沒結束,下一次調用b又過來了,這時候就可能會出問題:a檢測到data裏的數據太多了,就將最舊的數據刪了。然而沒等a真正將數據刪除,b又來了,它也要將最舊的數據刪了,這樣同一個數據就要被free兩次,編譯器就要叫:尼碼這個指針裏已經空了,你還要老子再free它,老子不幹了!

解決方法同上,也加一個@synchronized,將這段數據塊包起來,告訴編譯器:爲了保證服務質量,每次只向一個線程提供服務,等上一位大爺舒坦了,再讓下一個進來。

最後,有時候編譯器還會抽風,只告訴你,”我要掛了!”(EXC_BAD_ACCESS),然後嘔吐出一大堆排泄物:

“First throw call stack:

(0x30170ecb 0x3a907ce7 0x301709b9 0x4a177 0x40a97 0x30b5bab5 0x3013bf0f 0x3013bb2b 0x30139eb3 0x300a4729 0x300a450b 0x350036d3 0x32a05871 0x4f4b9 0x3ae05ab7)”

作爲程序員,我們要從這堆排泄物中找到編譯器的病因,看看它到底吃了啥:

在AppDelegate.m里加入這個函數:

void uncaughtExceptionHandler(NSException *exception) {

NSLog(@”CRASH: %@”, exception);

NSLog(@”Stack Trace: %@”, [exception callStackSymbols]);

//Internal error reporting

}

然後:

– (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {

NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);

}

這樣,當編譯器掛了的時候,就會打印出這樣的東西:

0   CoreFoundation                      0x30c16f9b <redacted> + 154,
1   libobjc.A.dylib                     0x3b491ccf objc_exception_throw + 38,
2   CoreFoundation                      0x30b4da39 <redacted> + 176,
3   TEST                    0x001ce2c9 -[TestBaseViewController viewDidLoad] + 848,

我們就能看到,問題出在[TestBaseViewController viewDidLoad]函數裏(請忽略後面的+848,我不知道是什麼意思,貌似也沒人知道)。雖然無法定位到具體哪一行,但至少是大大縮小的範圍。

另外,還有一個關於崩潰定位的小技巧:在所有你懷疑會出現閃退的地方附近,瘋狂的NSLog,這樣,當閃退的時候,很快就能定位到在哪個位置閃退了。

對於程序員來說,定位到問題意味着問題解決了90%。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章