iOS 内存管理

本文中的源代码来源:需要下载Runtime的源码,官方的工程需要经过大量调试才能使用。这里有处理好的objc4-756.2工程,以下都是基于处理好的objc4-756.2工程说明的。

一、内存布局

  • 栈(stack):由编译器自动分配、释放、存储函数的参数值、返回值和局部变量,在程序运行过程中实时分配和释放,由操作系统自动管理,无须程序员手动管理。栈区由高地址向低地址增长。
  • 堆(heap): 存放通过 alloc 等分配的内存块,空间的申请和释放由程序员控制。iOS 中的对象一般都会在堆区开辟空间,使用引用计数的机制去管理对象的内存。
  • bbs: 存放未初始化的全局变量和静态变量。
  • data:已初始化的全局变量、静态变量。
  • text: 存放 CPU 执行的机器指令,代码区是可共享,并且是只读的。

对于 iOS 对象的内存管理方案主要有三种:TaggedPointer、NONPOINTER_ISA、散列表

二、TaggedPointer

void main() {
    ClassA *a = [[ClassA alloc]init];
}

在执行上面代码的时候一般情况下系统会做以下事情:

  1. 在栈区开辟一个空间存放对象 a 的指针。
  2. 在堆区开辟一个空间存放对象 a 本身,并通过 a 的指针来访问 a 的内存。
  3. 存放在堆区的对象 a 需要在合适的时机释放。

iOS 系统对对象的管理一般就是上述的情况,系统的引用计数管理(MRC、ARC)的基础就是基于以上过程。

但是这种机制就是否是完美无缺的呢?或者说是否在一些特殊情况下拥有一种更加高效的方式去对对象的内存进行管理呢?苹果提供了一种名为 TaggedPointer 的内存管理技术。

痛点

系统在运行过程中会产生许多轻量级的对象,如果这些对象都要在堆上为其分配内存,维护它的引用计数,管理它的生命周期,无疑会给程序增加额外的逻辑、占用更多空间,造成效率的损失。比如系统只想在 NSNumber 中存放一个 1,但是又要用到 NSNumber 的方法,不能简单的使用 int 类型。如果这时系统使用常规的对象内存管理机制,就需要在栈区开辟一个空间,堆区开辟一个空间,还要管理对象的引用计数,这无疑有些得不偿失。就好像有的人总会想的一个问题:为何我要为 6 位数的密码来保护两位数的存款?

系统的做法(以前的做法)

苹果发现对于 64 为的 CPU,它的指针大小也是 64 位,而 64 位可以做什么呢,如果存放一个正整数它最大可以存放 2^64 的数值,那么通常情况下对于一个整数的存放,这个指针所占用的空间完全够用。这样系统就可以将一些轻量级对象的值放到指针中。这样系统就无须在堆区开辟内存,也更无须考虑对象的释放问题了。这大大降低了系统的内存空间和对象的管理成本。

但是有一些问题,就是毕竟很多重对象不能用 TaggedPointer 技术的,系统需要识别哪个指针是指向堆区的,哪个指针是使用 TaggedPointer 技术的。还有就是在指针中还有存放关于类的信息,不然光有值,确不能调用方法。

首先计算机对于对象的存储是有一定规律的,为了使对象在内存中对齐,对象的地址总是指针大小的整数倍,通常是 16 倍,这意味着啥?16的二级制是 1 0000,就是说一个正常对象的指针后四位都是 0,TaggedPointer 的方式就是将使用该技术的对象最后一位置位 1,这样就可以区分正常指针和 TaggedPointer 指针了。苹果又使用了接下来的三位去存放关于类的信息,这样系统就知道了这里存放的是什么类型的数据了。剩下的 60 为就是真正用来存放对象值的空间了,如果存放正整数可以存放 2^60 之多,适用于大部分情况,如果值所占空间过大,系统会重新在堆区开辟空间,对对象进行操作和管理。

但是这个方式似乎弃用了,苹果现在的 TaggedPointer 技术似乎更加复杂,至少我是无法完全解析,先看以下代码

    NSMutableString *muStr = [NSMutableString stringWithString:@"1"];
    for (int i=0; i<20; i++) {
        NSNumber *number = @([muStr longLongValue]);
        NSLog(@"%s, %p, %@", class_getName(number.class), number, number);
        [muStr appendFormat:@"1"];
    }
    
    NSString *baseStr = @"abcdefghijklmn";
    for (int i=0; i

这是打印

我发现每次打印都不相同,实在没什么规律,但是也能够确定哪些情况使用了 TaggedPointer 技术,正常对象的指针后四位都是0,上面打印的是16进制的指针,那么最后一位是 0(代表2进制后四位为0) 的就是正常对象,最后一位不是 0 的就是使用了 TaggedPointer 技术的对象了。

三、NONPOINTER_ISA

对于不能使用 TaggedPointer 的情况,系统只能去堆区开辟空间了,我们知道任何一个 OC 对象都有一个 isa 指针指向对象的类,使对象可以调用其方法、属性等。那么和 TaggedPointer 类似,对象的 isa 指针也是有 64 位(64位系统下)的,而指针信息的存储或许不需要那么多的位数,系统完全可以利用剩余的位数做一些什么。苹果利用了这些位数存储一些与内存管理相关的内容,使 isa 不仅仅是一个类对象的指针,这种技术就是 NONPOINTER_ISA。

下面看一下 isa 指针 isa_t 联合体的定义和里面的 ISA_BITFIELD

union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    Class cls;
    uintptr_t bits;
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };
#endif
};
# if __arm64__
#   define ISA_MASK        0x0000000ffffffff8ULL
#   define ISA_MAGIC_MASK  0x000003f000000001ULL
#   define ISA_MAGIC_VALUE 0x000001a000000001ULL
#   define ISA_BITFIELD                                                      \
      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;                                       \
      uintptr_t deallocating      : 1;                                       \
      uintptr_t has_sidetable_rc  : 1;                                       \
      uintptr_t extra_rc          : 19
