iOS objective-c runtime之内存管理

内存是程序运行的原材料,使用不同的方法将内存加工成各式各样的产品。原材料是产品质量的基础。所以对于内存的使用非常重要。
OC在iOS系统中内存管理方式是引用计数,区别于java的垃圾回收和C++和C的开发人员管理内存释放。

在引用计数中,每一个对象负责维护对象所有引用的计数值。当一个新的引用指向对象时,引用计数器就递增,当去掉一个引用时,引用计数就递减。当引用计数到零时,该对象就将释放占有的资源。

引用计数有三个要点:

  • 引用计数需要** 一个 “计数的值”**
  • 引用计数需要** 增加引用计数的方法**
  • 引用计数需要** 减少引用计数的方法**

1.runtime内存管理数据结构

引用计数的三点要求对于OC而言依然是不可或缺的,OC肯定会有相应的结构体和方法去满足上面的这些要求。从现在开始我们切换到ARC,来讨论OC的内存管理原理

OC中的大多数对象都继承NSObject,我们来看一段代码,这段代码中OC如何做到对a所指向的对象进行引用计数管理

{
   id a = [[NSObject alloc] init];
   id b = a;
}

我们将上面代码编译后看,编译器做了什么事情,下面是简化代码

    //1.alloc
    movq   0x3211(%rip), %rsi; (void *)0x000000010d142e58: NSObject
    movq   0x31c2(%rip), %rdi        ; "alloc"
    callq  0x10c7a2952               ; symbol stub for: objc_msgSend
    //2.init
    movq   0x31b3(%rip), %rsi        ; "init"
    callq  0x10c7a2952               ; symbol stub for: objc_msgSend
    //3.b = a
    callq  0x10c7a2964               ; symbol stub for: objc_retain
    //4.出了作用域,销毁b,
    callq  0x10c7a2976               ; symbol stub for: objc_storeStrong
    //5.出了作用域,销毁a,
    callq  0x10c7a2976               ; symbol stub for: objc_storeStrong

上面代码的意思是生成一个NSObject对象,用指针a,b进行持有。而在编译后的代码中我们猜测:

  • objc_retain 是OC引用计数中三要素之一的引用计数增加。
  • objc_retain objc_storeStrong两者内部肯定有对 引用计数“计数值”的减少操作。

1.1 OC引用计数数据结构和方法

通过上面的讲述,我们得到关键点objc_retain和objc_storeStrong,那么我们从这二者入手,首先是两者的源代码

//objc_retain 持有
id objc_retain(id obj)
{
    if (!obj) return obj; //nil 
    if (obj->isTaggedPointer()) return obj; //tagged是内存优化
    return obj->retain();
}

//objc_storeStrong
void objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    //先持有新值
    objc_retain(obj);
    *location = obj;
    //释放老值
    objc_release(prev);
}
//objc_release,释放
void objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return; //内存优化
    return obj->release();
}

1.isTaggedPointer是系统的内存优化,对于NSNumber,指针指向的不是对象的地址,指针本身的地址就是值。

objc_retain 是引用计数+1操作,而objc_storeStrong中调用的objc_release是-1操作,那么objc_retain和objc_release肯定有对引用计数值的操作,我们拿出objc_retain的详细调用栈看看

inline id objc_object::retain()
{
    // UseGC is allowed here, but requires hasCustomRR.
    assert(!UseGC  ||  ISA()->hasCustomRR()); 
    assert(!isTaggedPointer());
//如果没有实现retain/release,就会调用sidetable_retain()
    if (! ISA()->hasCustomRR()) {
        return sidetable_retain();
    }
//上面的都不满足,直接调用该类的retain,和release的方法
    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_retain);
}

//NSobjct协议
@protocol NSObject
- (instancetype)retain OBJC_ARC_UNAVAILABLE;
- (oneway void)release OBJC_ARC_UNAVAILABLE;
- (instancetype)autorelease OBJC_ARC_UNAVAILABLE;
- (NSUInteger)retainCount OBJC_ARC_UNAVAILABLE

ISA()->hasCustomRR()表明该类是否自定义了release和retain方法,在ARC下是不允许使用release和retain的更何况自定义。

我们发现,objc_object::retain()先断言处理掉两种异常情况:1.对象使用了GC(垃圾回收)但并没有自定义release和retain等方法;2. TaggedPointer;因为TaggedPointer是不会调用retain方法的。其次,当没有自定义release和retain 等方法的时,就使用sidetable_retain()。最后,对于自定义了release和retain 等方法的类,就直接调用他们的retain方法。

但还是没有看到引用计数真正+1的地方,但是ARC下上面的代码会进入sidetable_retain(), sidetable_retain中会有什么样的情况发生?先看代码

id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.indexed);
#endif
    //1.根据this(C++this,类似于OC的self) SideTables中取出一个SideTable容器
    SideTable& table = SideTables()[this];
    //2.给SideTable试着加锁
    if (table.trylock()) {
       //3.根据this从table的refcnts中取出引用计数“计数值”
       size_t& refcntStorage = table.refcnts[this];
       //4.值合法的情况下,增加SIDE_TABLE_RC_ONE
        if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
            refcntStorage += SIDE_TABLE_RC_ONE;
        }
        table.unlock();
        return (id)this;
    }
    return sidetable_retain_slow(table);
}

