原文出处:iOS_我们的对象会经历什么

文章是对最近一次技术分享 - 关于对象从创建到销毁的过程探究的整理.

@autoreleasepool
    {
        NSObject (__strong) *object = [[NSObject alloc]init];
    }

全文探讨基本上是对上面这句代码的说明.

目录

一. NSObject
二. alloc
三. init
四. 所有权修饰符
五. autoreleasepool
六. dealloc

一. NSObject

1. NSObject内存结构

由于内存分配会涉及到对象的结构,因此首先对对象的结构加以说明,如图:

NSObject结构图

实例对象的isa指针指向类对象.而根据runtime源码,我们可以看到类objc_class是继承自objc_object的,本质上,类是一个对象,这也就是我们通常会说的'类对象'的缘由.
其中objc_object有一个唯一的私有变量 - isa_t类型的isa指针,objc_class的定义中还包括了:

2. isa_t isa

union isa_t 
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
##if __arm64__
#### define ISA_MASK        0x0000000ffffffff8ULL
#### define ISA_MAGIC_MASK  0x000003f000000001ULL
#### define ISA_MAGIC_VALUE 0x000001a000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;//对象含有或者曾经含有关联引用,没有关联引用的可以更快地释放内存
        uintptr_t has_cxx_dtor      : 1;//析构器方法,如果没有析构器就会快速释放内存
        uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
        uintptr_t magic             : 6;//用于调试器判断当前对象是真的对象还是没有初始化的空间
        uintptr_t weakly_referenced : 1;//对象被指向或者曾经指向一个 ARC 的弱变量,没有弱引用的对象可以更快释放
        uintptr_t deallocating      : 1;//对象正在释放内存
        uintptr_t has_sidetable_rc  : 1;//对象的引用计数太大了,存不下
        uintptr_t extra_rc          : 19;//对象的引用计数超过 1,会存在这个这个里面
####     define RC_ONE   (1ULL<<45)
####     define RC_HALF  (1ULL<<18)
    };
##elif __x86_64__
#### define ISA_MASK        0x00007ffffffffff8ULL
#### define ISA_MAGIC_MASK  0x001f800000000001ULL
#### define ISA_MAGIC_VALUE 0x001d800000000001ULL
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 44; // MACH_VM_MAX_ADDRESS 0x7fffffe00000
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 8;
####     define RC_ONE   (1ULL<<56)
####     define RC_HALF  (1ULL<<7)
    };
##else
#### error unknown architecture for packed isa
##endif
// SUPPORT_PACKED_ISA
#endif
#if SUPPORT_INDEXED_ISA
##if  __ARM_ARCH_7K__ >= 2
#### define ISA_INDEX_IS_NPI      1
#### define ISA_INDEX_MASK        0x0001FFFC
#### define ISA_INDEX_SHIFT       2
#### define ISA_INDEX_BITS        15
#### define ISA_INDEX_COUNT       (1 << ISA_INDEX_BITS)
#### define ISA_INDEX_MAGIC_MASK  0x001E0001
#### define ISA_INDEX_MAGIC_VALUE 0x001C0001
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t indexcls          : 15;
        uintptr_t magic             : 4;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 7;
####     define RC_ONE   (1ULL<<25)
####     define RC_HALF  (1ULL<<6)
    };
##else
#### error unknown architecture for indexed isa
##endif
// SUPPORT_INDEXED_ISA
#endif
};

以上为isa指针的定义,可以看到isa指针是一个联合体,在不同环境下,结构体中属性值稍有差别.

其中

3. class_data_bits_t bits

限于篇幅,不再展示完整的定义(objc4-706,objc-runtime-new.h文件,844-1056行),可参考前文图示,标识了几个主要属性.

由源码可知,类结构中bits属性是一个叫做class_data_bits_t的结构体,包含Bits相关的set,getclear私有属性,和data等相关的公开属性,下面对公开属性做简单说明:

二. alloc

1.alloc流程

alloc简化调用堆栈

alloc方法完整的执行过程包括非常多的判断和跳转,这里做了简化.
当我们写了一个[Class alloc]命令,会相应的调用_objc_rootAlloc方法,最终会执行_class_createInstanceFromZone方法,如下:

static __attribute__((always_inline)) 
id
_class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, 
                              bool cxxConstruct = true, 
                              size_t *outAllocatedSize = nil)
{
    if (!cls) return nil;
    assert(cls->isRealized());
    //1. Read class's info bits all at once for performance
    bool hasCxxCtor = cls->hasCxxCtor();
    bool hasCxxDtor = cls->hasCxxDtor();
    bool fast = cls->canAllocNonpointer();
    //2. 
    size_t size = cls->instanceSize(extraBytes);
    if (outAllocatedSize) *outAllocatedSize = size;
    //3. 
    id obj;
    if (!zone  &&  fast) {
        obj = (id)calloc(1, size);
        if (!obj) return nil;
        obj->initInstanceIsa(cls, hasCxxDtor);
    } 
    else {
        if (zone) {
            obj = (id)malloc_zone_calloc ((malloc_zone_t *)zone, 1, size);
        } else {
            obj = (id)calloc(1, size);
        }
        if (!obj) return nil;
        // Use raw pointer isa on the assumption that they might be 
        // doing something weird with the zone or RR.
        obj->initIsa(cls);
    }
    //4. 
    if (cxxConstruct && hasCxxCtor) {
        obj = _objc_constructOrFree(obj, cls);
    }
    return obj;
}
uint32_t unalignedInstanceSize() {
        assert(isRealized());
        return data()->ro->instanceSize;
    }
    uint32_t alignedInstanceSize() {
        return word_align(unalignedInstanceSize());
    }
    size_t instanceSize(size_t extraBytes) {
        size_t size = alignedInstanceSize() + extraBytes;
        if (size < 16) size = 16;
        return size;
    }

也就是说会在类的ro属性instanceSize中读取,然后经过对齐后返回.

这里要补充的有两点:
1). 对象的内存地址和大小是在编译阶段就确定了的,编译阶段存在的内容会存储在类结构data中,而此时访问data,则为ro,在运行时,系统会将ro中的 数据赋值到rw中,并重新设置对象的内存结构,此时访问data,则为rw.
2). ' CF requires all objects be at least 16 bytes.'
所有的对象的大小都必须大于或等于 16 字节。

id _objc_constructOrFree(id bytes, Class cls)
{
    assert(cls->hasCxxCtor());  // for performance, not correctness
    id obj = object_cxxConstructFromClass(bytes, cls);
    if (!obj) free(bytes);
    return obj;
}
id 
object_cxxConstructFromClass(id obj, Class cls)
{
    assert(cls->hasCxxCtor());  
    id (*ctor)(id);
    Class supercls;
    supercls = cls->superclass;
// 1 调用父类构造方法
    if (supercls  &&  supercls->hasCxxCtor()) {
        bool ok = object_cxxConstructFromClass(obj, supercls);
        if (!ok) return nil;  
    }
//2 调用自身构造方法
    ctor = (id(*)(id))lookupMethodInClassAndLoadCache(cls, SEL_cxx_construct);
    if (ctor == (id(*)(id))_objc_msgForward_impcache) return obj;  
    if ((*ctor)(obj)) return obj;  
//3 失败,清理
    if (supercls) object_cxxDestructFromClass(obj, supercls);
    return nil;
}

简单来说,如果当前对象存在构造方法,则会依次调用父类和自身的构造方法,成功后返回对象,如果失败,则调用父类的析构方法来清理并返回nil.
这里提到的构造函数便是SEL_cxx_construct.至于该方法具体是什么,我们稍后再提.

Note

另外,要提醒大家使用Xcode自动跳转时要仔细一点,在runtime源码中有非常多的文件涉及到old版本和new版本.我在再次查阅过程中才意识到之前被引导到 old版本,这个部分内容之前存在非常多问题..觉得脸好疼..
希望大家不要犯同样的错误.

2. malloc

这个小节属于扩展,不了解也没关系.
上一部分我们提到了calloc方法,而该方法会最终走到libsystem_malloc.dylibmalloc方法来分配内存.
这里做以简单说明,更多内容可参考这篇文章.

malloc内存分配基于malloc zone,并将基于大小分为nano、tiny、small、large四种类型,申请时按需分配.具体信息如下图:

malloc zone

malloc在初次调用时,会分配一个default zone和一个scalable zone作为辅助,在64位环境下,default zone为nano zone,负责分配nano大小,scalable zone负责tiny、small和large内存的分配.

