解开内存位置
Debug Symbol
要解开crash report 中的内存位置,必须要有debug symbol。为了在阅读crash report 时可以找得到debug symbol,我们建议尽可能保留所有的build,在提供QA 或是公司内部其他成员测试的时候,应该要透过统一的入口。
如果有位同仁找了一位iOS 工程师安装版本,这位工程师直接用Xcode 跑了run 安装一个版本,发生crash 的时候,这位同仁可能会找另外一位iOS 工程师解决,这种状况下,可能根本搞不清楚当时装的是哪个版本,用的是哪个版本的SDK 以及Xcode—不同版本SDK 与Xcode 也会编出不同的binary。我们建议在发行内部版本时,会透过同一台build machine 编译,像是架设内部的Jenkins 等持续整合服务,或是透过Crashlytics 或HockeyApp 发布内部版本。
当你在Xcode 选择Product->Archive 之后,我们可以从Organizer window 中找到刚刚建立好的archive,debug symbol 就放在archive 里头。我们对archive 档案按右键,从右键选单中选择「Show Package Contents」,就会跳出这个archive 的内容,debug symbol 就放在dSyms 目录下。
一个App 可能有不只一个debug symbol,像KKBOX 除了主程式之外,还做了Apple Watch 的extension,所以也会有属于Apple Watch 这一端的debug symbol。
而我们在这边看到的两个.dSYM 其实也都是bundle,我们又要用右键选单的「Show Package Contents」查看里头的内容。
以KKBOX 来说,在KKBOX.app.dSYM 下的Contents/Resources/DWARF/KKBOX,才是我们最后想使用的debug symbol 档案。
atos
找到debug symbol 后,我们现在要用atos(address to symbol)这个command line 指令。重要参数包括:
- -arch,我们要使用哪个architecture。 在这边我们输入armv7,如果是64 位元环境则输入arm64;如果是解决Mac 的问题,则可能是x86_64 或i386。
- -o,debug symbol 档案。 我们传入刚才找到的Contents/Resources/DWARF/KKBOX 的完整路径。
- -l,KKBOX 被载入到哪段记忆体位置。 在前一章当中,我们知道是在0x10000 - 0xc1bfff,所以输入0x10000。
我们首先会输入Thread 0里头call stack中位在14的0x000ee070这个记忆体位置。在main thread中,call stack的底层一定是main
,如果不是main
,就代表我们找错debug symbol了。
看起来没错。接着我们可以把整个exception 的call stack 贴进去。
由于我们的debug symbol只会包含KKBOX,不包含UIKit等系统library,所以只会显示属于KKBOX的问题。总之,我们问题出在KKIPadMyboxUserSearchTableViewController这个class里头的tableView:cellForRowAtIndexPath:
里头,位在KKIPadMyboxUserSearchTableViewController.m第128行。
修正问题
接着我们就可以去检查KKIPadMyboxUserSearchTableViewController.m 第128 行做了什么,我们发现,这是KKBOX 的用户搜寻功能中,用来显示搜寻结果的相关部分,我们预期用户的名称会是字串。搭配QA 提供给我们的console log
Info: *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[NSNull length]: unrecognized selector sent to instance 0x3a866830'
看起来我们预期的是字串,但是server的API提供的JSON回应中,用户名称却是null。我们在Objective-C语言中,会把JSON的null转成NSNull型别,所以调用属于字串的length
这个method时,就会出现找不到selector的错误。所以,我们在这边要做一些型别判断,当用户名称是null的时候,要用别的方式显示。
说起来NSNull 还顶讨厌的,有时候我们会希望就算程式中任何地方出现NSNull,都不会crash。由于跟NSNull 有关的crash 往往是找不到selector,如果想要一劳永逸:让NSNull 可以回应所有的selector,如何?
我们从第一章中了解Objective-C的动态特性,知道可以对任何class添加method,最简单的方法就是使用category;Objective-C物件还有另外一个特性:如果一个class并没有某个selector,在找不到selector的时候,会先透过forwardInvocation:
,询问这个class要不要把这次的呼叫交给别的物件处理。
透过这两个特性,就可以来施点动态魔法:透过category实作NSNull的forwardInvocation:
。
@interface NSNull (SafeNull)
@end
@implementation NSNull (SafeNull)
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];
if (!signature) {
NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:@encode(void)];
return sig;
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
}
@end