千呼万唤始出来,犹抱琵琶半遮面。上面的代码终于揭露出引用计数“计数值”面目。上面代码分析如下:

  • 第一步,先从 全局SideTables Map中以对象自己为key,取出一个SideTable。那说明程序运行期间每一个对象都会有一个一一对应的SideTable存储在SideTables()这个map中。
  • 第二步,拿到对象对应的table,先尝试对table加锁。那么我们可以知道SideTable中肯定有一个锁的机制。
  • 第三步,从table的refcnts Map中以对象的指针地址为key,取出类型为size_t 的引用计数“计数值”。 那么可以知道SideTable中存储了一个refcnts Map,用来存储真正的引用计数值
  • 第四步,检验计数值是否合法,如果合法,对计数值 增加一个SIDE_TABLE_RC_ONE,这个SIDE_TABLE_RC_ONE就是引用计数的1。既然引用计数值增加1用的是SIDE_TABLE_RC_ONE,那么说明引用计数值除了携带计数值意外,还携带其他的信息

上面四步中加粗部分使我们对 对象的内存管理数据结构的部分功能总结。下面我们来看一下和对象一一对应的SideTable具体定义:

//简化后的代码。
struct SideTable {
    spinlock_t slock;     //1.锁 
    RefcountMap refcnts;  //2.引用计数值字典
    weak_table_t weak_table; //3.存储所有指向对象的weak指针
    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }
    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }
};

SideTable作为和对象一一对应的内存管理辅助数据结构,其有三个重要成员

  • 1.slock,在多线程编程中对SideTable操作时,用来加锁
  • 2.RefcountMap 一个Map,以对象指针为key,存储了size_t的引用计数计数值
  • 3.weak_table 用来存储所有指向对象的weak指针。

这块我们重点讲解RefcountMap 中引用计数数值代表的意义,weak_table到__weak处讲解。

RefcountMap

RefcountMap 根据对象的地址,存储了size_t类型计数值
size_t在32位系统上定义为 unsigned int;在64位系统上定义为 unsigned long,也就是说 size_t 在32位系统上是32位,在64位上是64位,我们以32位为例,将引用计数化为32位如下图

引用计数标记图.png

我们在id objc_object::sidetable_retain()中提到了引用计数+1 用SIDE_TABLE_RC_ONE表示,其实还有其他的,我们都列出来

#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)  
#define SIDE_TABLE_DEALLOCATING      (1UL<<1)  // MSB-ward of weak bit
#define SIDE_TABLE_RC_ONE            (1UL<<2)  // MSB-ward of deallocating bit
#define SIDE_TABLE_RC_PINNED         (1UL<<(WORD_BITS-1))
#define WORD_BITS 32

根据图和代码

  • 计数值的 第0位表示 是否有weak指针指向该对象。该位为1表示有weak指针指向,为0表示没有。这个标记的具体用途后面会讲。
  • 计数值的 第1位表示 该对象是否正在执行dealloc。该位为1表示正在执行,为0表示没有。这个标记位为了安全,防止dealloc执行时进行其他持有操作。
  • 计数值的 第2位到31位 表示引用计数的个数。 引用计数+1 实际执行的是 SIDE_TABLE_RC_ONE (1UL<<2),表明引用计数单位一(值为4),那么第0位和第1位的值不会对计数有影响。

之所以这么做是为了节省内存,用一个变量代替三个变量使用。

现在我们知道了OC为了辅助引用计数方式的内存管理,给每一个对象生成了一个sideTable,里面的RefcountMap保存了对象的引用计数。还包括了其他的信息。


OC内存管理数据结构

2.__weak

__weak是为了防止循环引用并且对象消失后,再次调用对象的方法或者属性不会产生crash。接下来我们讲解weak的实现原理。上代码

{
    id __weak weakObj = [[NSObject alloc] init];
}

编译后代码如下

    //1.alloc
    movq   0x2959(%rip), %rsi        ; (void *)0x000000010b125e58: NSObject
    movq   0x2922(%rip), %rdi        ; "alloc"
    callq  0x10a785a56               ; symbol stub for: objc_msgSend
  //2.init
    movq   0x2913(%rip), %rsi        ; "init"
    callq  0x10a785a56               ; symbol stub for: objc_msgSend
  //3.initWeak
    callq  0x10a785a4a               ; symbol stub for: objc_initWeak
  /4.release
    callq  0x10a785a62               ; symbol stub for: objc_release
  //5.destroyWeak
    callq  0x10a785a3e               ; symbol stub for: objc_destroyWeak

编译后的代码分为五部分:1.alloc。2.init。3不知道(objc_initWeak)。4.objc_release。5.不知道(objc_destroyWeak)。
对于weak,上面的编译代码中有两个特殊的点1.objc_initWeak,2. objc_destroyWeak。那么两个函数中肯定有weak的实现操作。

2.1 weak实现原理

下面是objc_destroyWeak,objc_initWeak。部分简化后的代码

//1.初始化weak
id objc_initWeak(id *object, id value) {
  *object = nil;
  return objc_storeWeak(object, value);
}

//2.weak的内存管理函数
id objc_storeWeak(id *location, id newObj)
{
    return storeWeak
        (location, (objc_object *)newObj);
}