三. init

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

init方法就简单了,只是返回当前对象,并没有其他多余操作.

2.16 补充
在巧哥的谈ObjC对象的两段构造模式这篇文章中提到:
"当我们通过 alloc 或 allocWithZone 方法创建对象时,cocoa 会返回一个未” 初使化 “过的对象。在这个过程中,cocoa除了上面提到的申请了一块足够大的内存外,还做了以下 3 件事:
将该新对象的引用计数 (Retain Count) 设置成 1。
将该新对象的 isa 成员变量指向它的类对象。
将该新对象的所有其它成员变量的值设置成零。(根据成员变量类型,零有可能是指 nil 或 Nil 或 0.0)"
但是这篇文章发表于2013-01-13,所以我觉得这部分内容可能已经不适用了,但是可以帮助我们了解alloc与init两段构造模式是由于历史原因以及Single Responsibility设计原则.

四. 所有权修饰符

该部分通过product -> Perform Action -> Assemble追踪,因为对汇编代码知之甚少,只观察内存管理相关关键词(扶额.png)..

1. __strong

1.1 对象持有自己

利用alloc/new/copy/mutableCopy生成对象,此处以alloc为例:

NSDictionary *dic = [[NSDictionary alloc]init];

我们声明了一个dic对象,相应的会调用:

id dic = objc_msgSend(NSDictionary, @selector(alloc));
objc_msgSend(obj,selector(init));
objc_storeStrong(dic);

也就是在ARC环境下,自动插入_objc_storeStrong命令,那么这个命令做了什么呢?

void
objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

也就是在值发生变化时,retain新值,并release旧值.

1.2 对象不持有自己

利用非alloc/new/copy/mutableCopy生成对象,如下:

NSDictionary *dic = [NSDictionary dictionary];

相应的会调用

id dic = objc_msgSend(NSDictionary, @selector(dictionary));
objc_objc_retainAutoreleasedReturnValue(dic);
objc_storeStrong(dic);

在非持有关系中,除了objc_storeStrong命令,还多了一个objc_objc_retainAutoreleasedReturnValue, objc_objc_retainAutoreleasedReturnValueautoreleasepool的一个内存优化命令,后面autore leasepool部分具体说明.

2. __weak

__weak通常用来解决循环引用的问题.如下示例代码:

id  __weak  obj1  =  obj;

相应的会调用:

id obj ;
objc_initWeak(&obj1,obj);
objc_destoryWeak(&obj1);

runtime源码中查看,会发现 objc_initWeakobjc_initWeak都是对objc_storeWeak函数的封装,那么 objc_storeWeak都做了什么呢?做了简化后如下:

storeWeak(id *location, objc_object *newObj)
{
    SideTable *oldTable;
    SideTable *newTable;
    ...
    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);
        *location = (id)newObj;
   }

忽略掉繁琐的操作后,也就是清除旧值,并设置新值.
另外,我们注意到这里的SideTableweak_table.这两个就是存储弱引用的表了,那么具体呢?

SideTable结构图

如图,SideTable是一个散列表,包含三个属性:

struct weak_entry_t {
    DisguisedPtr<objc_object> referent;//被引用的对象
    union {
        struct {
            weak_referrer_t *referrers;//The address of a __weak variable.
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];//4
        };
    };
    bool out_of_line() {
        return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
    }
    weak_entry_t& operator=(const weak_entry_t& other) {
        memcpy(this, &other, sizeof(other));
        return *this;
    }
    weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
        : referent(newReferent)
    {
        inline_referrers[0] = newReferrer;
        for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
            inline_referrers[i] = nil;
        }
    }
};

其中referent是被引用对象(key),也就是示例代码中的obj,下面的union即存储了所有指向该对象的弱引用,其中referrers 存储所有的弱引用对象的地址(value),且当引用少于4时,hash表被一个数组所代替。

在hash表中,赋值对象的内存地址作为键值key,第一个参数__weak修饰的属性变量的内存地址作为value存储.
具体来说,初始化时,objc_initWeak(&obj1,obj)将执行objc_storeWeak(&obj1, obj);,将被引用对象ob j地址作为key值,将当前引用对象obj1作为value存储,而当obj引用计数为0时,objc_destoryWeak(&obj1)函数会执 行objc_storeWeak(&obj1,0),把变量obj1的地址从 weak 表中删除.