#   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
#   define ISA_BITFIELD                                                        \
      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

在第二段代码中我们知道在 arm64 和 x86_64 架构下都定义了 ISA_BITFIELD,而其他情况是没有的,它们共同情况就是这两种架构都是 64 位的,32 为手机(模拟器)因为其指针能存储的信息太少(32位)而没有使用 NONPOINTER_ISA 的价值。我们主要看 arm64 架构下的情况,因为这是真机使用的情况。

位数 名字 作用 解释
1 nonpointer 判断该指针是否使用 NONPOINTER_ISA 技术 上小节得出正常指针后四位为 0,所以这指针最低位为 1 就可判断是否使用 NONPOINTER_ISA 技术
1 has_assoc 判断是否使用关联对象技术 关联对象需要一个全局的 manager 管理,需要在对象释放的时候移除
1 has_cxx_dtor 判断是否使用 c++ 构析函数(.cxx_destruct) 有的话需要做一些处理
33 shiftcls 存放类对象的内存地址信息 同前
6 magic 用于在调试时分辨对象是否未完成初始化 同前
1 weakly_referenced 是否有被弱引用指针引用过 弱引用的指针需要在对象释放时将指针自动置位 nil
1 deallocating 判断对象是否正在释放 同前
1 has_sidetable_rc 是否需要使用散列表来存放对象的引用计数 后 19 位还有一个 extra_rc,是用来存放对象引用计数的,但是如果对象引用计数过大,需要在外挂的散列表中查找对象的引用计数,如果比较小,可以存放在 extra_rc 的 19 位中
19 extra_rc 存放对象的引用计数 在对象引用计数较小时使用

四、散列表

散列表中包含:自旋锁(spinlock_t)、引用计数表(RefcountMap),弱引用表(weak_table_t)
下面是散列表结构体的数据结构

struct SideTable {
// 自旋锁
    spinlock_t slock;
// 引用计数表
    RefcountMap refcnts;
// 弱引用表
    weak_table_t weak_table;

    SideTable() {
        memset(&weak_table, 0, sizeof(weak_table));
    }

    ~SideTable() {
        _objc_fatal("Do not delete SideTable.");
    }

    void lock() { slock.lock(); }
    void unlock() { slock.unlock(); }
    void forceReset() { slock.forceReset(); }

    // Address-ordered lock discipline for a pair of side tables.

    template
    static void lockTwo(SideTable *lock1, SideTable *lock2);
    template
    static void unlockTwo(SideTable *lock1, SideTable *lock2);
};

尽管在 NONPOINTER_ISA 中留出来 19 位存放它的引用计数,但是对象的引用计数可能超出 NONPOINTER_ISA 所能存储的极限,而且在一些情况下并不能使用 NONPOINTER_ISA 技术,例如 32 位的手机,或者类对象内存地址信息超出 33 位时。这就要使用引用计数表来查找对象的引用计数。

还有在 NONPOINTER_ISA 下,系统只知道对象是否被弱引用,但并没有体现有哪些弱引用。如果有弱引用,就要查找弱引用表来对对象的弱引用进行处理。

1、散列表的结构

系统如何获取散列表?系统使用的是 Sidetable()[obj] 函数,获取 obj 的散列表,进而获取里面的引用计数和弱引用信息。

static StripedMap& SideTables() {
    return *reinterpret_cast*>(SideTableBuf);
}

根据上面的源代码,我们可以知道 SideTables() 得到的是一个 StripedMap 类型数据,看名字应该是一个存放 SideTable 的 Map。

下面是 StripedMap 部分源代码

template
class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };
#else
    enum { StripeCount = 64 };
#endif

    struct PaddedT {
        T value alignas(CacheLineSize);
    };

    PaddedT array[StripeCount];

    static unsigned int indexForPointer(const void *p) {
        uintptr_t addr = reinterpret_cast(p);
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }
}

这部分代码我们可以知道,系统应该是将 SideTable 存放在代码中定义的 array 中,array 的大小 StripeCount 在 TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR 情况下是 8(iPhone 真机),其他情况是 64。

indexForPointer 函数中可以看到返回的数组下标是对对象指针进行一番操作后对数组个数 StripeCount 取的余数。