//3.weak实际内存管理代码,简化后的代码
id storeWeak(id *location, objc_object *newObj)
{
    assert(HaveOld  ||  HaveNew);
    if (!HaveNew) assert(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;   
    SideTable *newTable;
//第一步
 retry:
    if (HaveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (HaveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }
// 第二步
    if (HaveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    } 
//第三步
    if (HaveNew) {
        newObj = (objc_object *)weak_register_no_lock(&newTable->weak_table, 
                                                      (id)newObj, location, 
                                                      CrashIfDeallocating);
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }
        *location = (id)newObj;
    }    
    SideTable::unlockTwo(oldTable, newTable);
    return (id)newObj;
}

一连串的函数调用,其中objc_initWeak的调用如下
objc_initWeak (将object先值为空)
|objc_storeWeak(传入一些其他业务参数)
|
storeWeak(真正开始操作)
而 重点在storeWeak,其传入weak指针location,和要指向的对象newObj。然后做三步操作

  • 第一步,先取出location指向的旧对象oldObj和要指向的对象newObj它们的内存辅助管理对象SideTable(第一节我们讲过了)。
  • 第二步,如果旧对象oldObj的SideTable存在,就执行weak_unregister_no_lock函数,从字面上理解,这个函数应该是将location从旧对象的SideTable的weak_table(存放weak指针的表,weak_table一会讲解,)中撤出。
  • 第三步,如果新对象newObj的SideTable存在,就将location注册到新对象newObj的SideTable的weak_table中,然后给newObj的SideTable的内存引用计数变量打上被weak指针了的标记。最后更改location的指向值。

上面三步的重点都指向SideTable的weak_table,并且weak_table应该可以存储很多weak指针,下面我们看一下weak_table的结构

//简化后的代码
struct weak_table_t {
    weak_entry_t *weak_entries;//weak_entry_t的一维数组 
    size_t    num_entries;//数组的个数
    uintptr_t mask; 
    uintptr_t max_hash_displacement;
};

struct weak_entry_t {
    DisguisedPtr referent; //可以包裹objc_object类型指针的一个struct
    union {
        struct {
            weak_referrer_t *referrers;
        };
    };
};

weak_table_t

  • weak_entries 指向weak_entry_t的指针,系统用其指向一个weak_entry_t的数组。
  • num_entries表明了这个数组数组中有多少个元素。

weak_entry_t

  • referent 用来包裹objc_object类型指针的结构体。

从两个数据结构和使用来看,1.指向对象的weak指针会被包裹成 weak_entry_t,可以认为weak_entry_t代表weak指针,2.weak_entry_t又会被存储在weak_table_t的weak_entries指向的数组中。

weak的实现原理靠对这个表进行操作来完成。源码中提供了对weak_table_t的各种各样的操作函数,具体的内容就不再讲述了,因为都是对weak_table_t中weak_entries的操作,对我们了解weak原理没有影响,这里就只列举一下。

//将weak指针注册到对象的sideTable的weak_table的weak指针数组中
id weak_register_no_lock(weak_table_t *weak_table, id referent, 
                         id *referrer, bool crashIfDeallocating);
//和weak_register_no_lock相反,将weak指针移除
void weak_unregister_no_lock(weak_table_t *weak_table, id referent, id *referrer);

//weak_read_no_lock是在weak指针使用的时候,需要调用,下面我们讲解weak指针的使用时候会说道。
id weak_read_no_lock(weak_table_t *weak_table, id *referrer);

//清楚对象的weak_table_t中所有的weak指针
void weak_clear_no_lock(weak_table_t *weak_table, id referent);

2.2weak指针的使用

上面我们介绍了weak指针的存储结构,我们结合weak指针的使用,来看一下weak指针的原理。老习惯,上代码

{
    id __weak weakObj = [[NSObject alloc] init];
    [weakObj class];
}

编译后的结果

//1.alloc, init
    movq   0x33f1(%rip), %rdi        ; (void *)0x0000000104e34e58: NSObject
    movq   0x33a2(%rip), %rsi        ; "alloc"
    movq   0x225b(%rip), %rax        ; (void *)0x0000000104a7fac0: objc_msgSend
    movq   0x3396(%rip), %rsi        ; "init"
//2.initWeak
    callq  0x1044927b6               ; symbol stub for: objc_initWeak
//3.NSObject 没有人持有,编译器增加release
    movq   0x222f(%rip), %rsi        ; (void *)0x0000000104a7cd20: objc_release
//4.使用weak指针前,先retain
    callq  0x1044927bc               ; symbol stub for: objc_loadWeakRetained
//5.调用
    movq   0x335a(%rip), %rdi        ; "class"
//6.第四步retain了一次,所以要释放一次
    callq  0x1044927ce               ; symbol stub for: objc_release
//7.销毁weak指针
    callq  0x1044927aa               ; symbol stub for: objc_destroyWeak

上面代码总共7步,其中最为特别的是第4-6步,系统在使用weak指针指向的对象时,先调用了objc_loadWeakRetained, 然后在第6步 release。那说明使用weak先得强持有一下,然后才能使用。为甚系统要怎么做,不麻烦吗? 看完objc_loadWeakRetained之后就知道为什么了。

id objc_loadWeakRetained(id *location)
{
    id result;
    SideTable *table;
 retry:
//1.先取出weak指针指向的对象
    result = *location;
//2.对象为nil,表明weak指向nil了。
    if (!result) return nil; 
//3.如果指向对象不为空,取出容器
    table = &SideTables()[result];
//4.加锁,很关键,因为不同现成的操作会想想取值,甚至crash
    table->lock();
    if (*location != result) {
        table->unlock();
        goto retry;
    }
//5.读取值
    result = weak_read_no_lock(&table->weak_table, location);
    table->unlock();
    return result;
}

objc_loadWeakRetained分了五部分

  • 1.先取出weak指针指向的对象。
  • 2.空判断
  • 3.根据对象取出对象的SideTable
  • 4.SideTable加锁
  • 5.调用weak_read_no_lock,传入对象的SideTable的weak_table,和weak指针。读取location指向的对象

五步中重要的最后一步,调用weak_read_no_lock读取值,上面第一步已经得到了指向的对象,为什么还要查找weak_table?先留个疑问,我们看一下weak_read_no_lock干了什么

id weak_read_no_lock(weak_table_t *weak_table, id *referrer_id) 
{
    objc_object **referrer = (objc_object **)referrer_id;
    objc_object *referent = *referrer;
    if (referent->isTaggedPointer()) return (id)referent;

//1.查看referrer_id指针是否在weak_table中
    weak_entry_t *entry;
    if (referent == nil  ||  
        !(entry = weak_entry_for_referent(weak_table, referent))) 
    {
        return nil;
    }
//2.判断对象是否有自定义的release/retain方法,hasCustomRR(第一节备注过)
    if (! referent->ISA()->hasCustomRR()) {
        //3.ARC下是没有自定义的,所以调用rootTryRetain方法
        if (! referent->rootTryRetain()) {
            return nil;
        }
    }
    else {
            //ARC下不会调用这块代码,影响篇幅,所以删除
    }
    return (id)referent;
}

//tryRetain
inline bool  objc_object::rootTryRetain()
{
    assert(!UseGC);
    if (isTaggedPointer()) return true;
    return sidetable_tryRetain();
}

//sidetable_tryRetain
bool objc_object::sidetable_tryRetain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.indexed);
#endif
//1.取出SideTable
    SideTable& table = SideTables()[this];

    bool result = true;