涉及到SideTable,那么我们可以顺便了解一下retain releaseretainCount了.

retain

retain操作实现如下:

id objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];
    table.lock();
    size_t& refcntStorage = table.refcnts[this];
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    return (id)this;
}

可以看到,retain操作是读取当前对象的SideTablerefcnts属性,如果没有越界,会将其增加SIDE_TABLE_RC_ONE(1UL<<2),而不仅仅是我们熟知的1,这由于引用计数的第一位用来表示计数是否越界,后两位分别被弱引用以及析构状态两个标识位占领.

retainCount

在说release之前,先说一下retainCount的实现.

#define SIDE_TABLE_RC_SHIFT 2
uintptr_t objc_object::sidetable_retainCount()
{
    SideTable& table = SideTables()[this];
    size_t refcnt_result = 1;
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it != table.refcnts.end()) {
        // this is valid for SIDE_TABLE_RC_PINNED too
        refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
    }
    table.unlock();
    return refcnt_result;
}

sidetable_retainCount()函数的实现,印证了对象的引用计数存在在SideTable中的想法,而且引用计数总是返回1+table.refcnts.这也是为什么我们有时候会听到有人说访问到的引用计数不正确.

release
#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))
uintptr_t  objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    assert(!isa.nonpointer);
#endif
    SideTable& table = SideTables()[this];
    bool do_dealloc = false;
    table.lock();
    RefcountMap::iterator it = table.refcnts.find(this);
    if (it == table.refcnts.end()) {
        do_dealloc = true;
        table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
    } else if (it->second < SIDE_TABLE_DEALLOCATING) {
        // SIDE_TABLE_DEALLOCATING 可作为计数是否为0的判断
        do_dealloc = true;
        it->second |= SIDE_TABLE_DEALLOCATING;
    } 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;
}

release操作,首先读取当前对象SideTablerefcnts属性,然后对计数加以判断,分类处理:

3. unsafe_unretained

现在已经很少显式声明unsafe_unretained属性的变量,但值得注意的是,在ARC环境下,self并不会被retainrelease,它其实是unsafe_unretained,生命周期全由它的调用方来保证.

clang文档关于self的说明

关于这一点,在苹果clang文档中有说明,另外也可以参考孙源的这篇文章.

4. _autoreleasing

_autoreleasing属性,是将对象放入autoreleaspool中,会在以下几种case出现:

其中,当调用方给方法参数传递参数时,默认是strong类型,而接收方方法的参数是_autoreleasing,为什么没有出现类型不匹配错误呢? 这是因为系统自动插入了临时转换命令,将调用方的strong对象参数转换为_autoreleasing临时变量传递给接收方.而当参数是__weak类型时,也类似,会讲__weak变量转换为_autoreleasing作为入参.

而加入autoreleaspool,具体发生了什么呢?

五. autoreleasepool

autoreleasepool追踪

通过clang重新文件之后,可以看到autoreleasepool被转换为两个命令objc_autoreleasePoolPushobjc_auto releasePoolPop,而查阅源码,会发现,这两个命令都是基于AutoreleasePoolPage的封装.

class AutoreleasePoolPage 
{
    ...
    magic_t const magic;//用来校验AutoreleasePoolPage的结构是否完整
    id *next;//游标,指向栈顶最新add进来的对象的下一个位置
    pthread_t const thread;//当前线程, thread:pool = 1:n
    AutoreleasePoolPage * const parent;//parent指针与child构成双向链表
    AutoreleasePoolPage *child;//
    uint32_t const depth;//深度
    ...
}

1. push

继续跟踪push方法,其流程如下:

push - autoreleaseFast流程图

首先会判断类别,如果是需要每个pool都生成一个新page,即DebugPoolAllocation为真,则执行autoreleaseNewPage方法,否则,执行autoreleaseFast方法.

autoreleaseFast方法中,如果存在page且未满,则直接添加;
如果不存在page,会响应autoreleaseNoPage;
如果当前page已满,则响应autoreleaseFullPage方法;
autoreleaseNoPageautoreleaseFullPage会生成新的page,然后向该page中添加对象.