所有这个 StripedMap 中会存放 8 或 64 个 SideTable 元素,存取是通过哈希算法计算出来的(不需要遍历数组就可以直接根据对象的指针计算出数组的下标)。

但是获取到 SideTable 只是第一步,我们最终的目的是要对表里面相应对象的引用计数和弱引用指针进行操作,在本节一开始的源代码 SideTable 结构体中就有这两方面的信息分别是 refcnts 和 weak_table 这两个参数。下面我们具体看一下如何使用 refcnts 获取对对象的引用计数及进行加减操作,如何使用 weak_table 找到对象的弱引用指针及进行添加删除弱引用指针操作。

2、对引用计数表的操作

我们看看 SideTable 中的 RefcountMap,点进去后可以看到

// RefcountMap disguises its pointers because we 
// don't want the table to act as a root for `leaks`.
typedef objc::DenseMap,size_t,true> RefcountMap;

它本质是一个 DenseMap 类型,也是通过哈希运算的方式通过对象的指针获取表中的内容,并进行操作。我们从注释中可以看到,苹果对对象的指针进行了伪装,目的是不泄露内存。

为了更好的理解对引用计数表的操作我们看一下源代码,看系统如何通过散列表进行 retain 操作的(retain 操作不仅包括散列表的,还有 NONPOINTER_ISA 的,通过前面的学习不难理解,这里先只贴出关于散列表的 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 操作首先是经过两次哈希查找(一次是SideTables()[this] 是前面那个取余的算法 StripedMap 里的 indexForPointer 函数,一个是table.refcnts[this]前面说的伪装算法)找到引用计数,然后进行加 SIDE_TABLE_RC_ONE 的操作。

看看SIDE_TABLE_RC_ONE的定义:#define SIDE_TABLE_RC_ONE (1UL<<2) // MSB-ward of deallocating bit 这个说明是两位比特,每次加 4。引用计数不是每引用一次加 1 吗?为什么会是 4?原因请看下图:

refcnts 里面存的 size_t 的结构

我们可以看到函数的一开始做了一个断言 assert(!isa.nonpointer); 这说明 isa 使用 NONPOINTER_ISA 的情况下会抛出异常,这个函数是只有在不使用 NONPOINTER_ISA 技术的情况下才会调用,这时我们不能从 isa 指针中找到 weakly_referenced 和 deallocating 的值,所以会保存在引用计数表当中。这样为了保证后两位不被改变,每次引用计数的“加 1”操作实际上是加 4。

当然对于使用 NONPOINTER_ISA 技术的对象,它的 retain 操作并不是那么简单,因为这样做完全放弃了 isa 指针中 19 位的 extra_rc。而且 weakly_referenced 和 deallocating 也重复写入了,系统会有更好的方式去解决这种情况,我们稍后讨论。

3、对弱引用表的操作

从注释来看,这是一个全局的弱引用表,将对象的 id 当做 key 值,weak_entry_t 结构体变量为 value 值,显然这是一个通过哈希查找来确定对象存放弱引用指针集合的地方。weak_entry_t 里面就存储着对象所有的 weak 指针。

/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    weak_entry_t *weak_entries;
    size_t    num_entries;
    uintptr_t mask;
    uintptr_t max_hash_displacement;
};
struct weak_entry_t {
    DisguisedPtr referent;
    union {
        struct {
            weak_referrer_t *referrers;
            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];
        };
    };

    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;
        }
    }
};

为了更好的理解系统对弱引用表的操作,我们探讨一下 __weak 修饰的对象初始化的原理。

① __weak修饰对象的初始化原理
{
    NSObject *obj = [[NSObject alloc] init];
    id __weak obj1 = obj;
}

id __weak obj1 = obj 事实上是调用 NSObject.mm 中的 id objc_initWeak(id *location, id newObj) 函数。

/* @param location Address of __weak ptr. 
 * @param newObj Object ptr. 
 */
id
objc_initWeak(id *location, id newObj)
{
    if (!newObj) {
        *location = nil;
        return nil;
    }

    return storeWeak
        (location, (objc_object*)newObj);
}

objc_initWeak 有两个参数,第一个参数是 __weak 修饰的指针,第二个是赋值的对象,具体到第一部分的代码就是:location 是 obj1 的指针,还没有赋值的情况下指向 nil,newObj 是 obj 对象。这里面主要调用 storeWeak 函数。<> 中的三个变量,点进去后发现它们分别是 false、true、true。我们看一下 storeWeak 这个函数