//2.查找引用计数 “计数值”
    RefcountMap::iterator it = table.refcnts.find(this);
//3.如果根本没有存,表明是alloc,init第一次,retain成功
    if (it == table.refcnts.end()) {
        table.refcnts[this] = SIDE_TABLE_RC_ONE;
    }
//4.计数值和SIDE_TABLE_DEALLOCATING相等,表明正在执行dealloc方法。retain失败
 else if (it->second & SIDE_TABLE_DEALLOCATING) {
        result = false;
 }
//5.计数值没有超过一定范围,那么计数值增加SIDE_TABLE_RC_ONE,retain成功
else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
        it->second += SIDE_TABLE_RC_ONE;
    } 
    return result;
}

为了不影响篇幅我们删除了一些无用的代码。并且一下子给出了该过程的全部代码。
weak_read_no_lock三步

  • 1.先判定weak指针是否存在在weak指针执行的对象的weak_table中,如果没有,返回nil
  • 2.判断对象是否自定义过 retain/release方法,如果没有执行if中的语句
  • 3.ARC下对象时不能自定义releas和retain方法该方法的,所以调用对象的rootTryRetain(),该方法返回true,表明retain成功,然后就能在weak_read_no_lock返回weak指针指向的对象。

weak_read_no_lock最终又调用了tryretain,而tryretain又有5步调用

  • 1.取出对象的SideTable
  • 2.从SideTable根据对象取出引用计数计数值
  • 3.计数值为空,表明是第一次引用,肯定是alloc,init,所以引用计数数值为SIDE_TABLE_RC_ONE(1),retain成功
  • 4.计数值的值为SIDE_TABLE_DEALLOCATING,表明对象在执行dealloc,这个时候是不能引用的,,retain失败
  • 5.计数值没有超过范围,直接计数值增加SIDE_TABLE_RC_ONE(1),引用成功。

通过上面的过程,weak指针的使用过程OC饶了几个圈。为什么要这么做?我们考虑下下面的场景。
线程A 正在使用 weak指针指向的对象,正在调用方法,线程B突然释放了weak指针指向的对象,那么线程A可能用着用着就crash了。
为了防止这种情况出现,weak指针的使用一定要先拿到对象,并持有住,然后再使用。其次,系统还在持有weak指针指向的对象的时候,进行了合法性检验,判断了weak指针是否在表中和对象是否正在执行dealloc。

weak指针的每一句使用代码都会执行上面的全过程,也即是说objc_loadWeakRetained会多次调用,所以尽量避免使用weak,除非真正需要。

知道了weak指针存储和weak指针的使用,weak指针还差最后一个特性,那就是当weak指针指向的对象dealloc时weak指针会自动变为nil。

2.3dealloc

对象的dealloc方法,ARC出现之后一个陌生而又熟悉的函数,将所有的释放工作隐藏在此,下面我们要揭秘dealloc。

在ARC下,编译器会把对象的release方法编译成objc_release, objc_release我们在第1节提到了,objc_release最终的调用函数是objc_object::sidetable_release。

uintptr_t 
objc_object::sidetable_release(bool performDealloc)
{
//1.取出对象的table
    SideTable& table = SideTables()[this];
    bool do_dealloc = false;
    if (table.trylock()) {
//2.从table.refcnts 取出引用计数计数值
        RefcountMap::iterator it = table.refcnts.find(this);
//3.计数值没有找到,就直接将计数值更改为SIDE_TABLE_DEALLOCATING(正在执行dealloc),
        if (it == table.refcnts.end()) {
            do_dealloc = true;
            table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
        }
//4.计数值小于 SIDE_TABLE_DEALLOCATING(2),当然也小于SIDE_TABLE_RC_ONE(引用计数单位1,实际的值为4),那么计数值也标记为SIDE_TABLE_DEALLOCATING,准备执行dealloc
else if (it->second < SIDE_TABLE_DEALLOCATING) {
            // SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
            do_dealloc = true;
            it->second |= SIDE_TABLE_DEALLOCATING;
        }
//5.计数值没有超过SIDE_TABLE_RC_PINNED(就数值最大值),
else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
            it->second -= SIDE_TABLE_RC_ONE;
        }
        table.unlock();
        if (do_dealloc  &&  performDealloc) {
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
        }
        return do_dealloc;
    }

    return sidetable_release_slow(table, performDealloc);
}

