僵尸对象及其检测

246 阅读6分钟

野指针

指针指向的对象已经被回收,指针仍指向已经回收的内存地址,那么这个指针就叫做野指针。

内存分配

  • 申请1块空间,实际上是向系统申请1块别人不再使用的空间。

  • 释放1块空间,指的是占用的空间不再使用,这个时候系统可以分配给别人去使用。

  • 在这个空间分配给别人之前,数据还是存在的.

    • OC对象释放以后,表示OC对象占用的空间可以分配给别人
    • 但是再分配给别人之前这个空间仍然存在,对象的数据仍然存在

僵尸对象

僵尸对象一种用来检测内存错误(EXC_BAD_ACCESS)的对象,它可以捕获任何对尝试访问坏内存的调用。

如果给僵尸对象发送消息时,那么将在运行期间崩溃和输出错误日志。通过日志可以定位到野指针对象调用的方法和类名。

野指针访问问题

  1. 使用野指针访问对象,有时候会报错(EXC_BAD_ACCESS),有时候不会

    • 当野指针指向的内存未分配给别人时,可以访问到对象
    • 当野指针指向的内存分配给别人后,访问就会出问题

    因此,我们不允许通过野指针去访问已经被释放的对象。

开启僵尸对象检测

在 Xcode 中设置Edit Scheme -> Diagnostics -> Zombie Objects

源码分析

新建一个终端项目(Command Line Tool),并开启僵尸对象检测,具体代码如下:

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
​
void printClassInfo(id obj)
{
    Class cls = object_getClass(obj);
    Class superCls = class_getSuperclass(cls);
    NSLog(@"self:%s - superClass:%s", class_getName(cls), class_getName(superCls));
}
​
@interface People : NSObject{
    int _age;
}
​
@end@implementation People
- (void)dealloc {
    [super dealloc];
}
​
@end
​
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        People *aPeople = [People new];
        
        NSLog(@"before release!");
        printClassInfo(aPeople);
      
        [aPeople release];
        
        NSLog(@"after release!");
        printClassInfo(aPeople);
        getchar();
    }
    return 0;
}
​

打印消息为:

Zombie-Object[66158:522009] before release!
Zombie-Object[66158:522009] self:People - superClass:NSObject
Zombie-Object[66158:522009] after release!
Zombie-Object[66158:522009] self:_NSZombie_People - superClass:nil

(1)由打印消息得知,People释放后所属的类型变为了_NSZombie_People,即对象释放后变成了僵尸对象,保存当前释放对象的内存地址,防止被系统回收。

这边其实可以看到 _NSZombie_People 是没有父类的,是一个根类,并没有实现任何方法,因此所有发送给僵尸类的消息都要经过完整的消息转发机制。这也是触发僵尸对象机制会断点在 forwarding 的原因。

(2)从Xocde的Debug Memory Graph也可以看出,没有Person类型的引用,但是多了_NSZombie_People类型的引用,也说明Person对象释放后变成了_NSZombie_People类型的对象。

Debug Memory Graph.jpg

对象释放过程

我们先理解对象释放的过程,让Runtime 源码告诉你:

/***********************************************************************
* object_dispose
* fixme
* Locking: none
**********************************************************************/
id object_dispose(id obj)
{
    if (!obj) return nil;
​
    objc_destructInstance(obj);    
    free(obj);
​
    return nil;
}

我们可以看看 objc_destructInstance 到底都干了些什么。从其注释可以知道该方法做了下面几件事: 【C++ destructors】【ARC ivar cleanup】【Removes associative references】 并没有释放其内存,而是在free(obj) 时才释放了内存。

/***********************************************************************
* objc_destructInstance
* Destroys an instance without freeing memory. 
* Calls C++ destructors.
* Calls ARC ivar cleanup.
* Removes associative references.
* Returns `obj`. Does nothing if `obj` is nil.
**********************************************************************/
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();
​
        // This order is important.
        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
        obj->clearDeallocating();
    }
​
    return obj;
}