autoreleaseNewPage方法,如果当前存在page,则执行autoreleaseFullPage方法,否则响应autorelea seNoPage方法,然后就同上了,去执行添加方法.

具体如何添加呢?

push add 示意图

在一个page中,除了存储page实例本身的数据空间,都用来存储加入pool的对象.
当插入对象时,会在next指针位置首先插入一个POOL_BOUNDARY对象(一个nil对象)作为标示,然后添加实例对象,并在末尾处更新next指针,使其指向下一个add进来的对象的位置,也就是下一个POOL_BOUNDARY对象了,然后返回插入哨兵对象的地址.

2. pop

收到pop命令后,首先会根据push操作返回的POOL_BOUNDARY哨兵对象的地址获取到当前page,然后在当前page中,将晚于哨兵对象添 加的对象都发送一次release命令,并更新next指针位置,最后kill掉空page,示意如下:

pop release示意图

一个曾经困扰我的问题是:
既然一个线程可以对应多个pool,而pool可以嵌套并且是可以跨page的,那么,当只需要销毁内层pool中的对象时,会发生什么呢?
这个问题在意识到,pool的销毁是在线程结束时发生的,因此即使有多个pool,也会在同一时间销毁,不存在只销毁内层的状态.???

3. TLS

在追踪push的时候,注意到源码中对EMPTY_POOL_PLACEHOLDER的一段注释:

// EMPTY_POOL_PLACEHOLDER is stored in TLS when exactly one pool is
// pushed and it has never contained any objects. This saves memory
// when the top level (i.e. libdispatch) pushes and pops pools but
// never uses them.

好奇这个TLS到底是个什么样的存在,做了一点功课.

TLS,Thread Local Storage,线程局部存储,也就是将一块内存作为某个线程专属的存储,同一线程的多个pool共享这个存储区域.那么这个区域具体存储什么呢?
上文__strong的实现中我们提到,对于自己不持有的对象,系统会自动插入的一个命令objc_retainAutoreleasedReturnValue,与之对应的还有objc_retainAutoreleasedReturnValue objc_autoreleaseReturnValue.更详细的优化命令如下图:

autorelease 进行的优化

那么这些命令和TLS又有什么关系呢?上代码:

id objc_retainAutoreleasedReturnValue(id obj)
{
    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;
    return objc_retain(obj);
}
id objc_autoreleaseReturnValue(id obj)
{
    if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;
    return objc_autorelease(obj);
}

其中的acceptOptimizedReturnprepareOptimizedReturn是优化标识,而优化标识和优化对象就是存储在TLS上的.当可以优化时,直接从TLS上返回当前对象地址,而不执行retainrelease操作.
比如对同一个对象进行retain操作后又进行release操作,那么这两个操作都不会执行.

而更具体的什么时候存储在TLS上,什么情况下进行优化,现在还存疑,不再细说.

4. when

六. dealloc

dealloc方法在最后一次release后被调用,但此时实例变量(Ivars)并未释放,父类的dealloc的方法将在子类dealloc方 法返回后自动调用.析构对象,并释放空间..具体流程如图:

dealloc流程简图

其中,objc_clear_deallocating方法,除了清除SideTable中的引用计数外,也会对弱引用表进行清除.

这里提到的析构方法SEL_cxx_destruct,和上文提到的构造方法SEL_cxx_construct相对应.在孙源的ARC下dealloc过 程及.cxx_destruct的探究文 中提到SEL_cxx_destruct方法名为.cxx_destruct,而我在编号objc4-706的文件objc- runtime.mm中只找到如下定义,不知道是不是版本不同已经更新的关系:

SEL SEL_cxx_construct = NULL; 
SEL SEL_cxx_destruct = NULL;

七. 所以,这是所有了吗

不,还不是..
上面部分,我们大概梳理了对象的生成与销毁过程.那么什么时候生成该对象呢?在此之前发生了什么?

你知道我经历了什么

PS

一直在看,没有真正公开写过技术文,没想到...挺难的...
文章也不涉及新知识,没有什么高深见解,更多的只是梳理.虽然配图都是前期做PPT的时候画好的,但是还是用了一整天的时间才写到最后.知识有限,又难免被打断,也许 有疏漏错误之处,如果有人可以指出,会非常感激.

不管怎样,动手来写是个好的开始.
继续努力.