我们看到objc_object::sidetable_release分了五部分做释放工作。

  • 1.取出对象的SideTable。
  • 2.以对象作为key从SideTable的refcnts取出引用计数计数值。
  • 3.计数值根本没有存,表明引用计数也为零,这个时候讲计数值标记为SIDE_TABLE_DEALLOCATING(正在执行dealloc),准备执行对象dealloc方法。
  • 4.计数值小于SIDE_TABLE_DEALLOCATIN,那计数值只能是标记为weak了。那么就给计数值的第1位标记1,即对象处于SIDE_TABLE_DEALLOCATING(正在执行dealloc),准备执行对象dealloc方法。
  • 5.计数值大于等于SIDE_TABLE_RC_ONE,并且合法,那么就引用计数减少一。并不执行对象dealloc方法。

那么objc_object::sidetable_release是真正做了release的工作,并且引用计数为0时,调用对象的dealloc方法。

既然这样,我们来看对象的dealloc方法做什么什么事情,我们先自定义一个对象的dealloc。

  #import "MyObject.h"
@implementation MyObject

- (void)dealloc
{
    
}
@end

编译之后

    0x10b4106fc <+28>: movq   0x2aad(%rip), %rsi        ; (void *)0x000000010b413280: MyObject
    0x10b410707 <+39>: movq   0x2a5a(%rip), %rsi        ; "dealloc"
    0x10b41070e <+46>: movq   %rax, %rdi
    0x10b410711 <+49>: callq  0x10b410978               ; symbol stub for: objc_msgSendSuper2
    0x10b410716 <+54>: addq   $0x20, %rsp
    0x10b41071a <+58>: popq   %rbp
    0x10b41071b <+59>: retq   

编译器增加了objc_msgSendSuper2,来调用父类的dealloc方法。最终会调用到NSObject的dealloc方法,自定义的dealloc方法做了什么,开发人员自己会知道,但是NSObject做了什么事情,我们需要做一个解释。

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

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

inline void objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;
    object_dispose((id)this);
}

id  object_dispose(id obj)
{
    if (!obj) return nil;
    objc_destructInstance(obj);
#if SUPPORT_GC
    if (UseGC) {
        auto_zone_retain(gc_zone, obj); // gc free expects rc==1
    }
#endif
    free(obj)
    return nil;
}

//objc_destructInstance dealloc的重头戏
void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = !UseGC && obj->hasAssociatedObjects();
        bool dealloc = !UseGC;

        // This order is important.
        //1.如果类存在c++给析构函数,就执行C++析构函数
        if (cxx) object_cxxDestruct(obj);
        //2.移除和释放通过runtime关联到对象升上的关联对象(一般分类的属性会通过runtime关联进来)
        if (assoc) _object_remove_assocations(obj);
        //3.清除对象的SideTable的weak_table
        if (dealloc) obj->clearDeallocating();
    }
    return obj;
}

inline void  objc_object::clearDeallocating()
{
    sidetable_clearDeallocating();
}

void  objc_object::sidetable_clearDeallocating()
{
    SideTable& table = SideTables()[this];

    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    table.lock();
    //1.从对象的SideTable的refcnts取出引用计数计数值
    RefcountMap::iterator it = table.refcnts.find(this);
   //2.计数值存在,并且计数值第0位是1,表明有weak指针指向,这个时候采取调用weak_clear_no_lock清楚weak_table,这块表明计数值第0位的标记是有用的
    if (it != table.refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table.weak_table, (id)this);
        }
       //4.计数值从refcnts移除,可以扔掉,再也不用了。
        table.refcnts.erase(it);
    }
    table.unlock();
}

调用栈入下
dealloc
|__ _objc_rootDealloc
|______ objc_object::rootDealloc()
|_________ object_dispose
|_____________ objc_destructInstance
|_________________object_cxxDestruct
|_________________object_remove_assocations
|_________________sidetable_clearDeallocating

长了点,但是重点在objc_destructInstance和sidetable_clearDeallocating
首先objc_destructInstance有三个重要事情。

  • 1.查看对象是否有_cxxDestruct方法,如果有就调用。(object_cxxDestruct一个古怪的东西,后面会讲)
  • 2.释放通过runtime增加给对象的关联对象,比如我们在分类中常用的objc_setAssociatedObject方法增加的对象。
  • 3.释放对象的weak表,清除完weak表后,weak表中的指针就都会变为nil,weak原理的最后一个谜团完全解开。

其次sidetable_clearDeallocating,是实际的清除weak表的过程,分了3步分

  • 1.从对象的SideTable的refcnts取出引用计数计数值
  • 2.如果计数值存在,并且计数值的第0位为1,表明对象有weak指针指向,那就调用weak_clear_no_lock清除weak表,我们发现前面说个的计数值的第0位标记是有用的,加快了对象的dealloc.
  • 3.最后一步,对象消失了,计数值没有用了,从表中移除。

我们还差一个object_cxxDestruct,一个古怪的方法,到底干了什么?
上面的过程中,我们貌似没有说对象的属性释放的地方,例如

@interface MyObject1 : MyObject
@property (nonatomic, strong) MyObject *next;
@end

MyObject1在释放的时候应该释放next指的对象,会不会object_cxxDestruct就是来做这件事的
我们对如下代码做了debug,

{
    MyObject1  *weakObj = [[MyObject1 alloc] init];
    weakObj.next = [[MyObject alloc] init];
}

@implementation MyObject1
- (void)dealloc
{
}
@end

