iOS EXC_BAD_ACCESS的产生和调试

Crash大概可以分成两种:SIGABRTEXC_BAD_ACCESS

  • SIGABRT :是程序可以控制的崩溃,会因为应用做了系统不支持的事情而终断,简单来看,这种crash大半都是可追溯的,因为当crash发生的时候,会帮我们断点定位到可能存在问题的代码处
  • EXC_BAD_ACCESS :是全局堆栈crash,没有太多的信息可追溯,难以追踪、调试
实际场景有下面几种情况:

结论来自腾讯Bugly

  1. 对象释放后内存没被改动过,原来的内存保存完好,可能不Crash或者出现逻辑错误(随机Crash)。
  2. 对象释放后内存没被改动过,但是它自己析构的时候已经删掉某些必要的东西,可能不Crash、Crash在访问依赖的对象比如类成员上、出现逻辑错误(随机Crash)。
  3. 对象释放后内存被改动过,写上了不可访问的数据,直接就出错了很可能Crash在objc_msgSend上面(必现Crash,常见)。
  4. 对象释放后内存被改动过,写上了可以访问的数据,可能不Crash、出现逻辑错误、间接访问到不可访问的数据(随机Crash)。
  5. 对象释放后内存被改动过,写上了可以访问的数据,但是再次访问的时候执行的代码把别的数据写坏了,遇到这种Crash只能哭了(随机Crash,难度大,概率低)!!
  6. 对象释放后再次release(几乎是必现Crash,但也有例外,很常见)。

参考下面的这张图:
野指针表现形式.png

常见的调试方式:

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方法

iOS监控-野指针定位

该文章提出,我们可以通过Hook根类的dealloc方法将它重定位到我们Proxy对象的dealloc方法中来,这样当某个对象被释放的时候就会调用Proxy的dealloc方法,在该方法中让对象的isa指针指向Proxy对象,同时监听该对象消息转发的过程,如果接下来Proxy对象仍然能够收到消息的话,即抛出异常。同时为了避免内存泄露,在延时30s之后,将对象重定位回原来的类,并调用该类的dealloc方法。这样就完成了一次监听工作。


image
  • 需要注意的是,我们选取的Proxy对象要和准备监听的对象结构是对齐的,这一个原则是我们所不能违背的。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 1.ios高性能编程 (1).内层 最小的内层平均值和峰值(2).耗电量 高效的算法和数据结构(3).初始化时...
    欧辰_OSR阅读 29,835评论 8 265
  • 前言 iOS崩溃是让iOS开发人员比较头痛的事情,app崩溃了,说明代码写的有问题,这时如何快速定位到崩溃的地方很...
    齐滇大圣阅读 65,584评论 29 443
  • Swift1> Swift和OC的区别1.1> Swift没有地址/指针的概念1.2> 泛型1.3> 类型严谨 对...
    cosWriter阅读 13,801评论 1 32
  • iOS面试题目100道 1.线程和进程的区别。 进程是系统进行资源分配和调度的一个独立单位,线程是进程的一个实体,...
    有度YouDo阅读 30,057评论 8 137
  • 今天很幸运,和一个小姐姐偶遇交了现场定金,这是第二次预售,最大的感触仍然是那些第一次相遇就信任我的姐姐们,想到这些...
    T久处不厌阅读 1,248评论 0 0