template 
static id 
storeWeak(id *location, objc_object *newObj)
{
// 根据传递过来的情况参数分别是:
// location:__weak 指针
// newObj:obj 对象
// haveOld: 初始化的时候 false,第二次可能为 true
// haveNew: 赋值 nil 为 false,赋值对象为 true
    assert(haveOld  ||  haveNew);
    if (!haveNew) assert(newObj == nil);

    Class previouslyInitializedClass = nil;
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 retry:
    if (haveOld) {
        oldObj = *location;
        oldTable = &SideTables()[oldObj];
    } else {
        oldTable = nil;
    }
    if (haveNew) {
        newTable = &SideTables()[newObj];
    } else {
        newTable = nil;
    }

    SideTable::lockTwo(oldTable, newTable);

    if (haveOld  &&  *location != oldObj) {
        SideTable::unlockTwo(oldTable, newTable);
        goto retry;
    }

    // Prevent a deadlock between the weak reference machinery
    // and the +initialize machinery by ensuring that no 
    // weakly-referenced object has an un-+initialized isa.
    if (haveNew  &&  newObj) {
        Class cls = newObj->getIsa();
        if (cls != previouslyInitializedClass  &&  
            !((objc_class *)cls)->isInitialized()) 
        {
            SideTable::unlockTwo(oldTable, newTable);
            class_initialize(cls, (id)newObj);

            // If this class is finished with +initialize then we're good.
            // If this class is still running +initialize on this thread 
            // (i.e. +initialize called storeWeak on an instance of itself)
            // then we may proceed but it will appear initializing and 
            // not yet initialized to the check above.
            // Instead set previouslyInitializedClass to recognize it on retry.
            previouslyInitializedClass = cls;

            goto retry;
        }
    }

    // Clean up old value, if any.
    if (haveOld) {
        weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    }

    // Assign new value, if any.
    if (haveNew) {
        newObj = (objc_object *)
            weak_register_no_lock(&newTable->weak_table, (id)newObj, location, 
                                  crashIfDeallocating);
        // weak_register_no_lock returns nil if weak store should be rejected

        // Set is-weakly-referenced bit in refcount table.
        if (newObj  &&  !newObj->isTaggedPointer()) {
            newObj->setWeaklyReferenced_nolock();
        }

        // Do not set *location anywhere else. That would introduce a race.
        *location = (id)newObj;
    }
    else {
        // No new value. The storage is not changed.
    }
    
    SideTable::unlockTwo(oldTable, newTable);

    return (id)newObj;
}

根据传递过来的情况参数分别是:

  • location:__weak 指针
  • newObj:obj 对象
  • haveOld: 初始化的时候 false,第二次可能为 true
  • haveNew: 赋值 nil 为 false,赋值对象为 true

这里定义了两个表,旧表和新表(oldTable、newTable),分别在 haveOld 和 haveNew 时使用 SideTables()[] 进行赋值。

由于多线程并发操作在为新旧表上锁的时候,location 的内容可以已经被修改(weak 指针指向其他内存块),需要判断 if (haveOld && *location != oldObj),如果被修改要回到 retry 重新执行。

if (haveNew && newObj) 这个分支是处理 newObj 没有完成初始化的情况。下面是核心

看 haveOld 的分支,如果 weak 指针之前已经指向了一个内存块,比如第二次为 location 赋值,haveOld 为 true,这时需要将 weak 指针从表中删除(通过 weak_unregister_no_lock 函数)。也就是说当 weakObj = nil 代码执行时,系统会自动删除弱引用表中的相应指针。也就是说在对象被释放时,只需 weak_unregister_no_lock 所有弱引用指针就可以了。

上面已经把表中的 weak 指针删除了,如果 haveNew == true,就需要再次添加回来,这里调用了 weak_register_no_lock 函数,我们看一看它的具体内容,了解系统如何操作弱引用表。

/** 
 * @param weak_table The global weak table.
 * @param referent The object pointed to by the weak reference.
 * @param referrer The weak pointer address.
 */
id 
weak_register_no_lock(weak_table_t *weak_table, id referent_id, 
                      id *referrer_id, bool crashIfDeallocating)
{
    objc_object *referent = (objc_object *)referent_id;
    objc_object **referrer = (objc_object **)referrer_id;

    if (!referent  ||  referent->isTaggedPointer()) return referent_id;

    // ensure that the referenced object is viable
    bool deallocating;
    if (!referent->ISA()->hasCustomRR()) {
        deallocating = referent->rootIsDeallocating();
    }
    else {
        BOOL (*allowsWeakReference)(objc_object *, SEL) = 
            (BOOL(*)(objc_object *, SEL))
            object_getMethodImplementation((id)referent, 
                                           SEL_allowsWeakReference);
        if ((IMP)allowsWeakReference == _objc_msgForward) {
            return nil;
        }
        deallocating =
            ! (*allowsWeakReference)(referent, SEL_allowsWeakReference);
    }

    if (deallocating) {
        if (crashIfDeallocating) {
            _objc_fatal("Cannot form weak reference to instance (%p) of "
                        "class %s. It is possible that this object was "
                        "over-released, or is in the process of deallocation.",
                        (void*)referent, object_getClassName((id)referent));
        } else {
            return nil;
        }
    }

    // now remember it and where it is being stored
    weak_entry_t *entry;
    if ((entry = weak_entry_for_referent(weak_table, referent))) {
        append_referrer(entry, referrer);
    } 
    else {
        weak_entry_t new_entry(referent, referrer);
        weak_grow_maybe(weak_table);
        weak_entry_insert(weak_table, &new_entry);
    }

    // Do not set *referrer. objc_storeWeak() requires that the 
    // value not change.

    return referent_id;
}

