iOS 内存管理(上)

什么是内存管理

程序在运行过程中通常会有以下行为,来增加程序的内存占用

  • 创建一个对象
  • 定义一个变量
  • 调用一个函数或者方法

设备的内存是毕竟有限的,每个软件所能占用的内存也是有限的;当程序所占用的内存较多时,系统就会发出内存警告,这个时候就得回收一些不需要再使用的内存空间(例如回收一些不再使用的对象、变量等);如果程序占用内存过大,系统可能会强制关闭程序,造成闪退、崩溃现象,影响用户体验。

在开发过程中,对内存进行合理的分配、适当的清除内存,回收不再使用的对象,来确保程序的稳定性

在 iOS 开发中,那些对象需要进行内存管理呢?

  • 所有继承自 NSObject 的对象需要进行内存管理
  • 非对象类型(例如 intcharfloatdoublestructenum 等)不需要进行内存管理

继承自 NSobject 的对象的存储是在
堆是由程序员分配释放,如果程序员不释放,程序结束时可能由 OS 回收
OC 对象一般放在
栈是由系统自动分配释放,存放函数的参数值,局部变量的值等

内存管理分类

苹果的内存管理分为以下三种:

  • 垃圾回收(iOS 运行环境不支持)
  • 手动内存管理(Manual Reference Counting)
  • 自动管理内存(Automatic Reference Counting)
手动内存管理(MRC)
  1. 引用计数

每个 OC 对象都有自己的引用计数,它是一个整数,系统是根据对象的引用计数来判断什么时候需要回收一个对象所占用的内存。遵循以下规则:

  • 任何一个对象,刚创建的时候,初始的引用计数为 1(alloc、new、copy)
  • 对象被引用时,需要给对象发送一条 retain 消息,对象的引用计数 +1
  • 不再使用对象时,需要给对象发送一条 release 消息,对象的引用计数 -1
  • 当对象的引用计数为 0 时,这个对象即将被销毁,其占用的内存被系统回收。给对象发送 dealloc 消息

谁创建谁 release
retainrelease
有加必须有减,成对出现

  1. 自动释放池

当我们不再使用一个对象的时候应该将其空间释放,但是有时候我们不知道何时应该将其释放。为了解决这个问题,苹果提供了 autorelease 方法。

autorelease 是一种支持引用计数的内存管理方式,只要给对象发送一条 autorelease 消息,会将对象放到一个自动释放池中,当自动释放池被销毁时,会对池子里面的所有对象做一次 release 操作

好处:不用再关心对象释放的时间;不用再关心什么时候调用 release
实质:autorelease 实际上只是把对 release 的调用延迟了,对于每一个 autorelease,系统只是把该对象放入了当前的 autorelease pool 中,当该 pool 被释放时,该 pool 中的所有对象会被调用 release

注意,这里只是发送 release 消息,如果当时的引用计数依然不为 0,则该对象依然不会被释放。

自动管理内存(ARC)

自动引用计数(Automatic Reference Counting),即 ARC,是苹果在 WWDC2011 和 iOS 5 引入的自动管理机制,是新的LLVM 3.0编译器的一项特性。其规则与 MRC 一致,区别在于,ARC 模式下不需要手动 retainreleaseautorelease。编译器会在适当的位置插入 releaseautorelease

内存管理优化方案

除了前面提到的 MRCARC 内存管理方案,苹果还有以下三种优化方案

  • Tagged Pointer: 用来处理小对象(例如 NSNumberNSDate等)
  • Nonpointer_isa: 是否对 isa 指针开启指针优化(1:不止是类对象地址,isa 中包含了类信息、对象引用计数等)
  • SideTables: 散列表,主要有两个表,分别为 引用计数表弱引用表

下面通过一个题目引出今天的主题

//1
- (void)taggedPointerDemo {
  
    self.queue = dispatch_queue_create("com.lc.cn", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"lhc"];  // alloc 堆 iOS优化 - taggedpointer
             NSLog(@"%@",self.nameStr);
        });
    }
}

//2
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    
    NSLog(@"来了");
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"lhc_和谐学习"];
            NSLog(@"%@",self.nameStr);
        });
    }
}

运行代码,此时会运行方法 1,正常打印;点击屏幕,此时会运行方法2,程序崩溃了。崩溃原因:self.nameStr 调用了对象的 setter 方法,而 setter 的本质是新值 retain,旧值 release。多条线程同时对一个对象执行 setter 方法,导致上一个 release 还没结束,下一个 release 执行,从而 过度释放 以至于程序崩溃。

