僵尸对象及其检测

野指针

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

内存分配

  • 申请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 指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择器,响应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。

参考文档

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352

推荐阅读更多精彩内容