我们看一下各参数的意义:

  • weak_table:对象所在的弱引用表
  • referent_id:新对象 obj
  • referrer_id:__weak 指针

我们直接看函数的后面 // now remember it and where it is being stored 注释后面的内容。

这个过程就是通过 weak_entry_for_referent 函数得到弱引用表中对象所有弱引用指针集合 entry,它的定义可以看前面的代码,如果存在 entry,就把 weak 指针添加到 entry 中(使用 append_referrer),不存在 entry 时,需要创建一个新的 entry,第一个元素就是 weak 指针,然后使用 weak_entry_insert 插入弱引用表中。

我们看一下 weak_entry_for_referent 函数,查看 entry 的查找是否为哈希运算

static weak_entry_t *
weak_entry_for_referent(weak_table_t *weak_table, objc_object *referent)
{
    assert(referent);

    weak_entry_t *weak_entries = weak_table->weak_entries;

    if (!weak_entries) return nil;

    size_t begin = hash_pointer(referent) & weak_table->mask;
    size_t index = begin;
    size_t hash_displacement = 0;
    while (weak_table->weak_entries[index].referent != referent) {
        index = (index+1) & weak_table->mask;
        if (index == begin) bad_weak_table(weak_table->weak_entries);
        hash_displacement++;
        if (hash_displacement > weak_table->max_hash_displacement) {
            return nil;
        }
    }
    
    return &weak_table->weak_entries[index];
}

里面使用 index 来获取数组 weak_entries 中的 entry,系统一开始通过新对象来获取 index 可能的最小值,然后通过循环遍历来找到 具体的 entry 值。可以说系统使用了哈希算法找到了 entry 位置的最小值,但是之后通过的是遍历获取 entry 具体的位置。

4、对散列表结构的思考及自旋锁的作用
散列表结构

我们看到系统无论是操作引用计数还是弱引用指针集都需要经过两次哈希查找,而对象的地址唯一,完全可以只需要一个 SideTable,这样只需要一次查找就可以找到它们,这样做的效率似乎更高,为何系统不这样使用?

原因与系统的多线程并发有关,系统的每个线程都有可能访问散列表中的数据,如果不对散列表进行保护,在多线程并发情况下容易造成数据错乱,故而 SideTable 使用了自旋锁,即在某个线程访问 SideTable 时,其他线程只能等待该线程访问完才可以访问,如果系统只有一张 SideTable,那么会造成散列表数据的访问总是在等待中。

不给每个对象都配一套自旋锁的原因是自旋锁本身会占用一定的系统资源,而且系统的线程数不可能无限,而对象的数量远远超过线程的数量。这也是 SideTable 只有 8 或 64 个的原因。

五、引用计数及相应内存管理方法

当一段代码需要访问某个对象时,该对象的引用计数加 1;当不再访问时引用计数减 1,当引用计数为 0 时,系统回收对象所占内存。
一般来说:

  • 当程序调用 alloc、new、copy、mutableCopy 开头的方法时该对象的引用计数加 1。
  • 调用 retain 方法时,该对象的引用计数加 1.
  • 调用 release 方法时,该对象引用计数减 1.

iOS 中提供如下引用计数方法
retain、release、autorelease、retainCount、dealloc

1、MRC 和 ARC 的区别
  • MRC 是手动引用计数,需要手动调用引用计数相关方法,ARC 是自动引用计数,系统在合适的时机自动调用引用计数相关方法,禁止手动调用引用计数相关方法。

  • ARC 是编译器和 Runtime 协作的结果。

  • ARC 中新增 weak、strong 属性关键字。

2、retain

retain 是使对象引用计数加 1 的方法,前面已经讨论过存散列表情况下的 retain 操作,但是在使用 NONPOINTER_ISA 情况下,以及二者混合使用的情况还没有讨论,那么我们看一下 retain 的具体实现。

- (id)retain {
    return ((id)self)->rootRetain();
}

ALWAYS_INLINE id 
objc_object::rootRetain()
{
    return rootRetain(false, false);
}

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    if (isTaggedPointer()) return (id)this;

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain();
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        if (slowpath(tryRetain && newisa.deallocating)) {
            ClearExclusive(&isa.bits);
            if (!tryRetain && sideTableLocked) sidetable_unlock();
            return nil;
        }
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        if (slowpath(carry)) {
            // newisa.extra_rc++ overflowed
            if (!handleOverflow) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }
            // Leave half of the retain counts inline and 
            // prepare to copy the other half to the side table.
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
        sidetable_addExtraRC_nolock(RC_HALF);
    }

    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    return (id)this;
}

以上三个函数是 retain 调用后的具体过程,我们主要看最后一个 rootRetain 函数。

首先判断该对象是否使用 TaggedPointer 技术,非 TaggedPointer 对象才可继续进行。在 do while 循环中 if (slowpath(!newisa.nonpointer)),这是没有 NONPOINTER_ISA 技术的对象情况,此时的情况就是上节讨论的散列表 retain 操作,调用了 sidetable_retain 函数,然后返回掉。