为什么方法 1 正常,方法 2 调用就崩溃了呢?两段代码只是字符串长短不一样,猜想一下是不是它们的数据类型不一样呢,我们下个断点看下

方法 2

从上面我们可以看到,方法 1 中的 nameStr 类型是 NSTaggedPointerString;方法 2 中的 nameStr 类型是 NSCFString。它们的数据类型不一致,是因为这个吗,我们先来看下 NSString 的内存管理情况

NSString 的内存管理

通过以下初始化 NSString 的方式,来测试 NSString 的内存管理

//方式一
NSString *str1 = @"123";
NSString *str2 = [[NSString alloc] initWithString:@"234"];
NSString *str3 = [NSString stringWithString:@"456"];

NSLog(@"%p -- %@", str1, [str1 class]);
NSLog(@"%p -- %@", str2, [str2 class]);
NSLog(@"%p -- %@", str3, [str3 class]);

//方式二:
//字符串长度在9以内
NSString *str4 = [NSString stringWithFormat:@"123456789"];
NSString *str5 = [[NSString alloc] initWithFormat:@"123456789"];

NSLog(@"%p -- %@", str4, [str4 class]);
NSLog(@"%p -- %@", str5, [str5 class]);

//字符串长度大于9
NSString *str6 = [NSString stringWithFormat:@"1234567890"];
NSString *str7 = [[NSString alloc] initWithFormat:@"1234567890"];

NSLog(@"%p -- %@", str6, [str4 class]);
NSLog(@"%p -- %@", str7, [str5 class]);

运行结果如下

从上面我们可以发现几种字符串类型

  • __NSCFConstantString: 字符串常量,是一种编译时常量,存储在字符串常量区
  • NSTaggedPointerString: 小对象类型,是苹果在 64 位环境下对 NSStringNSNumber 等对象做的优化
    • 当字符串是由数字、英文字母组合且长度小于等于 9 时,会自动成为 NSTaggedPointerString 类型,存储在常量区
    • 当有 中文或者其他特殊符号 时,会直接成为 __NSCFString 类型,存储在堆区
  • __NSCFString: 是在运行时创建的 NSString 子类,创建后引用计数会加 1,存储在 堆上

看到这里,上面的题目中方法 1 正常运行也就可以理解了

Tagged Pointer(小对象)

虽然我们知道了小对象的意义,但是苹果的底层到底怎么处理的,我们需要进入源码探索下,怎么下手呢?

从上面的题中我们可以知道 self.nameStr 调用的是对象的 setter 方法,而 setter 的本质是新值 retain,旧值 release。 我们就可以按照属性的调用流程去查看苹果对 Tagged Pointer 小对象的处理,在 objc-781 -> setProperty -> reallySetProperty 我们看到了如下源码

再次进入 objc_retainobjc_release 源码

//retain
__attribute__((aligned(16), flatten, noinline))
id 
objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}

//release
__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}

通过源码我们可以看到,都做了一层判断是否是小对象 isTaggedPointer(),如果是小对象,则直接返回对象,不做 retainrelease 操作

小对象地址分析

一般的 NSString 对象指针,它们的 string 值指针地址 是分开的。那么 Tagged Pointer 会有什么特殊性呢?

Tagged Pointer 指针,既包含了指针,也包含值,在 iOS 14 后苹果对 Tagged Pointer 采用了混淆处理

NSString *str1 = [NSString stringWithFormat:@"1"];
NSString *str2 = [[NSString alloc] initWithFormat:@"1234567890"];

NSLog(@"%p -- %@ -- %@", str1, str1, [str1 class]);
NSLog(@"%p -- %@ -- %@", str2, str2, [str2 class]);

iOS 14 之后

在类的加载时,_read_images 源码中有个方法对 Tagged Pointer 做了混淆处理 initializeTaggedPointerObfuscator();,源码实现如下

static void
initializeTaggedPointerObfuscator(void)
{
    if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
        // Set the obfuscator to zero for apps linked against older SDKs,
        // in case they're relying on the tagged pointer representation.
        DisableTaggedPointerObfuscation) {
        objc_debug_taggedpointer_obfuscator = 0;
    } else {
        //iOS 14 之后,对 taggedpointer 进行了混淆
        // Pull random data into the variable, then shift away all non-payload bits.
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
    }
}

那么底层是如何进行混淆处理的呢?这里我们需要查找 Tagged Pointer编码解码 操作

//编码
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return (void *)(objc_debug_taggedpointer_obfuscator ^ ptr);
}

//解码
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

