前段时间做了一个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%。