发现weakObj指向的对象执行dealloc时堆栈中有一个cxx_destruct的调用

debugdealloc.png

在图中我们发现,weakObj (MyObject1)调用dealloc时,第二行出现了cxx_destruct,并且2-6行的调用栈和我们上面讲的一模一样。黄色框中我们发现有objc_object::sidetable_release的调用,并且MyObject的dealloc也出现了。那么事实出来了,cxx_destruct是用来释放weakObj (MyObject1)中的用属性方式定义的next (MyObject),cxx_destruct用来释放类的属性。

OC通过给对象增加一张weak表来存放所有指向当前对象的weak指针。当对象的dealloc调用时清除该表。当weak指针自己要消失或者值发生变化的时候,也会对该表进行更改

weak表存储结构

同时ARC下 NSObject dealloc分了三部分来做对象的真正内存释放,其中最后一步就是清楚weak表。来达到weak自动变为nil的操作。
dealloc flow

3.__autoreleasing修饰符

__autoreleasing比较简单,就是把对象的持有放入到一个“释放池”中,释放池对对象进行一次持有,当释放池做清理时干掉这些持有。

  - (void)test
{
    id __autoreleasing arObj =  [[NSObject alloc] init];
}
movq   0x2861(%rip), %rsi        ; (void *)0x00000001069fee58: NSObject
movq   0x283a(%rip), %rdi        ; "alloc"
callq  0x106060b54               ; symbol stub for: objc_msgSend
movq   0x282b(%rip), %rsi        ; "init"
callq  0x106060b54               ; symbol stub for: objc_msgSend
callq  0x106060b3c               ; symbol stub for: objc_autorelease

编译之后代码比较简单,没有其他的只有一个 objc_autorelease, objc_autorelease干了这么一件事情,代码如下

id objc_autorelease(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;

    return obj->autorelease();
}
 //
inline id objc_object::autorelease()
{
    // UseGC is allowed here, but requires hasCustomRR.
    assert(!UseGC  ||  ISA()->hasCustomRR());

    if (isTaggedPointer()) return (id)this;
    if (! ISA()->hasCustomRR()) return rootAutorelease();
    return ((id(*)(objc_object *, SEL))objc_msgSend)(this, SEL_autorelease);
}

inline id  objc_object::rootAutorelease()
{
    assert(!UseGC);

    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}

id objc_object::rootAutorelease2()
{
    assert(!isTaggedPointer());
    return AutoreleasePoolPage::autorelease((id)this);
}
static inline id autorelease(id obj)
{
    assert(obj);
    assert(!obj->isTaggedPointer());
    id *dest __unused = autoreleaseFast(obj);
    assert(!dest  ||  *dest == obj);
    return obj;
}

static inline id *autoreleaseFast(id obj)
{
    AutoreleasePoolPage *page = hotPage();
    if (page && !page->full()) {
         return page->add(obj);
    } else if (page) {
         return autoreleaseFullPage(obj, page);
    } else {
         return autoreleaseNoPage(obj);
    }
}

static __attribute__((noinline))
id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page)
{
    // The hot page is full. 
   // Step to the next non-full page, adding a new page if necessary.
   // Then add the object to that page.
   assert(page == hotPage());
   assert(page->full()  ||  DebugPoolAllocation);
   do {
       if (page->child) page = page->child;
       else page = new AutoreleasePoolPage(page);
   } while (page->full());
      setHotPage(page);
      return page->add(obj);
   }

static __attribute__((noinline))
id *autoreleaseNoPage(id obj)
{
   // No pool in place.
   assert(!hotPage());
   if (obj != POOL_SENTINEL  &&  DebugMissingPools) {
         // We are pushing an object with no pool in place, 
         // and no-pool debugging was requested by environment.
         _objc_inform("MISSING POOLS: Object %p of class %s "
                         "autoreleased with no pool in place - "
                         "just leaking - break on "
                         "objc_autoreleaseNoPool() to debug", 
                         (void*)obj, object_getClassName(obj));
         objc_autoreleaseNoPool(obj);
         return nil;
   }

   // Install the first page.
   AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
   setHotPage(page);
  // Push an autorelease pool boundary if it wasn't already requested.
  if (obj != POOL_SENTINEL) {
      page->add(POOL_SENTINEL);
  }
  // Push the requested object.
  return page->add(obj);
}

调用栈有点长,但不难看懂,按照我们上面说的,肯定有一个容器来存储持有这些autoreleaseing对象,那么上面代码中体现出一个东西,就是AutoreleasePoolPage。autoreleaseFast逻辑很清晰,分三步

  • 1.先获取当前的释放池,如果释放池没满,就直接加入进去
  • 2.如果满了,并且释放池能获取到,如果当前释放池有子释放池,就将对象加入自释放池,如果没有,就新创建一个。
  • 3.如果当前全系统没有释放池,那么就直接创建一个。

上面三步有几个疑问,释放池,AutoreleasePoolPage,子释放池,都是什么意思,系统的释放池是如何建立的。好,我们先来看一下AutoreleasePoolPage定义

    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
  1. AutoreleasePoolPage的定义结构中,对于其他对象的持有使用的是next,AutoreleasePoolPage在创建时申请了一块连续区间,类似一个数组,每一个数组都是一个指针,每当有对象要持有的时候,会调用其add 方法,add方法定义如下
    id *add(id obj)
    {
        assert(!full());
        unprotect();
//重点关注
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
//
        protect();
        return ret;
    }

这段代码清晰的说明了持有过程。