通过上述代码,我们可以看到,编码和解码过程是加了两层异或操作,目的就是为了拿到 Tagged Pointer 本身。举个例子: 以 1000 1001 为例,mask0101 1000

    1000 1001
^   0101 1000  mask(编码)
    1101 0001
^   0101 1000  mask(解码)
    1000 1001

现在我们可以将上述解码的源码放到工程外面去解码,看看 Tagged Pointer 的真实情况

从上面 Tagged Pointer 的地址可以看到,62 就是 bASCII 码,我们在多打印几个案例看下

到这里,我们验证了小对象指针地址中确实存储了值。那么小对象地址高位其中的0xa、0xb 以及地位的 1、2、4又是什么含义呢?这里我们去看源码底层是如何判断 Tagged Pointer

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    // #   define _OBJC_TAG_MASK (1UL<<63)
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

_OBJC_TAG_MASK 等价于 1 左移 63 位,即最高位(64位)为 1(2^63),与上 ptr 结果是否等于 _OBJC_TAG_MASK。简单点说,就是判断 64 位上值是否为 1。

上面打印的结果中 0xa 转换成二进制数据为 10100xb 转换成二进制数据为 1011。64 位为 1,63~61 三位表示 tagType 类型:010 表示 NSString 类型;011 表示 NSNumber 类型。

我们可以通过 _objc_makeTaggedPointer 方法第一个参数传入的 tag 查看其定义

#if __has_feature(objc_fixed_enum)  ||  __cplusplus >= 201103L
enum objc_tag_index_t : uint16_t
#else
typedef uint16_t objc_tag_index_t;
enum
#endif
{
    // 60-bit payloads
    OBJC_TAG_NSAtom            = 0, 
    OBJC_TAG_1                 = 1, 
    OBJC_TAG_NSString          = 2, 
    OBJC_TAG_NSNumber          = 3, 
    OBJC_TAG_NSIndexPath       = 4, 
    OBJC_TAG_NSManagedObjectID = 5, 
    OBJC_TAG_NSDate            = 6,

    // 60-bit reserved
    OBJC_TAG_RESERVED_7        = 7, 

    // 52-bit payloads
    OBJC_TAG_Photos_1          = 8,
    OBJC_TAG_Photos_2          = 9,
    OBJC_TAG_Photos_3          = 10,
    OBJC_TAG_Photos_4          = 11,
    OBJC_TAG_XPC_1             = 12,
    OBJC_TAG_XPC_2             = 13,
    OBJC_TAG_XPC_3             = 14,
    OBJC_TAG_XPC_4             = 15,
    OBJC_TAG_NSColor           = 16,
    OBJC_TAG_UIColor           = 17,
    OBJC_TAG_CGColor           = 18,
    OBJC_TAG_NSIndexSet        = 19,

    OBJC_TAG_First60BitPayload = 0, 
    OBJC_TAG_Last60BitPayload  = 6, 
    OBJC_TAG_First52BitPayload = 8, 
    OBJC_TAG_Last52BitPayload  = 263, 

    OBJC_TAG_RESERVED_264      = 264
};
#if __has_feature(objc_fixed_enum)  &&  !defined(__cplusplus)
typedef enum objc_tag_index_t objc_tag_index_t;
#endif

Tagged Pointer 用于存储小对象(NSNumber、NSDate、NSString(有条件)等),它不再是简单的地址,而是 地址+值,所以,它实际上已经不再是一个对象了,只是一个披着对象的普通变量
Tagged Pointer 存储在 常量区,不需要 malloc和free,可以直接读取更改值,相比存储组在堆区的数据 读取上效率快了3倍创建的效率快了106倍 左右
Tagged Pointer 的 64 位地址中,前 4 位代表数据类型,后 4 位主要适用于系统做一些处理,中间位用于存储值
Tagged Pointer 不会进入 retainrelease,直接返回,不需要引用计数管理,可以直接被系统自主的释放和回收

retain 源码