然后是 if (slowpath(tryRetain && newisa.deallocating)) 的分支,说明对象正在被释放,此时将 SideTable 解锁,然后返回掉。

接下来就是使用 NONPOINTER_ISA 技术的对象了,它进行了 newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry); // extra_rc++ 操作。看后面的注释,说明这是给 extra_rc 加 1 操作,这也验证了前面说的,19 位 extra_rc(模拟器 8 位)是用来存储引用计数的了。

循环体中还有一个 if (slowpath(carry)) 的分支,这个分支是用来处理 extra_rc 溢出的情况。我们只看最后三行代码,首先 transcribeToSideTable = true,这是告诉后面需要往散列表中写数据了,循环结束后第一个分支判断就是基于这个数据。newisa.extra_rc = RC_HALF,是给 extra_rc 重新赋值。

# if __arm64__
#   define RC_HALF  (1ULL<<18)

# elif __x86_64__
#   define RC_HALF  (1ULL<<7)

# else
#   error unknown architecture for packed isa
# endif

我们看一下 RC_HALF 这个只,在 arm64 下是 218,在 x86_64 下是 27,正好是 extra_rc 最大值的一半。所以就是每次 extra_rc 达到最大值的时候,就将 extra_rc 减少到一半,后面一半的内容累加到散列表中。

newisa.has_sidetable_rc = true 很好理解,因为溢出就使用了散列表,所以为 true,这个变量也是存在 NONPOINTER_ISA 指针中的,前面小节(三)中总结的表中有这个变量。

然后我们看溢出后怎样把额外引用计数存储在散列表中,就是循环体外 if (slowpath(transcribeToSideTable)) 分支,我们看一下里面的函数 sidetable_addExtraRC_nolock 注释告诉我们是要 Copy 另一半的 retain count 到 SideTable 中。

bool 
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
    assert(isa.nonpointer);
    SideTable& table = SideTables()[this];

    size_t& refcntStorage = table.refcnts[this];
    size_t oldRefcnt = refcntStorage;
    // isa-side bits should not be set here
    assert((oldRefcnt & SIDE_TABLE_DEALLOCATING) == 0);
    assert((oldRefcnt & SIDE_TABLE_WEAKLY_REFERENCED) == 0);

    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;

    uintptr_t carry;
    size_t newRefcnt = 
        addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
    if (carry) {
        refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    }
    else {
        refcntStorage = newRefcnt;
        return false;
    }
}

addc 函数的调用size_t newRefcnt = addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry) 很好的说明了将 delta_rc 添加到 oldRefcnt 上面。
下面通过一幅流程图来更加清晰的了解一下其中的过程:

retain 流程图.jpg

3、release

release 的操作就是和 retain 的操作相反,同样是先判断 TaggedPointer,再判断 NONPOINTER_ISA,然后 extra_rc 减 1,减到 0,从 SideTable 中拿出 RC_HALF 的数据到 extra_rc,引用计数为 0 时,如果需要调用 dealloc 释放。这里对 release 就不做详细说明了。

4、autorelease 和 @autoreleasepool

autoreleasepool 本质上是一个以栈为结点的双向链表结构,结构如下图:


autoreleasepool 数据结构

一个 AutoreleasePoolPage 拥有 4096 个字节用来存放加入 autoreleasepool 的对象等信息,当 AutoreleasePoolPage 填满时,会自动创建一个 child 开辟新的空间。

AutoreleasePoolPage 主要有push、pop、autorelease 等方法。

@autoreleasepool {} 使用 clang 编译后(clang -rewrite-objc main.m)main.cpp 文件中最后会出现如下代码

{ __AtAutoreleasePool __autoreleasepool; 
       
}

全局搜索 __AtAutoreleasePool,可以找到如下结构体

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

也就是说 @autoreleasepool {} 本质上是如下代码

    {
        void *atautoreleasepoolobj = objc_autoreleasePoolPush();
        // autoreleasepool 里面的代码
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }

objc_autoreleasePoolPushobjc_autoreleasePoolPop 就是类 AutoreleasePoolPagepush、pop 方法

void *
objc_autoreleasePoolPush(void)
{
    return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt)
{
    AutoreleasePoolPage::pop(ctxt);
}

NSObject *obj1 = obj; 这种代码在 ARC 环境下会自动插入 autorelease,如下

NSObject *obj1 = obj;
[obj1 autorelease];

下面我用一个示例来说明一下 autoreleasepool 的工作原理

int main(int argc, const char * argv[]) {
    NSObject *obj = [NSObject new];
    @autoreleasepool {
        NSObject *obj1 = obj;
        @autoreleasepool {
            NSObject *obj2 = obj;
            // ......
            // 假设 objm 正好是第一个 AutoreleasePoolPage 的栈顶
            NSObject *objm = obj;
            NSObject *objm1 = obj;
            NSObject *objm2 = obj;
            @autoreleasepool {
                NSObject *objm3 = obj;
                // ......
                NSObject *objn = obj;
                
            }
        }
    }
    return 0;
}