2.除了AutoreleasePoolPage的持有以外,系统将释放池设计成一个树状结构,但是这个树状结果比较特殊,每一个AutoreleasePoolPage只有一个子节点。类似于双向链表。

AutoreleasePoolPage
AutoreleasePoolPage
AutoreleasePoolPage
在销毁释放池的时候,会从销毁的释放池开始,销毁起儿子节点,孙子节点,直到没有。
问题来了,为什么要建立一个这样的结构,而且每个释放池清理要清理器子节点。我们看看我们编程的环境,看如下代码

- (void)test
{
    id __autoreleasing arObj =  [[NSObject alloc] init];
//pool 1
    @autoreleasepool {
        id __autoreleasing arObj1 =  [[NSObject alloc] init];
//pool 2
        @autoreleasepool {
            id __autoreleasing arObj2 =  [[NSObject alloc] init];
        }
//pool 3
        @autoreleasepool {
            id __autoreleasing arObj3 =  [[NSObject alloc] init];
        }
        id __autoreleasing arObj4 =  [[NSObject alloc] init];
    }
}

可以看到上面代码有三个释放池,每个释放池都有特定作用域,所以我们的需求要达到以下几点
1.每一个释放池管辖特定作用域内的对象。
2.最顶层的@autoreleasepool 出了作用域,内部的@autoreleasepool 也应该出作用域,也就是最顶层释放了,最内部的也得释放。
介于上面两个需求,我们猜测AutoreleasePoolPage分了多个,并且有双向链表结构是为了管控多个作用域的自动释放,并且释放池之间的父子关系和代码作用域的关系一样。
介于此,我们把上面的代码编译一下,编译器干什么事情

    movq   0x2981(%rip), %rsi        ; (void *)0x000000010c889e58: NSObject
    movq   0x295a(%rip), %rdi        ; "alloc"
    callq  0x10beebb54               ; symbol stub for: objc_msgSend
    movq   0x294b(%rip), %rsi        ; "init"
    callq  0x10beebb54               ; symbol stub for: objc_msgSend
    callq  0x10beebb3c               ; symbol stub for: objc_autorelease

//pool 1
    callq  0x10beebb48               ; symbol stub for: objc_autoreleasePoolPush
    0x2943(%rip), %rsi        ; (void *)0x000000010c889e58: NSObject
    movq   0x291c(%rip), %rdi        ; "alloc"
    callq  0x10beebb54               ; symbol stub for: objc_msgSend
    movq   0x2909(%rip), %rsi        ; "init"
    callq  0x10beebb54               ; symbol stub for: objc_msgSend
    callq  0x10beebb3c               ; symbol stub for: objc_autorelease

//pool 2
    callq  0x10beebb48               ; symbol stub for: objc_autoreleasePoolPush
    movq   0x2901(%rip), %rsi        ; (void *)0x000000010c889e58: NSObject
    movq   0x28da(%rip), %rdi        ; "alloc"
    callq  0x10beebb54               ; symbol stub for: objc_msgSend
    movq   0x28c7(%rip), %rsi        ; "init"
    callq  0x10beebb54               ; symbol stub for: objc_msgSend
    callq  0x10beebb3c               ; symbol stub for: objc_autorelease
        //pool 2释放
    callq  0x10beebb42               ; symbol stub for: objc_autoreleasePoolPop
//pool 3
    callq  0x10beebb48               ; symbol stub for: objc_autoreleasePoolPush
    movq   0x28b6(%rip), %rsi        ; (void *)0x000000010c889e58: NSObject
    movq   0x288f(%rip), %rdi        ; "alloc"
    callq  0x10beebb54               ; symbol stub for: objc_msgSend
    movq   0x287c(%rip), %rsi        ; "init"
    callq  0x10beebb54               ; symbol stub for: objc_msgSend
    callq  0x10beebb3c               ; symbol stub for: objc_autorelease
        //pool 3释放
    callq  0x10beebb42               ; symbol stub for: objc_autoreleasePoolPop
    movq   0x2870(%rip), %rax        ; (void *)0x000000010c889e58: NSObject
    movq   0x2849(%rip), %rsi        ; "alloc"
    callq  0x10beebb54               ; symbol stub for: objc_msgSend
    movq   0x2842(%rip), %rsi        ; "init"
    callq  0x10beebb54               ; symbol stub for: objc_msgSend
    callq  0x10beebb3c               ; symbol stub for: objc_autorelease

//pool 1释放
    callq  0x10beebb42               ; symbol stub for: objc_autoreleasePoolPop

编译后出现了几个新函数objc_autoreleasePoolPush,objc_autoreleasePoolPop,这两个是成对出现,和@ autoreleasepool的出现顺序一样,objc_autoreleasePoolPush表明新pool的开始,objc_autoreleasePoolPop当前结束。
Ok,最后们再看一下关于objc_autoreleasePoolPush和objc_autoreleasePoolPop的具体实现,

//释放池开始
void * objc_autoreleasePoolPush(void)
{
    if (UseGC) return nil;
    return AutoreleasePoolPage::push();
}
//释放池开始实际调用
static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_SENTINEL);
        } else {
            dest = autoreleaseFast(POOL_SENTINEL);
        }
        assert(*dest == POOL_SENTINEL);
        return dest;
    }

