内常见Crash 的类型
在苹果官方文件Technical Note TN2151 Understanding and Analyzing iOS Application Crash Reports上,可以看到完整的错误说明,当中最常见的是Bad Memory Access(EXC_BAD_ACCESS / SIGSEGV / SIGBUS)与Abnormal Exit(EXC_CRASH / SIGABRT)这两项。如果遇到了在这之外的错误,可以参考前述苹果文件,尤其是像错误代码为00000020这类的「其他错误」,大概也就只有这篇文件可以参考,去Stack Overflow也不见得可以找到答案。
Bad Memory — 内存错误
在ARC 问世之后,这样的问题已经少了很多,不然在iOS 5 之前,记忆体错误几乎占所有crash 的最大宗。记忆体错误代表的是我们尝试使用一个不正确的记忆体指标,在crash log 的Exception Sub-code 这段,会出现当时尝试使用的记忆体位置。
最常见的内存问题就是一个Objective-C 实例的retain 与release 不成对,一个实例已经retain count 为0 了,我们还继续要求这个实例release;或是一个变数在release 的时候没有指向nil,所以这个变数所指向的实例已经retain count 为0 了,我们还尝试调用,于是调用到错误的内存。此外也包含C 的内存错误,像还没有alloc 一块内存就先调用。
虽然有了ARC之后内存问题少很多,但还是会发生。我内存管理Part 1与记忆体管理Part 2 - ARC讨论了不少相关议题,在这边就不重复。
要修正记忆体管理问题,找到crash 发生在哪一行是第一步,可以找到是哪个物件、或是哪块记忆体出问题,不过要修正的不见得就是直接发生crash 的那一行:一个物件或一块记忆体产生之后,往往会在很多地方使用过,所以retain、release 不成对的状况很有可能发生在crash 的地方之前。
例如,我们现在写一个手动管理记忆体的UIViewController,有个叫做button的成员变数,我们在loadView
的地方我们手动写了一行button = [UIButton buttonWithType:UIButtonTypeCustom]
,之后把button变成self.view的subview。在这个UIViewController的dealloc
的地方,我们写了[button release]
,结果发生了crash,要修正的可能就不是这行[button release]
,而是一开始要把button retain一份,写成button = [[UIButton buttonWithType:UIButtonTypeCustom] retain]
。
要找到记忆体在什么地方不成对,可以用Instrument 的Zombie 这项设定做profiling。
Abnormal Exit - 发生了Exception
只要程式中有地方发生了NSException throw,或是没有达到NSAssert 的条件,就会触发这种错误,前一节的NSNull 问题就是这种。遇到这种错误,首先要看的不是crash 的thread,要去看「Last Exception Backtrace」,以及console 上的讯息。
常见Exception 包括:
找不到selector
出现这种错误的时候,会跳出「unsupported selector」错误讯息。
这种错误的原因是,我们期待操作的物件,与实际上拿到的物件不一样。我们想要一个Array的时候可能拿到字串,想要拿到字串的时候却拿到NSNull,或是我们期待的是一个mutable 的物件,结果拿到的却是immutable 的。于是,这个物件没有我们期待的selector 可以使用。
说起来这个问题是Objective-C 这个语言天生的问题:所有的物件都可以cast 成id,然后一个物件放进array 或dictionary 拿出来之后,也无法确实确认是哪种型别。以下面这行code 来说:
NSString *s = [aDict objectForKey:@"key"];
我们根本不能相信s 一定是NSString,所以就会写一堆这样的code:
NSString *s = [aDict objectForKey:@"key"];
if ([s isKindOfClass:[NSString class]]) {
// 繼續做事
}
如果不这么写,就有可能发生crash。真的要解决问题,第一个方法就是,我们以后就别写Objective-C 了,直接改写Swift,一方面Swift 的array 与dictionary 可以透过Generics 语言特性指定里头的物件型态,再来Swift 语法中会经常强迫我们确认物件型别,在Swift 中,我们可能会写出大量的if let 语法:
if let s = aDict["key"] as? NSString {
// 繼續做事
}
再苹果在WWDC 2015 中,宣布Objective-C 也可以选用Generics 语法,应该也会有一些帮助。
另外一个方式是,我们尽量避免直接使用NSArray或NSDictionary当model,而是在这些物件上另外包装一层我们自己的model实例,在想要取用某个property的时候,这个model class会做好型别的判断,确实回传符合型别的物件。像GitHub推出的open source专案Mantle,就可以帮助我们撰写这类的model实例,在这个Project的设计中,透过大量的tranformer实例,让每个property都转出正确的形态。
nil 的操作
无论是对NSMutableArray或NSMutableDictionary插入nil,都会发生crash。要避免这个问题,就是在做插入的动作之前,都先检查一下现在要插入的物件是否是nil;或是,如果你使用Xcode 6.3之后的版本,也可以使用nullable
、nonnull
等关键字,确认使用的变数是否是nil。
要不然就是改写Swift:Swift语法特别强调一个变数是否可以指向nil,这项特性叫做Optional,一个可以指向nil的变数必须设成Optional,也就是变数后方必须加上一个问号,而这个变数以后每次出现,后方都一定会出现问号与惊叹号。而Objective-C的nullable
、nonnull
等关键字其实就是为了与Swift API一致。
Out of Bounds
如果一个array 只有两笔资料,但我们却去要第三笔资料,就会产生out of bounds 错误。
一边enumerate 一边改动array
假如我们一边enumerate 一个array,一边改动它,就会跳出exception。像我们想要把一个mutable 的字串array 中,长度小于3 的字串都拿掉,如果像以下这种写法就会crash:
for (NSString *s in array) {
if ([s length] < 3) {
[array removeObject:s]
}
}
我们可以先把想删除的物件先放到另外一个array 中,再告诉原本的array 要删除哪些东西。
NSMutableArray *arrayToDelete = [NSMutableArray array];
for (NSString *s in array) {
if ([s length] < 3) {
[arrayToDelete addObject:s];
}
}
[array removeObjectsInArray:arrayToDelete];
不过,如果我们想做的事情是想把一些东西从某个array 滤掉,也可以考虑改用NSPredicate。上面的code 其实意思也就是:把长度大于2 的字串留下来。
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"SELF.length > 2"];
[a filterUsingPredicate:predicate];
UIKit 中的assertion
UIKit中有不少跟资料一致性相关的assertion。当我们要求一个table view删除或加入某些列、同时带有动画效果的时候(透过呼叫-insertRowsAtIndexPaths:withRowAnimation:
与-deleteRowsAtIndexPaths:withRowAnimation:
这些method),如果table view的data source没有对应的变化—像原本table view里头有六列,我们要求删除一列,但table view的data source并没有变成五列,那么就会造成table view crash。
所以在遇到经常变动的model 的时候,我们需要考虑关闭动画效果。以KKBOX 的歌单功能来说,我们除了可以让用户手动编辑歌单之外,歌单的内容也可能因为背景的同步作业、或是下载歌曲的状态改变而更动;如果在table view 中出现动画的时候,发生这些状况,就会crash。
此外,在使用UIKit 的各种元件的时候,我们要对0.25 秒这个时间保持敏感,绝大多数在UIKit 中的动画效果都是0.25 秒,像上面提到的table view 新增或删除row 的动画、 UINavigationController push 或pop view controller 的动画,键盘升起的动画,以及present modal view 的动画(这个在iOS 7 之后倒是有一些改变)等等。如果在一个动画执行到一半的时候,我们的App 又要做一件跟这件动画相反的事情(像navigation controller push 的动画还没做完,我们就叫它pop),状况好一点,是view heirarchy 会变乱,画面变得乱七八糟,状况不好就是直接crash 了。