OC源码 —— alloc, init, new和dealloc

上一篇最后讲release的时候说到,在release的最后,当引用计数减为0的时候就进入了dealloc的过程。这一篇就来讲讲dealloc和相关的一些方法。先从dealloc的对头alloc说起。


alloc

关于alloc,最常见的用法应该算是:

[[XXClass alloc] init];

方法alloc的调用栈是这样的:

+ (id)alloc {
    return _objc_rootAlloc(self);
}

id _objc_rootAlloc(Class cls)
{
    return callAlloc(cls, false/*checkNil*/, true/*allocWithZone*/);
}

static id callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    return class_createInstance(cls, 0);
}

id  class_createInstance(Class cls, size_t extraBytes)
{
    return _class_createInstanceFromZone(cls, extraBytes, nil);
}

static id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    size_t size = cls->instanceSize(extraBytes);
    id obj = (id)calloc(1, size);
    obj->initInstanceIsa(cls, hasCxxDtor);
    return obj;
}

在深入alloc方法的时候,我发现alloc方法的调用栈很深,但是做的事情其实不多,中间很多方法看起来一大段,其实真正执行的只有一小部分,所以我在展示源代码的时候只保留了最常用的代码。

下面就来分析一下alloc方法最深处的_class_createInstanceFromZone()方法。

  • size_t size = cls->instanceSize(extraBytes);
size_t instanceSize(size_t extraBytes) {
    size_t size = alignedInstanceSize() + extraBytes;
    // CF requires all objects be at least 16 bytes.
    if (size < 16) size = 16;
    return size;
}

uint32_t alignedInstanceSize() {
  return word_align(unalignedInstanceSize());
}

uint32_t unalignedInstanceSize() {
    return data()->ro->instanceSize;
}

static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
#   define WORD_MASK 7UL

第一步获取对象的大小,注释已经讲的很清楚,对象大小最小为16。并且word_align()方法确保大小是8的倍数,其实就是按字节对齐。

  • id obj = (id)calloc(1, size);
    懂一点c的人都明白,这是分配一块大小为size的内存,并返回指向起始地址的指针。

  • obj->initInstanceIsa(cls, hasCxxDtor);
    初始化isa,这一部分的源代码在Runtime源码 —— 对象、类和isa这篇文章中已经讲过了,这里就不说了。

  • return obj;
    最后返回初始化好的obj。

总的来说alloc方法还是很简单的。


init

init方法更简单:

- (id)init {
    return _objc_rootInit(self);
}

id _objc_rootInit(id obj)
{
    return obj;
}

简单到什么都没做。

看到这里,我就奇怪了,这init什么都不做,还要调用了干什么,只要用一个alloc不就够了么,我测试了一下:

alloc方法测试.png

只调用alloc,可以设置属性,可以调用方法,看起来我们习惯的写法是远古遗留的产物。


new

现在其实这么写的已经不多了:

[[XXClass alloc] init];

大多数都用一个new代替了:

[XXClass new];

这个方法也很简单:

+ (id)new {
    return [callAlloc(self, false/*checkNil*/) init];
}

其实就是把alloc和init结合起来了,不过现在init什么事都不做,new和alloc方法其实是一个意思了。


dealloc

自从进入ARC时代,dealloc方法已经不常见了,除非有CF对象需要释放。

本文开始的时候也提到过,在release方法最后,当引用计数降为0的时候,会调用dealloc方法释放内存。看看dealloc方法究竟做了什么事。

- (void)dealloc {
    _objc_rootDealloc(self);
}

void _objc_rootDealloc(id obj)
{
    obj->rootDealloc();
}

inline void objc_object::rootDealloc()
{
    if (fastpath(isa.nonpointer  &&  
                 !isa.weakly_referenced  &&  
                 !isa.has_assoc  &&  
                 !isa.has_cxx_dtor  &&  
                 !isa.has_sidetable_rc))
    {
        free(this);
    } 
    else {
        object_dispose((id)this);
    }
}

在最后的rootDealloc()方法中,有个if判断,条件分为5个部分:

  1. isa.nonpointer
    是否是nonpointer,基本都是
  2. !isa.weakly_referenced
    是否被弱引用
  3. !isa.has_assoc
    是否有关联对象
  4. !isa.has_cxx_dtor
    是否有c++析构器
  5. !isa.has_sidetable_rc
    引用计数是否曾经溢出过

如果以上判断条件都为真,那么可以快速释放对象,这也是为什么在Runtime源码 —— 对象、类和isa这篇文章中介绍isa结构体相关字段时提到过的可以快速释放对象,就是在这里知道的。

那么大多数情况是什么样的呢?大多数情况这个判断都是false,因为第4点,只需要类中声明过属性或者实例变量,那么就需要c++析构函数来释放这些ivars。所以想要快速释放,要求还挺高。

那就看看慢速释放的过程吧:

object_dispose((id)this);

id object_dispose(id obj)
{
    objc_destructInstance(obj);    
    free(obj);
    return nil;
}

void *objc_destructInstance(id obj) 
{
    if (obj) {
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        if (cxx) object_cxxDestruct(obj);
        if (assoc) _object_remove_assocations(obj);
        obj->clearDeallocating();
    }

    return obj;
}

释放对象的时机是在objc_destructInstance()方法调用之后,这个方法做的事情是销毁这个对象存在的证据,分成3个部分销毁:

  • object_cxxDestruct(obj);
  • _object_remove_assocations(obj);
  • obj->clearDeallocating();
