Crash大概可以分成两种:SIGABRT 和 EXC_BAD_ACCESS。
- SIGABRT :是程序可以控制的崩溃,会因为应用做了系统不支持的事情而终断,简单来看,这种crash大半都是可追溯的,因为当crash发生的时候,会帮我们断点定位到可能存在问题的代码处
- EXC_BAD_ACCESS :是全局堆栈crash,没有太多的信息可追溯,难以追踪、调试
实际场景有下面几种情况:
结论来自腾讯Bugly:
参考下面的这张图: 野指针表现形式.png
- 对象释放后内存没被改动过,原来的内存保存完好,可能不Crash或者出现逻辑错误(随机Crash)。
- 对象释放后内存没被改动过,但是它自己析构的时候已经删掉某些必要的东西,可能不Crash、Crash在访问依赖的对象比如类成员上、出现逻辑错误(随机Crash)。
- 对象释放后内存被改动过,写上了不可访问的数据,直接就出错了很可能Crash在objc_msgSend上面(必现Crash,常见)。
- 对象释放后内存被改动过,写上了可以访问的数据,可能不Crash、出现逻辑错误、间接访问到不可访问的数据(随机Crash)。
- 对象释放后内存被改动过,写上了可以访问的数据,但是再次访问的时候执行的代码把别的数据写坏了,遇到这种Crash只能哭了(随机Crash,难度大,概率低)!!
- 对象释放后再次release(几乎是必现Crash,但也有例外,很常见)。
常见的调试方式:
1.NSZombie Objects --- 僵尸对象
原理:对于已经释放的对象,系统会把它标识为僵尸对象,当给这个僵尸对象发消息的时候,会出现Crash,通过系统打印的log信息我们就能够进行定位。
开启Zombie Objects像下面这样的:
static NSMutableArray *array;
- (void)viewDidLoad {
[super viewDidLoad];
array = [[NSMutableArray alloc] initWithCapacity:5];
[array release];
}
- (void) viewWillAppear:(BOOL)animated {
[array addObject:@"Hello"];
}
运行起来之后你就会收到一条像这样的消息:
-[__NSArrayM addObject:]: message sent to deallocated instance 0x6557370
从log中可以看到,给已经释放的数组发送了一条消息。有了这些信息我们就能够比较快的进行定位代码,解决问题。(如果发生了全系统栈Crash,很多时候这个就没什么用了)
2.Address Sanitizer-地址消毒器(翻译过来是这样的🤒)
原理:当程序创建变量分配内存时,将此内存后面的一段内存也冻结住,标识为中毒内存。如图所示,黄色是变量所占内存,紫色是冻结的中毒内存。
内存示意图
当程序访问到中毒内存时(越界访问),就会抛出异常,并打印出相应的log信息。如果变量释放了,变量所占的内存也会标识为中毒内存,这时候访问这段内存同样会抛出异常(访问已经释放的对象)。
像这样的:
char *buffer;
- (void)viewDidLoad {
[super viewDidLoad];
unsigned size = 11;
buffer = malloc(size);
sprintf(buffer, "Hello World!");
NSLog(@"%p, %s", buffer, buffer);
}
运行起来之后:
捕获到的内存越界
同样的代码,要是使用Zombie Objects选项来检测的话,是很难被发现的。因此对于 Zombie 来说 ,Address Sanitizer 拥有着更加强大的捕获能力,它们虽然功能相似,但是还是存在差异的:
Zombie VS Sanitizer从功能上看,貌似 Sanitizer 能干一些 Zombie 所不能干的事,但是 Sanitizer 还是存在弊端的:
- 使用 Address Sanitizer 除了分配对象的内存之外,还需要额外的内存,这会导致App内存大量增加,用起来有可能会比较卡。(仅仅考虑Debug环境,线上环境你勾选这个?苹果大爷不会让你通过审核的🙄)
- Address Sanitizer 可能会没有log(官方的说法是显而易见的错误),不过会在访问中毒内存的代码处断住。
3.Hook对象的dealloc方法
该文章提出,我们可以通过Hook根类的dealloc方法将它重定位到我们Proxy对象的dealloc方法中来,这样当某个对象被释放的时候就会调用Proxy的dealloc方法,在该方法中让对象的isa指针指向Proxy对象,同时监听该对象消息转发的过程,如果接下来Proxy对象仍然能够收到消息的话,即抛出异常。同时为了避免内存泄露,在延时30s之后,将对象重定位回原来的类,并调用该类的dealloc方法。这样就完成了一次监听工作。
image
-
需要注意的是,我们选取的Proxy对象要和准备监听的对象结构是对齐的,这一个原则是我们所不能违背的。
网友评论