//释放池退出调用
void objc_autoreleasePoolPop(void *ctxt)
{
    if (UseGC) return;
    AutoreleasePoolPage::pop(ctxt);
}
//释放池退出实际调用
static inline void pop(void *token) 
    {
        AutoreleasePoolPage *page;
        id *stop;

        page = pageForPointer(token);
        stop = (id *)token;
        if (DebugPoolAllocation  &&  *stop != POOL_SENTINEL) {
            // This check is not valid with DebugPoolAllocation off
            // after an autorelease with a pool page but no pool in place.
            _objc_fatal("invalid or prematurely-freed autorelease pool %p; ", 
                        token);
        }

        if (PrintPoolHiwat) printHiwat();

        page->releaseUntil(stop);

        // memory: delete empty children
        if (DebugPoolAllocation  &&  page->empty()) {
            // special case: delete everything during page-per-pool debugging
            AutoreleasePoolPage *parent = page->parent;
            page->kill();
            setHotPage(parent);
        } else if (DebugMissingPools  &&  page->empty()  &&  !page->parent) {
            // special case: delete everything for pop(top) 
            // when debugging missing autorelease pools
            page->kill();
            setHotPage(nil);
        } 
        else if (page->child) {
            // hysteresis: keep one empty child if page is more than half full
            if (page->lessThanHalfFull()) {
                page->child->kill();
            }
            else if (page->child->child) {
                page->child->child->kill();
            }
        }
    }

从上面代码可以看出,每个释放池开始时都会调用autoreleaseNewPage创建一个新的,并且成为当前释放池的child (自释放池),当释放池结束,会调用objc_autoreleasePoolPop,起传入参数就是objc_autoreleasePoolPush所创建的释放池对象。 (代码中的其他调用不一一解释了,太多,大家可以去看源码)

最后 我们的app最开头的main函数中

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

这个中出现的autoreleasepool为root 释放池。

4. __unsafe_unretained修饰符

__unsafe_unretained这个修饰符我们比较少的用到,因为从表面上看起就是不安全的,因为他不持有对象,对象释放了,再次使用它就会crash。老规矩,编译前后代码如下

- (void)test
{
    id __unsafe_unretained usObj =  [[NSObject alloc] init];
}
movq   0x2859(%rip), %rsi        ; (void *)0x000000010b809e58: NSObject
movq   0x2832(%rip), %rdi        ; "alloc"
callq  0x10ae6bb5e               ; symbol stub for: objc_msgSend
movq   0x2823(%rip), %rsi        ; "init"
callq  0x10ae6bb5e               ; symbol stub for: objc_msgSend
callq  0x10ae6bb6a               ; symbol stub for: objc_release

可以看出没有任何多余持有过程,表明__unsafe_unretained是不持有。和weak相比,系统也不会对其进行置nil。所以对象消失后,再次访问就要尴尬了。

那么到底有什么用?
相比weak, __unsafe_unretained在自定义的dealloc调用时,是可以访问其地址的,并且不会crash。 所以如果有这样的需求,那__unsafe_unretained可以派上用场。

以上就是基本的四个修饰符的详细解释。

4.盲区

平时开发中,我们往往忽略函数传值和返回值的对象持有方式,现在我们来看看

- (__weak id)test:(__strong id)s1 w1:(__weak id)w1 a:(__autoreleasing id)a1 u:(__unsafe_unretained id)us1
{
    return [[NSObject alloc] init];
}

编译后的代码如下

  //传值s1,strong
    callq  0x1010b0ace               ; symbol stub for: objc_storeStrong
  //传值w1,weak
    callq  0x1010b0aaa               ; symbol stub for: objc_initWeak
    movq   0x2a99(%rip), %rdi        ; (void *)0x0000000101a50e58: NSObject
    movq   0x2a5a(%rip), %rsi        ; "alloc"
    movq   0x1bbb(%rip), %r8         ; (void *)0x000000010169bac0: objc_msgSend
    jmp    0x1010b0475               ; <+117> at ViewController.m:51
    movq   0x2a44(%rip), %rsi        ; "init"
    movq   0x1b9d(%rip), %rax        ; (void *)0x000000010169bac0: objc_msgSend
    jmp    0x1010b0492               ; <+146> at ViewController.m:51
  //销毁weak
    callq  0x1010b0a9e               ; symbol stub for: objc_destroyWeak
  //销毁strong
    callq  0x1010b0ace               ; symbol stub for: objc_storeStrong
  //返回值使用autorelease持有
     jmp    0x1010b0a98               ; symbol stub for: objc_autoreleaseReturnValue
    callq  0x1010b0a9e               ; symbol stub for: objc_destroyWeak
    movq   -0x38(%rbp), %rdi

通过上面的代码看出,函数传值和普通的变量持有没什么大的区别,过程分为两个部分
1.函数调用先对参数从前到后一一做初始化持有。
2.返回值使用autorelease持有后返回。
但我们发现了来个问题
1.传值使用autoreleasing的传值,系统并没有做任何事情,反而和__unsafe_unretained一模一样。
2.使用weak传参,在函数返回之后又来了一次objc_destroyWeak。
这个两个目前还不清楚什么要这么做

总结

本片主要讲了ARC下内存管理的原理,以及常用修饰符的作用原理。

  • 1.OC通过给每一个对象增加一个SideTable表,来完成内存管理。SideTable中包含了引用计数就数值。
  • 2.SideTable中的weak_table_t作为存储所有指向对象的weak指针,OC通过操作这个weak_table_t来完成weak持有。
  • 3.__autoreleasing修饰符,系统增加了释放池,对__autoreleasing的对象进行持有。释放池成双向链表结构。满足不同作用域的需求。
  • 4.函数调用前,对函数参数进行一次初始化赋值。函数调用结束,返回值会使用autorelease进行持有。

更新
最新源码地址 https://opensource.apple.com/source/objc4/objc4-706

你可能感兴趣的:(iOS objective-c runtime之内存管理)