如果是普通的对象,就会走 retainrelease 操作,首先我们去查看 retain 的源码。通过 objc_retain -> retain -> rootRetain 底层源码如下

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

    bool sideTableLocked = false;
    bool transcribeToSideTable = false;

    // 为什么有isa?因为需要对引用计数+1,而引用计数存储在isa的bits中,需要进行新旧isa的替换
    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        // 判断是否为 nonpointer isa
        if (slowpath(!newisa.nonpointer)) {
            // 如果不是 nonpointer isa,直接操作散列表sidetable
            ClearExclusive(&isa.bits);
            if (rawISA()->isMetaClass()) return (id)this;
            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;
        //执行引用计数+1操作,即对bits中的 1ULL<<45(arm64) 即extra_rc,用于该对象存储引用计数值
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++

        //判断extra_rc是否满了,carry是标识符
        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.
            //如果extra_rc满了,则直接将满状态的一半拿出来存到extra_rc,另一半存储到散列表中
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            sideTableLocked = true;
            transcribeToSideTable = true;
            newisa.extra_rc = RC_HALF; // (1ULL<<7) 就是8位的一半
            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;
}
    1. 判断是否为 nonpointer isa,如果不是则直接操作散列表
    1. 判断是否正在释放,如果正在释放,则执行 dealloc 流程
    1. extra_rc++ 引用计数加 1,carry 标示 extra_rc 是否满了
    1. 如果 extra_rc 满了,此时将满状态的 extra_rc 的一半存在 extra_rc,另一半存在散列表中

如果都存在散列表中,当需要 release 时,需要去访问散列表,每次都需要开解锁,比较消耗性能。extra_rc 存储一半的话,可以直接操作 extra_rc,不需要操作散列表。性能会提高很多。

Retain流程图

SideTables 散列表

散列表的结构如何?它在内存中是不是有多张?通过 sidetable_unlock()sidetable_lock() 源码

void 
objc_object::sidetable_lock()
{
    SideTable& table = SideTables()[this];
    table.lock();
}

void 
objc_object::sidetable_unlock()
{
    SideTable& table = SideTables()[this];
    table.unlock();
}

我们可以看到散列表不止一个,散列表的结构如下

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

我们再来看 SideTables 其是通过 SideTablesMapget 方法获取。而 SideTablesMap 是通过 StripedMap 定义的

static StripedMap& SideTables() {
    return SideTablesMap.get();
}

static objc::ExplicitInit> SideTablesMap;

再次进入 StripedMap 的定义可以看到同一时间,真机中散列表最多只能有8张,非真机下散列表最多有 64 张

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

散列表的优点

散列表的本质是一张 哈希表,它集合了数组和链表的长处,增删改查比较方便(数组读取快,存储慢;链表存储快,读取慢)

release 源码

上面分析了 retain 的源码,下面来分析 release 的底层实现。通过 objc_setProperty -> reallySetProperty -> objc_release -> release -> rootRelease -> rootRelease 源码如下:

ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    if (isTaggedPointer()) return false;

    bool sideTableLocked = false;

    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        // 判断是否是 nonpointer isa
        if (slowpath(!newisa.nonpointer)) {
            //如果不是,则直接操作散列表
            ClearExclusive(&isa.bits);
            if (rawISA()->isMetaClass()) return false;
            if (sideTableLocked) sidetable_unlock();
            return sidetable_release(performDealloc);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        uintptr_t carry;
        // 引用计数 -1,优先操作 extra_rc
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
        // extra_rc 为 0,走 underflow
        if (slowpath(carry)) {
            // don't ClearExclusive()
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

 underflow:
    // newisa.extra_rc-- underflowed: borrow from side table or deallocate

    // abandon newisa to undo the decrement
    newisa = oldisa;
    // 判断散列表中是否存储的引用计数
    if (slowpath(newisa.has_sidetable_rc)) {
        if (!handleUnderflow) {
            ClearExclusive(&isa.bits);
            return rootRelease_underflow(performDealloc);
        }

        // Transfer retain count from side table to inline storage.

        if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            // Need to start over to avoid a race against 
            // the nonpointer -> raw pointer transition.
            goto retry;
        }

        // Try to remove some retain counts from the side table.
        //从散列表中取出存储的引用计数
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        // To avoid races, has_sidetable_rc must remain set 
        // even if the side table count is now zero.

        if (borrowed > 0) {
            // Side table retain count decreased.
            // Try to add them to the inline count.
            // 引用计数 -1
            newisa.extra_rc = borrowed - 1;  // redo the original decrement too
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            if (!stored) {
                // Inline update failed. 
                // Try it again right now. This prevents livelock on LL/SC 
                // architectures where the side table access itself may have 
                // dropped the reservation.
                isa_t oldisa2 = LoadExclusive(&isa.bits);
                isa_t newisa2 = oldisa2;
                if (newisa2.nonpointer) {
                    uintptr_t overflow;
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    if (!overflow) {
                        stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                       newisa2.bits);
                    }
                }
            }

            if (!stored) {
                // Inline update failed.
                // Put the retains back in the side table.
                sidetable_addExtraRC_nolock(borrowed);
                goto retry;
            }

            // Decrement successful after borrowing from side table.
            // This decrement cannot be the deallocating decrement - the side 
            // table lock and has_sidetable_rc bit ensure that if everyone 
            // else tried to -release while we worked, the last one would block.
            sidetable_unlock();
            return false;
        }
        else {
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

    // Really deallocate.

    if (slowpath(newisa.deallocating)) {
        ClearExclusive(&isa.bits);
        if (sideTableLocked) sidetable_unlock();
        return overrelease_error();
        // does not actually return
    }
    newisa.deallocating = true;
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)) goto retry;

    if (slowpath(sideTableLocked)) sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    if (performDealloc) {
        // 发送 dealloc 消息
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}
    1. 判断是否是 nonpointer isa,如果不是直接操作散列表
    1. 如果是 nonpointer isa,则对 extra_rc 中的引用计数 -1,并存储标记 carry
    1. extra_rc 中的引用计数为 0,判断散列表中是否有引用计数,如果有执行引用计数 -1 操作
    1. 如果 extra_rc 中没有值,散列表中的也没有,则直接走析构
