野指针就是指向一个已删除的对象或者受限内存区域的指针。
我们写C++的时候强调指针初始化为NULL,强调用完后也为其赋值为NULL,谁分配的谁回收,来避免野指针的问题。
比较常见的就是这个指针指向的内存,在别处被回收了,但是这个指针不知道,依然还指向这块内存。
MRC 时代因为引用计数手动控制,所以内存很容易在别处被回收。ARC解决了大部分这种问题。、
在iOS9之前,系统库的delegate
和target-action
有一部分是assign(unsafe_unretain)
的形式,这时候如果内存在别处被回收了,也是会出现野指针的。
所以iOS9之后这些地方就改成了weak内存修饰符,内存被回收的时候通过weak表,把这些指针设为nil。也大幅度减少了野指针的出现。
如果现在在工程中依然频繁出现野指针,几乎可以肯定是错误地使用了内存。
表现:Crash
对于Mach
、Unix
、NSException
三种不同层级的crash,NSException比较好说,可以直接定位到OC代码。问题主要来自EXC_BAD_ACCESS(SIGSEGV)
这种异常,难以在我们的应用代码中定位。
image
- SIGILL 执行了非法指令,一般是可执行文件出现了错误
- SIGTRAP 断点指令或者其他trap指令产生
- SIGABRT 调用abort产生
- SIGBUS 非法地址。比如错误的内存类型访问、内存地址对齐等
- SIGSEGV 非法地址。访问未分配内存、写入没有写权限的内存等
- SIGFPE 致命的算术运算。比如数值溢出、NaN数值等
实际我们遇到Mach Exception
绝大部分都是野指针的问题。SIGSEGV/SIGABRT/SIGTRAP 比较多见。
野指针问题表现千奇百怪,而且因为崩溃的地方并不是造成野指针的地方,而且难以重现,所以问题往往难以定位。
image
腾讯Bugly的这张图可以看到,野指针几乎可以造成各种类型的Mach Exception
。
定位工具
Zoombie Object
这是目前帮助最大的调试模式。实现原理就是 hook 住了对象的dealloc方法,通过调用自己的__dealloc_zombie
方法来把对象进行僵尸化。
id object_dispose(id obj)
{
if (!obj) return nil;
objc_destructInstance(obj);
free(obj);
return nil;
}
正常的对象释放方法如上,但是僵尸对象调用了objc_destructInstance
后就直接return了,不再free(obj);
。同时生成一个"_NSZombie_" + clsName
类名,调用objc_setClass(self, zombieCls);
修改对象的 isa 指针,令其指向特殊的僵尸类。
如果这个对象再次收到消息,objc_msgsend
的时候,调用abort()崩溃并打印出调用的方法。
野指针指向的内存没有被覆盖的时候,或者被覆盖成可以访问的内存的时候,不一定会出现崩溃。这个时候向对象发送消息,不一定会崩溃(可能刚好有这个方法),或者向已经释放的对象发送消息。 但是如果野指针指向的是僵尸对象,那就一定会崩溃了,会崩溃在僵尸对象第一次被其它消息访问的时候。
Zombie Object without Xcode
僵尸对象必须在连接Xcode中debug的时候使用,如果我们想跟我们的崩溃收集工具集成在一起,就需要自己实现类似Zombie Object的东西。
逻辑是通过hook住NSObject的根类的dealloc方法,然后在新的dealloc方法中将本来即将释放的对象的isa指针改为指向我们创建的一个新的僵尸类。
iOS使用代码排查野指针错误
和 开发自己的NSZombie这两篇文章里介绍了在代码里实现类似Zoombie Object的方法,然而实际上是无法使用的,这两种实现跟 Zombie Object 实现上不小的区别,实际应用中有大量误判的情况。</