Zombie Object 的生成过程

让我们在前面的项目中打个符号断点,去看看开启僵尸对象检测后对象的释放过程

使用__dealloc_zombie符号断点

    0x1885150a8 <+56>:  mov    x0, x19
    0x1885150ac <+60>:  bl     0x18856262c               ; symbol stub for: object_getClass
    0x1885150b0 <+64>:  str    xzr, [sp, #0x10]
    0x1885150b4 <+68>:  bl     0x18856185c               ; symbol stub for: class_getName
    0x1885150b8 <+72>:  str    x0, [sp]
    0x1885150bc <+76>:  adrp   x1, 618
    0x1885150c0 <+80>:  add    x1, x1, #0x402            ; "_NSZombie_%s"
    0x1885150c4 <+84>:  add    x0, sp, #0x10
    0x1885150c8 <+88>:  bl     0x18856161c               ; symbol stub for: asprintf
    0x1885150cc <+92>:  ldr    x0, [sp, #0x10]
    0x1885150d0 <+96>:  bl     0x18856247c               ; symbol stub for: objc_lookUpClass
    0x1885150d4 <+100>: mov    x20, x0
    0x1885150d8 <+104>: cbnz   x0, 0x1885150f8           ; <+136>
    0x1885150dc <+108>: adrp   x0, 617
    0x1885150e0 <+112>: add    x0, x0, #0xdfa            ; "_NSZombie_"
    0x1885150e4 <+116>: bl     0x18856247c               ; symbol stub for: objc_lookUpClass
    0x1885150e8 <+120>: ldr    x1, [sp, #0x10]
    0x1885150ec <+124>: mov    x2, #0x0
    0x1885150f0 <+128>: bl     0x1885623ac               ; symbol stub for: objc_duplicateClass
    0x1885150f4 <+132>: mov    x20, x0
    0x1885150f8 <+136>: ldr    x0, [sp, #0x10]
    0x1885150fc <+140>: bl     0x188561d6c               ; symbol stub for: free
    0x188515100 <+144>: mov    x0, x19
    0x188515104 <+148>: bl     0x18856239c               ; symbol stub for: objc_destructInstance
    0x188515108 <+152>: mov    x0, x19
    0x18851510c <+156>: mov    x1, x20
    0x188515110 <+160>: bl     0x18856266c               ; symbol stub for: object_setClass

总结起来的伪代码大概是

// 获取到即将deallocted对象所属类(Class)
Class cls = object_getClass(self);
// 获取类名
const char *clsName = class_getName(cls)
// 生成僵尸对象类名
const char *zombieClsName = "_NSZombie_" + clsName;
// 查看是否存在相同的僵尸对象类名,不存在则创建
Class zombieCls = objc_lookUpClass(zombieClsName);
if (!zombieCls) {
    // 获取僵尸对象类 _NSZombie_
    Class baseZombieCls = objc_lookUpClass("_NSZombie_");
    // 创建 zombieClsName 类
    zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
}
//释放字符串                                           
free(zombieClsName)
// 在对象内存未被释放的情况下销毁对象的成员变量及关联引用。
objc_destructInstance(self);
// 修改对象的 isa 指针,令其指向特殊的僵尸类
object_setClass(self, zombieCls);

上面的伪代码就是开启僵尸对象检测后,对象释放的大致调用过程。执行 objc_destructInstance(obj) 方法后,并没有 free(obj) 这一步的调用。

从此处断点可以大概看出 Zombie Object 的生成过程。 _NSZombie_%s 验证了开启僵尸对象检测后的对象所指向的类。从这个调用栈也可以说明系统开启僵尸对象检测后不会释放该对象所占用的内存,只是释放了与该对象所有的相关引用。

结论

  1. 系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。这种对象所在的内存无法重用,因此不可遭到重写,所以将随机变成必然。
  2. 系统会修改对象的 isa 指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择器,响应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。

参考文档

本文参考以下文章并根据个人实践后完成,非常感谢文章作者