main 函数中第一个 @autoreleasepool,开始会调用 push 方法

static inline void *push() 
    {
        id *dest;
        if (DebugPoolAllocation) {
            // Each autorelease pool starts on a new pool page.
            dest = autoreleaseNewPage(POOL_BOUNDARY);
        } else {
            dest = autoreleaseFast(POOL_BOUNDARY);
        }
        assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);
        return dest;
    }

我们正常开发 iOS 的时候都是走的第二个分支,即 dest = autoreleaseFast(POOL_BOUNDARY);,这是每个AutoreleasePoolPage 填满后创建新的(第一个分支是每次 push 都新建 AutoreleasePoolPage,我们不做进一步了解),POOL_BOUNDARY 就是 nil,也就是每次 push 实际上就是调用 autoreleaseFast(nil)

    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);
        }
    }

autoreleaseFast 里面有三个分支,首先获取当前的 page,如果有 page 并且没有被填满,就 add 一个参数,第一个 autoreleasepool 的 push 不会走这个分支,因为此时还没有创建 page。第二个分支是 page 被填满的情况,这一次也不会走,我们直接看第三个分支 autoreleaseNoPage

    id *autoreleaseNoPage(id obj)
    {
        。。。。。。/ 前面还有代码,不做了解
        // Install the first page.
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        
        // Push a boundary on behalf of the previously-placeholder'd pool.
        if (pushExtraBoundary) {
            page->add(POOL_BOUNDARY);
        }
        
        // Push the requested object or pool.
        return page->add(obj);
    }

这个函数的功能就是新建一个 page,然后加到 hotPage 中(加到双向链表的最后一项,作为上一个 page 的 child),然后调用 add 函数,具体到本次就是 add(nil)

    id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

*next++ = obj; 意思是 *next = obj; next++;,所以 add 函数是把 obj 写入 next 指针,然后指针进一位,知道栈顶(达到 4096 字节)

那么第一个 @autoreleasepool 的 push 作用是创建一个 AutoreleasePoolPage,并插入一个 nil,如下图:

新建第一个AutoreleasePoolPage

之后的 NSObject *obj1 = obj; ARC 环境下自动插入 [obj1 autorelease]; 我们看看 autorelease 方法的实现

-(id) autorelease
{
    return _objc_rootAutorelease(self);
}
id
_objc_rootAutorelease(id obj)
{
    assert(obj);
    return obj->rootAutorelease();
}
inline id 
objc_object::rootAutorelease()
{
    if (isTaggedPointer()) return (id)this;
    if (prepareOptimizedReturn(ReturnAtPlus1)) return (id)this;

    return rootAutorelease2();
}
__attribute__((noinline,used))
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 == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }

我们看上面的调用过程,最终调用 AutoreleasePoolPage 的 autorelease 方法。autorelease 方法中仍然调用 autoreleaseFast 方法,与 push 相同,但是本次插入不是 nil 而是 autorelease 的对象,本次就是 obj1。故而如下图:

插入 obj1

我们看接下来第二个 @autoreleasepool 的 push,然后从 obj2 到 objm 的插入(objm 正好为栈顶),如下图:

插入 objm

我们看当插入 objm1,的时候,第一个 AutoreleasePoolPage 已满,看 autoreleaseFast 走第二个分支,即调用 autoreleaseFullPage

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);
    }

这里有一个 do while 循环,本次会走一次,走 else 分支,新建一个 AutoreleasePoolPage,将当前 page 作为 parent 传递给 new page,然后 page 指针指向 new page。最后把 objm 加到 new page 上面。如下图:

插入 objm1

之后一直到 objn


插入 objn

之后最里面的 @autoreleasepool 结束,调用 pop 方法。

 static inline void pop(void *token) 
    {
        AutoreleasePoolPage *page;
        id *stop;

        if (token == (void*)EMPTY_POOL_PLACEHOLDER) {
            // Popping the top-level placeholder pool.
            if (hotPage()) {
                // Pool was used. Pop its contents normally.
                // Pool pages remain allocated for re-use as usual.
                pop(coldPage()->begin());
            } else {
                // Pool was never used. Clear the placeholder.
                setHotPage(nil);
            }
            return;
        }

        page = pageForPointer(token);
        stop = (id *)token;
        if (*stop != POOL_BOUNDARY) {
            if (stop == page->begin()  &&  !page->parent) {
                // Start of coldest page may correctly not be POOL_BOUNDARY:
                // 1. top-level pool is popped, leaving the cold page in place
                // 2. an object is autoreleased with no pool
            } else {
                // Error. For bincompat purposes this is not 
                // fatal in executables built with old SDKs.
                return badPop(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();
            }
        }
    }

pop 方法中的参数 token 是 @autoreleasepool push 方法的返回值,这是成对存在的,之前最后一个 @autoreleasepool 的返回值是插入 POOL_BOUNDARY(nil)的指针,所以参数告诉我们从哪个地址开始 pop。

stop = (id *)token; page->releaseUntil(stop);,这两句就是具体的 pop 操作,我们看看具体实现

void releaseUntil(id *stop) 
    {
        // Not recursive: we don't want to blow out the stack 
        // if a thread accumulates a stupendous amount of garbage
        
        while (this->next != stop) {
            // Restart from hotPage() every time, in case -release 
            // autoreleased more objects
            AutoreleasePoolPage *page = hotPage();

            // fixme I think this `while` can be `if`, but I can't prove it
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }

            page->unprotect();
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();

            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }

        setHotPage(this);

#if DEBUG
        // we expect any children to be completely empty
        for (AutoreleasePoolPage *page = child; page; page = page->child) {
            assert(page->empty());
        }
#endif
    }