Release 流程图

dealloc 源码

通过 dealloc -> _objc_rootDealloc -> rootDealloc ->,查看源码实现如下

inline void
objc_object::rootDealloc()
{
    // 小对象,直接 return
    if (isTaggedPointer()) return;  // fixme necessary?

    // 没有这些,直接free
    if (fastpath(isa.nonpointer  &&          // isa
                 !isa.weakly_referenced  &&  //引用计数表
                 !isa.has_assoc  &&          //关联函数
                 !isa.has_cxx_dtor  &&       // C++
                 !isa.has_sidetable_rc))     // 散列表
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        //有
        object_dispose((id)this);
    }
}
    1. 如果是 Tagged Pointer,直接 return
    1. 判断是否有 isa、cxx、关联对象、弱引用表、引用计数表,没有的话,直接走 free 释放内存
    1. 如果有,进入下一步 object_dispose,源码如下:
id 
object_dispose(id obj)
{
    if (!obj) return nil;
    // 销毁实例
    objc_destructInstance(obj);    
    free(obj);

    return nil;
}
    1. 销毁实例
    1. 释放内存

objc_destructInstance 的源码如下

void *objc_destructInstance(id obj) 
{
    if (obj) {
        // Read all of the flags at once for performance.
        bool cxx = obj->hasCxxDtor();
        bool assoc = obj->hasAssociatedObjects();

        // This order is important.
        if (cxx) object_cxxDestruct(obj); //C++ 析构函数
        if (assoc) _object_remove_assocations(obj); // 删除关联引用
        obj->clearDeallocating();
    }

    return obj;
}

inline void 
objc_object::clearDeallocating()
{
    //判断是否为nonpointer isa
    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());
}

NEVER_INLINE void
objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));

    SideTable& table = SideTables()[this];
    table.lock();
    if (isa.weakly_referenced) {
        //清空弱引用表
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    if (isa.has_sidetable_rc) {
        //清空引用计数
        table.refcnts.erase(this);
    }
    table.unlock();
}
  • 调用c++析构函数
  • 删除关联引用
  • 释放散列表
  • 清空弱引用表

retainCount 源码分析

通过 retainCount -> _objc_rootRetainCount -> rootRetainCount 查看源码实现如下

inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    //判断是否是 nonpointer isa
    if (bits.nonpointer) {
        //是,才会走下层的处理
        uintptr_t rc = 1 + bits.extra_rc;
        //alloc创建的对象引用计数为0,包括sideTable,所以对于alloc来说,是 0+1=1,这也是为什么通过retaincount获取的引用计数为1的原因
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    //不是,正常返回
    sidetable_unlock();
    return sidetable_retainCount();
}

这里我们看一道面试题,如下代码,打印结果是什么?

NSObject *objc = [NSObject alloc];
NSLog(@"%ld", CFGetRetainCount((__bridge CFTypeRef)objc));

做过开发的人都知道,结果肯定是 1,但是为什么是 1呢?下面我们打个断点来验证下

alloc 创建的对象实际的引用计数为 0,但是它打印的结果为 1,因为在底层 rootRetainCount 方法中,引用计数默认加 1,这里只有对引用计数的读取操作,是没有写入操作的。简单来说就是:为了防止 alloc 创建的对象被释放(引用计数为 0 会被释放),所以在编译阶段,程序底层默认进行了 +1操作。实际上在 extra_rc 中的引用计数仍然为 0。

dealloc 流程图

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