part1
object_cxxDestruct(obj);

void object_cxxDestruct(id obj)
{
    object_cxxDestructFromClass(obj, obj->ISA());
}

static void object_cxxDestructFromClass(id obj, Class cls)
{
    void (*dtor)(id);

    // Call cls's dtor first, then superclasses's dtors.
    for ( ; cls; cls = cls->superclass) {
        if (!cls->hasCxxDtor()) return; 
        dtor = (void(*)(id))
            lookupMethodInClassAndLoadCache(cls, SEL_cxx_destruct);
        if (dtor != (void(*)(id))_objc_msgForward_impcache) {
            (*dtor)(obj);
        }
    }
}

学过c++的都知道析构函数从子类开始,逐层向上调用。这个方法其实就做了这个事情,有一点奇怪的就是,在我们的类中,并没有声明过任何析构函数,那么查找到的析构函数究竟是什么呢?

用代码测试一下,先在类中增加一个属性,这样就能进入这个方法了:

// ZNObject.h
@interface ZNObject : NSObject
@property (nonatomic, copy) NSString *name;
@end

// ZNObject.m
@implementation ZNObject
- (void)dealloc {
    NSLog(@"znobject dealloc");
}

接着添加这些代码用来测试dealloc的过程:

// ViewController.m
@interface ViewController() 
@property (nonatomic, strong) ZNObject *obj;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.obj = [ZNObject new];
}

- (void)viewDidAppear {
    self.obj = nil;
}

挺愚蠢的,其实加个大括号就可以了。

在self.obj = nil;这行先加个断点,进入断点之后,在object_cxxDestructFromClass()方法中添加一个断点,运行进入断点如下图:

寻找c++析构函数.png

先看左侧的调用栈,当我将self.obj设为nil的时候,就进入了release的过程。这里引用计数为0,所以继续进入了dealloc的过程。先调用了ZNObject类中的dealloc,打印出了log,然后就进入了本节dealloc的流程。

看看dtor的内容,析构函数是一个叫做.cxx_destruct的方法,但是这个方法找不到实现的代码,怎么才能看到这个方法做了什么呢?

换个思路试试看,c++的析构函数是在对象有ivar的时候才会被调用,调用的目的就是为了释放这些ivar,那么我们只需要观察ivar的变化情况就可以了。

调整一下测试的代码,先给属性赋个值,再运行:

观察name属性的变化.png

添加一个watchpoint,当name变化的时候,会自动进入断点,直接运行程序:

.cxx_destruct内部.png

可以看到name的销毁是在objc_storeStrong方法中进行的,这个方法被.cxx_destroy调用。遗憾的是,依然不能窥探到.cxx_destroy究竟做了些什么事。

讲了这么多,其实就说明了ivar释放的过程,下面进入第二步。

part2
_object_remove_assocations(obj);

void _object_remove_assocations(id object) {
    vector< ObjcAssociation,ObjcAllocator<ObjcAssociation> > elements;
    {
        AssociationsManager manager;
        AssociationsHashMap &associations(manager.associations());
        if (associations.size() == 0) return;
        disguised_ptr_t disguised_object = DISGUISE(object);
        AssociationsHashMap::iterator i = associations.find(disguised_object);
        if (i != associations.end()) {
            // copy all of the associations that need to be removed.
            ObjectAssociationMap *refs = i->second;
            for (ObjectAssociationMap::iterator j = refs->begin(), end = refs->end(); j != end; ++j) {
                elements.push_back(j->second);
            }
            // remove the secondary table.
            delete refs;
            associations.erase(i);
        }
    }
    // the calls to releaseValue() happen outside of the lock.
    for_each(elements.begin(), elements.end(), ReleaseValue());
}

关于关联对象,最常见的应用应该就是在category中声明属性。这一块这里就不展开了,后面谈Associated Object的时候再说吧。

这里只要知道这一块做的事情就是移除对象的associated object,更直接一点就是如果有这样的代码:

objc_setAssociatedObject(self.obj, @selector(obj), self.obj2, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

在self.obj对象dealloc的时候,就会进入_object_remove_assocations(obj)方法。

part3
obj->clearDeallocating();

inline void objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        sidetable_clearDeallocating();
    }
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        // Slow path for non-pointer isa with weak refs and/or side table data.
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}

在这一节最开始提到了一共5个判断条件,part1对应的是4和part2对应的是3,其他3个条件就都在这个方法里啦。

第一个if判断对应1,因为基本都是nonpointer,所以直接看else中的内容。else中对应了2和5两个条件,看看clearDeallocating_slow()是怎么做的:

void objc_object::clearDeallocating_slow()
{
    SideTable& table = SideTables()[this];
    if (isa.weakly_referenced) {
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        table.refcnts.erase(this);
    }
}

这里再次出现了SideTables,在上一篇讲release的时候有提到过,SideTable存储了溢出的引用计数,也与弱引用相关,这里其实就是对这两部分进行处理。关于SideTable的具体内容就不展开了。这里只需要知道:

  • 如果有对此对象的弱引用,那么把所有的弱引用都置为nil。weak对象设为nil原来就是在这里进行的。
  • 如果引用计数曾经溢出过,那么SideTable中就存储过相关信息,当然在这个时间点,引用计数的值肯定是为0的,但是即使是0也不放过,还要把曾经存在的痕迹抹除掉。感觉好残忍呀。。。

到这里就完成了dealloc的全部过程。

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

推荐阅读更多精彩内容