我们先看 id obj = *--page->next; 拆解开后是 page->next --; id obj = *page->next; 外循环体中每次循环 next-1,然后 objc_release(obj); 释放一次,最终 next 会逼近 stop 最终退出循环,我们看最里面的 @autoreleasepool pop 会怎么样?如图:

最后一个 autoreleasepool pop

第二个 @autoreleasepool pop 会跨两个 AutoreleasePoolPage,具体逻辑请看上面 pop 代码中的内循环,如果 page->empty()(page 为空) page 就指向他的父节点继续查找。pop 函数中releaseUntil后面的代码也告诉我们,系统会清理掉当前 page 的 child。

第三个 @autoreleasepool 会释放 obj1 和第一个 AutoreleasePoolPage,这里不多解释。

至此关于自动释放池的部分就说明到这里

5、运行时优化(Thread Local Storage)

从上小节可知自动释放池是一个双向链表,每个结点都是有 AutoreleasePoolPage 的对象组成,它的结构比较复杂,开销比较大,对于这种现状,系统提供了一种 Thread Local Storage 技术在 ARC 环境下对对象的自动释放做了优化,使其性能大大高于自动释放池技术。

对于一个工厂方法我们分别分析使用 @autoreleasepoolThread Local Storage 的情况

@autoreleasepool

//MRC
+ (instancetype)createObj {
id any = [[CustomClass alloc]init];
return [any autorelease];
}

CustomClass *obj = [CustomClass createObj];

类方法 + createObj 创建的对象不会在方法结束时被销毁,而是在 autoreleasepool 执行 pop 操作的时候释放。这样将对象放入自动释放出,和把对象从自动释放出取出的过程消耗了一定的资源

ARC 中对 autorelease 进行的优化 —— Thread Local Storage

//ARC
+ (instancetype)createObj {
    id tmp = [[self alloc]init];
    return objc_autoreleaseReturnValue(tmp);
}

id tmp = objc_retainAutoreleasedReturnValue([CustomClass createObj]);
CustomClass * obj = tmp;
objc_storeStrong(&obj, nil);//就是release

这里面主要使用了一下三个函数 objc_autoreleaseReturnValue、objc_retainAutoreleasedReturnValue、objc_storeStrong

id 
objc_autoreleaseReturnValue(id obj)
{
    if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;

    return objc_autorelease(obj);
}

id
objc_retainAutoreleasedReturnValue(id obj)
{
    if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;

    return objc_retain(obj);
}

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

objc_storeStrong 是对对象的 release 操作,不多做解释。
我们看看另外两个函数

objc_autoreleaseReturnValue、objc_retainAutoreleasedReturnValue 成对使用,首先 objc_autoreleaseReturnValue 时如果 prepareOptimizedReturn(ReturnAtPlus1) == YES 代表需要进行优化,那么直接返回 obj,不把它加入自动释放池,此时 obj 的引用计数为 1,不会被释放。objc_retainAutoreleasedReturnValue 会经过 (acceptOptimizedReturn() == ReturnAtPlus1 判断,说明 obj 是经过优化的 obj,这样就不会进行 retain 操作,这样引用计数还是 1,最后经过 objc_storeStrong 把 obj 释放掉。

这样使用 Thread Local Storage 技术就可以减少 obj 对象进入自动释放池,从自动释放池出来的操作,又减少了一次 retain 操作和一次 release 操作从而大大降低了系统的消耗(如果是 @autoreleasepool 情况:工厂函数会 retain 一次,外界调用会 retain 一次,@autoreleasepool pop 时会释放一次,调用结束会释放一次)。

Thread Local Storage 优化技术有使用条件,那就是工厂方法和调用方都支持 ARC,因为只有这样方法内的objc_autoreleaseReturnValueobjc_retainAutoreleasedReturnValue才会配套使用.很多系统库还可能是 MRC 实现的,这样的系统类调用工厂方法生成的对象还是得进 @autoreleasepool

这也是为何 ARC 模式下也要保留 @autoreleasepool 模式的原因之一。

这也说明了只要在 ARC 环境下我们平常写的代码都会使用 Thread Local Storage 技术进行优化,而不进入自动释放池。(着重强调,因为与直观不符,我们平常写的代码一般不会进入 @autoreleasepool)

这里对 Thread Local Storage 技术只做简单介绍,对于平常理解和面试一般够用,如果想要更加详细的理解请看 iOS 底层拾遗:autorelease 优化。

你可能感兴趣的:(iOS 内存管理)