美文网首页iOS 底层探索之路
iOS 底层探索:内存管理 (上)

iOS 底层探索:内存管理 (上)

作者: 欧德尔丶胡 | 来源:发表于2020-11-25 13:38 被阅读0次

iOS 底层探索: 学习大纲 OC篇

前言

  • 由于ARC的出现,内存管理在我们开发中其实经常被熟练的开发者忽视,但是经过对底层的深入研究,我们会发现苹果对内存的管理的处理会让我们学习到很多东西,其次移动设备的内存资源是有限的,当 App 运行时占用的内存大小超过了限制后,就会被强杀掉,从而导致用户体验被降低。所以,为了提升 App 质量,开发者要非常重视应用的内存管理问题。

  • 这篇开始由浅入深分析总结整理内存管理的内容

内容

一、内存布局

具体参考: iOS 底层探索:内存五大区

针对4GB虚拟内存

二、内存管理方案

不同场景下的内存方案有以下三种:

  • TaggedPointer:⼩对象-NSNumber,NSDate
  • NONPOINTER_ISA:⾮指针型isa
  • 散列表:引⽤计数表,弱引⽤表
1.TaggedPointer

先分析一个面试题

//MARK: - taggedPointer 面试题
- (void)taggedPointerDemo {
  
    self.queue = dispatch_queue_create("com.HJ.cn", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"HJ"];  // alloc 堆 iOS优化 - taggedpointer
             NSLog(@"%@ --- %d",self.nameStr , i);
        });
    }
}
//------完整的打印. 运行不报错
2020-11-24 14:22:04.326685+0800 002---taggedPointer[28847:474034] HJ --- 9995
2020-11-24 14:22:04.327784+0800 002---taggedPointer[28847:474074] HJ --- 9996
2020-11-24 14:22:04.332117+0800 002---taggedPointer[28847:474077] HJ --- 9997
2020-11-24 14:22:04.333909+0800 002---taggedPointer[28847:474068] HJ --- 9998
2020-11-24 14:22:04.334401+0800 002---taggedPointer[28847:474031] HJ --- 9999

//------打印. 运行报错
for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"HJ_内存管理的探索"];
            NSLog(@"%@",self.nameStr);
        });
    }

运行报错如下图


我们知道这段就崩溃了,在多线程的时候讨论过,这段崩溃是由于多线程同时setter /getter 导致的过度释放。

我们可以看到两段代码唯有唯有字符串长短不一样。为什么会这样呢?我们通过断点调试分析如下:

不崩溃 崩溃

我们可以看到 一个是NSTaggedPointerString ,一个是__NSCFString,这里就可以看出@"HJ" 是经过iOS优化过的,它变成了一个字符串常量NSTaggedPointerString

打印其地址如下:很明显他们存储在不同的区域。

    NSString * str1 = [NSString stringWithFormat:@"HJ"];
    NSString * str2 = [NSString stringWithFormat:@"HJ_内存管理的探索"];
    NSLog(@"%p-%@",str1,str1);
    NSLog(@"%p-%@",str2,str2);
   //真机下运行的
   // 0x8a9c70c41689beb6   -   HJ
   // 0x000000028234fcf0   -   HJ_内存管理的探索

我们进入源码查看retain和release方法如下:

id 
objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;
    return obj->retain();
}


__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;
    return obj->release();
}
  • 分析:
    很明显我们可以看到:if (obj->isTaggedPointer()) return; 如果obj->isTaggedPointer()就直接return,再次说明了苹果利用了TaggedPointer做了相应内存的处理。所以在上面的面试题中我们就能很好的解释,为什么第一个不会崩溃,因为它在多线程时,没有不断的进行retain和release的本质操作,所以不存在过度释放的可能,则第二个相反。

那么TaggedPointer 到底是什么呢???

还记得之前分析【类的加载下】read_image的时候如下:

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
   ....省略...
      initializeTaggedPointerObfuscator(); //初始化 TaggedPointer 模糊处理
   ....省略...
}
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 {
        // 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;
    }
}

可以看出在SDK10_14之后,经过了一次模糊处理:
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;其中#define _OBJC_TAG_MASK 1UL

为什么要做一次这个处理呢?

这里要说明一点小对象的引用,在iOS中一个对象8个字节,占8位就是64个字节,然后一个@"HJ" 或者@"1",这种需要占用这么多字节吗?很明显造成了浪费,所以就引用了小对象这个概念。在苹果推出了 采用64位架构的A7双核处理器 iphone 5s的时候,为了节省内存提高执行效率,苹果提出了Tagged Pointer的概念。

进入objc_debug_taggedpointer_obfuscator 我们可以看到进行了两次异或处理,类似于编码与解码的过程。

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

我们移植代码在模拟器上运行如下:

{
NSString *str2 = [NSString stringWithFormat:@"b"];
NSLog(@"%p-%@",str2,str2);
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(str2));
}
uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

0xbdc43db247c3b1b0 - b
0xa000000000000621

很明显 0xa000000000000621 这是一个非常简单的指针 ,分析如下:

  • 在苹果的64位OC实现中,若对象指针的二进制第一位是1,则该指针为Tagged Pointer。
    例如0xa000000000000621其中a的2进制为1010,第一位1表示这是Tagged Pointer010就是2表示这是一个NSString类;
#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,
    .....
  • 这个地址最后一位1表示字符串的数目,这里是0001表示有1位字符串;其中真正用来存储的位数只有中间的14位16进制。
  • bASCII码中查询对应的就是62
  • 说明 :这个地址本身其实就存储了字符串的值,可以说是存储在&strS内存中值,只是伪装成了地址,它不需要存储在数据区,也不需要申请堆空间

关于更深的字符串研究可以参考:采用Tagged Pointer的字符串

TaggedPointer总结:

    1. TaggedPointer专⻔⽤来存储⼩对象,例如NSNumberNSDate
    1. TaggedPointer指针不再是地址了,⽽是真正的值。所以,实际上它不再是⼀个对象了,它只是⼀个披着对象⽪的普通变量⽽已。所以,它的内存并不存储在堆中,也需要mallocfree
    1. 在内存读取上有着3倍的效率,创建时⽐以前快106倍
2. NONPOINTER_ISA 与 散列表

分析了iOS 底层探索:isa与类关联的原理,我们得出结论:在isa初始化obj->initInstanceIsa(cls, hasCxxDtor)的时候,通过isa_t联合体,在位域运算中,将类信息cls存进了存储类的指针的值shiftclx , 最后isa = newisa;isa中既有类的指针,又有类的信息,这个过程也叫做指针优化:NONPOINTER_ISA,所以isa其实不仅仅是一个指针,其中一些位仍然存储了每个对象的信息,例如:引用计数是否被弱引用。仔细一想,NONPOINTER_ISA的设计思想是不是和TaggetPointer类似呢!

再次查看isa的本质,再次看看哪里存了引用计数等信息呢?

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

找到ISA_BITFIELD的结构体信息如下


重点
  • nonpointer:表示是否对isa开启指针优化 。0代表是纯isa指针,1代表除了地址外,还包含了类的一些信息、对象的引用计数等。

  • extra_rc:表示该对象的引用计数值,实际上是引用计数减一。例如:如果引用计数为10,那么extra_rc为9。如果引用计数大于10,则需要使用has_sidetable_rc

  • has_sidetable_rc:当对象引用计数大于10时,则需要进位

其中has_sidetable_rc就引入了散列表的概念:

在runtime内存空间中,SideTables是一个hash数组,里面存储了SideTableSideTables的hash键值就是一个对象obj的address。 因此可以说,一个obj,对应了一个SideTable。但是一个SideTable,会对应多个obj。因为SideTable的数量有限,所以会有很多obj共用同一个SideTable

我们先看看SideTable的结构

struct SideTable {
    spinlock_t slock;      // 自旋锁,用于上锁/解锁 SideTable。
    RefcountMap refcnts;    //用来存储OC对象的引用计数的 hash表 (仅在未开启isa优化或在isa优化情况下isa_t的引用计数溢出时才会用到)。
    weak_table_t weak_table;  //弱引用表 存储对象弱引用指针的 hash表 。是OC中weak功能实现的核心数据结构。
     ......
}

目前有了 NONPOINTER_ISA散列表初步概念之后,我们就去看看在内存管理ARC&MRC中是如何使用的呢?

三、ARC&MRC

我们早已经进入ARC(ARC是LLVM和Runtime配合的结果)时代,那些MRC的retain,release等操作已经消失在我们视野中了,绝大多数的内存管理细节已经由编译器代劳了。但是研究内存管理,还是逃不过他们。关于ARC&MRC定义,我建议参考这篇内容:iOS内存管理(MRC、ARC)深入浅出这里我不做过多的说明,我们主要来探索以下几个常用方法的本质:

1.alloc

关于alloc我们以前分析过,具体可见 iOS 底层探索:alloc & init
这里贴一个流程图以便复习:

alloc基本流程图

2.reatinCount

reatinCount:也就是引用计数。操作引用计数的方法:retainrelease

在非 ARC 环境可以使用retainCount方法获取某个对象的引用计数,其会调用 objc_objectrootRetainCount()方法


- (NSUInteger)retainCount {
    return _objc_rootRetainCount(self);
}

uintptr_t
_objc_rootRetainCount(id obj)
{
    ASSERT(obj);

    return obj->rootRetainCount();
}

在 ARC 时代除了使用Core Foundation库的CFGetRetainCount()方法,也可以使用Runtime_objc_rootRetainCount(id obj) 方法来获取引用计数,此时需要引入头文件。这个函数也是调用 objc_objectrootRetainCount()方法:

// rootRetainCount() 方法对引用计数存储逻辑进行了判断
inline uintptr_t 
objc_object::rootRetainCount()
{
    if (isTaggedPointer()) return (uintptr_t)this;

    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    if (bits.nonpointer) {
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

rootRetainCount()方法对引用计数存储逻辑进行了判断,

  • 如果是TaggedPointer,可以直接获取引用计数;
  • 如果不是TaggedPointer, 调用的sidetable_retainCount()方法:

注 : 这里暂时不讲解sidetable_retainCount()等方法,在下面讲解retainrelease可操作引用计数的的时候穿插理解。

3.retain

retain:MRC下 ,作为修饰属性的关键字,该属性在赋值的时候,先release之前的值,然后再赋新值给属性,引用再加1。 如果手动调用会对引用计数加1

///在MRC里,retain关键字表示会由编译器帮我们生成ivar和set/get方法,生成的set方法里自带内存管理

@property (nonatomic, retain) Dog * dog;

 (void)setDog:(Dog *)dog;

//编译时自动生成的代码
- (void)setDog:(Dog *)dog {
    //这里必须要判断_dog != dog
    if (_dog != dog) {
         //先release之前的值
        [_dog release];
         //然后再赋新值给属性,引用再加1
        _dog = [dog retain];
    }
}

- (Dog *)dog {
    return _dog;
}

我们可以查看retain的调用栈,也可以直接进入源码中查找如下:

+ (id)retain {
    return (id)self;
}

// Replaced by ObjectAlloc
- (id)retain {
    return _objc_rootRetain(self);
}

NEVER_INLINE id
_objc_rootRetain(id obj)
{
    ASSERT(obj);

    return obj->rootRetain();
}

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

核心源码rootRetain的解析:

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    // 如果如果是taggedPointer直接返回不需要引用计数
    if (isTaggedPointer()) return (id)this;
    // 默认不使用sideTable
    bool sideTableLocked = false;
    // 是否需要将引用计数转到sidetable
    bool transcribeToSideTable = false;

    // 记录新旧两个isa指针
    isa_t oldisa;
    isa_t newisa;

    do {
        transcribeToSideTable = false;
         通过 LoadExclusive 方法加载 isa 的值,加锁
        oldisa = LoadExclusive(&isa.bits);
        // 此时 newisa = oldisa
        newisa = oldisa;
        // slowpath表示if中的条件是小概率事件
        // 如果newisa(此时和oldisa相等) 如果没有采用isa优化
        if (slowpath(!newisa.nonpointer)) {
            // 解锁
            ClearExclusive(&isa.bits);
            //rawISA() = (Class)isa.bits
            // 如果当前对象的 isa 指向的类对象是元类(也就是说当前对象不是实例对象,而是类对象),直接返回
            if (rawISA()->isMetaClass()) return (id)this;
            // 如果不需要retain对象(引用计数+1) 且sideTable是锁上的
            if (!tryRetain && sideTableLocked)
                // sidetable解锁
                sidetable_unlock();
            if (tryRetain)
                // sidetable_tryRetain 尝试对引用计数器进行+1的操作 返回+1操作是否成功
                return sidetable_tryRetain() ? (id)this : nil;
            else
                // 将sidetable中保存的引用计数+1同时返回引用计数
                return sidetable_retain();
        }
        // 如果需要尝试 +1 但是当前对象正在销毁中
        if (slowpath(tryRetain && newisa.deallocating)) {
            // 解锁
            ClearExclusive(&isa.bits);
            // 如果不需要去尝试 +1 并且 SideTables 表锁住了,就将其解锁
            // 这里的条件 应该永远都不会被满足
            if (!tryRetain && sideTableLocked)
                sidetable_unlock();
            // 如果对象正在被释放 执行retain是无效的
            return nil;
        }
        // 引用计数是否溢出标志位
        uintptr_t carry;
        //为 isa 中的 extra_rc 位 +1 ,并保存引用计数
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
        // 如果 isa中的extra_rc 溢出
        if (slowpath(carry)) {
            // newisa.extra_rc++ 溢出
            // 是否需要处理溢出 这个变量是rootRetain函数外部传入的参数 是否需要处理溢出时的情况
            if (!handleOverflow) {
                //解锁
                ClearExclusive(&isa.bits);
                // rootRetain_overflow 方法实际上就是递归调用了当前方法只是将handleOverflow
                // 置为yes
                return rootRetain_overflow(tryRetain);
            }
            // 保留isa中extra_rc一半的值 将另一半转移到sidetable中
            // 如果不需要尝试 +1 并且 sidetable 表未加锁,就将其加锁
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            // sidetable加锁
            sideTableLocked = true;
            // 需要将引用计数转移到sidetable
            transcribeToSideTable = true;
            // 将newisa中的引用计数置为之前的一半 # define RC_HALF  (1ULL<<18)
            newisa.extra_rc = RC_HALF;
            // isa中是否使用sidetable存储retiancount的标志位置为1
            newisa.has_sidetable_rc = true;
        }
        //while循环开始 直到 isa.bits 中的值被成功更新成 newisa.bits
        // StoreExclusive(uintptr_t *dst, uintptr_t oldvalue, uintptr_t value)
        // 将更新后的newisa的值更新到isabit中
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

    // 如果需要转移引用计数到sidetable中
    if (slowpath(transcribeToSideTable)) {
        // 将溢出的引用计数加到 sidetable 中
        sidetable_addExtraRC_nolock(RC_HALF);
    }
    // 如果不需要去尝试 +1 并且 SideTables 表锁住了,就将其解锁
    if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    // 返回当前对象 引用计数已完成+1操作
    return (id)this;
}

根据对isa的理解和rootRetain源码的解读,接下来我们会分以下四种情况rootRetain进行分析。

3.1 如果没有采用isa优化

在这里我们假设的条件:!newisa.nonpointer

objc_object::rootRetain(bool tryRetain, bool handleOverflow) {

    isa_t oldisa;
    isa_t newisa;

    do {
       transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
       if (slowpath(!newisa.nonpointer)) {
            // 解锁
            ClearExclusive(&isa.bits);
            //rawISA() = (Class)isa.bits
            // 如果当前对象的 isa 指向的类对象是元类(也就是说当前对象不是实例对象,而是类对象),直接返回
            if (rawISA()->isMetaClass()) return (id)this;
            // 如果不需要retain对象(引用计数+1) 且sideTable是锁上的
            if (!tryRetain && sideTableLocked)
                // sidetable解锁
                sidetable_unlock();
            if (tryRetain)
                // sidetable_tryRetain 尝试对引用计数器进行+1的操作 返回+1操作是否成功
                return sidetable_tryRetain() ? (id)this : nil;
            else
                // 将sidetable中保存的引用计数+1同时返回引用计数
                return sidetable_retain();
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
    return (id)this;
}
  • 3.1.1 这里没有优化isa的情况下,并且 tryRetain = false 我们全局搜索rootRetain(,我们会发现,只有当执行rootTryRetain(),才会tryRetain = true,源码如下:
objc_object::rootTryRetain()
{
    return rootRetain(true, false) ? true : false;
}

那么谁引用了rootTryRetain()? 再次搜索会发现在objc_loadWeakRetained这个函数中调用了。看名字就知道这里使用了weak弱引用计数。这里后面再分析。

  • 3.1.2. 这里没有优化isa的情况下,并且 tryRetain = false ,那么它会走sidetable_retain()这个函数,进入源码分析如下:
// 将 SideTable 表中的引用计数 +1
id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    // 根据对象获取 存储引用计数的sidetable
    SideTable& table = SideTables()[this];
    
    table.lock();
    // 获取sidetable中存储的引用计数值
    size_t& refcntStorage = table.refcnts[this];
    // 如果引用计数值没有溢出
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        // 引用计数值+SIDE_TABLE_RC_ONE
        // #define SIDE_TABLE_RC_ONE            (1UL<<2)
        // SIDE_TABLE_RC_ONE = 4 为什么这里会+4我们下面会介绍
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    table.unlock();

    return (id)this;
}

通过上面这个方法,我们对引用计数器完成了+1(实际上是+4)的操作,那么这里为什么会+4呢?
那是因为对于table.refcnts,实际上并不完全是表示引用计数的值,refcnts的最后两位有特殊的标示意义:

#define SIDE_TABLE_WEAKLY_REFERENCED (1UL<<0)
#define SIDE_TABLE_DEALLOCATING      (1UL<<1) 
倒数第一位标记当前对象是否被weak指针指向(1:有weak指针指向);
倒数第二位标记当前对象是否正在销毁状态(1:处在正在销毁状态);

因此,我们每次执行retain方法时,虽然每次都是+4,但是对于引用计数真实的值来说就是+1 , 64位环境下只有62位是保存溢出的引用计数的.

3.2 正常的 rootRetain(足以保存引用计数)

在这里我们假设的条件:isa 中的 extra_rc的位数足以存储retainCount

//简化rootRetain 
objc_object::rootRetain(bool tryRetain, bool handleOverflow) {

    isa_t oldisa;
    isa_t newisa;

    do {
       transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;

        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
    return (id)this;
}

方法执行的流程如下:

1.使用 LoadExclusive 加载 isa 的值
2.调用 addc(newisa.bits, RC_ONE, 0, &carry) 方法将 isa 的值加一
3.调用 StoreExclusive(&isa.bits, oldisa.bits, newisa.bits) 更新 isa 的值
4.返回当前对象

3.3 rootRetain(extra_rc不足以保存引用计数)

在这里我们假设的条件:调用 addc 方法为extra_rc时,8 位的 extra_rc 可能不足以保存引用计数。

//简化rootRetain 
objc_object:::rootRetain(bool tryRetain, bool handleOverflow) {
    .....省略......
   do {
    transcribeToSideTable = false;
    oldisa = LoadExclusive(&isa.bits);
    newisa = oldisa;
    .....省略......
    uintptr_t carry;
    newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);
   // 是否需要处理溢出 这个变量是rootRetain函数外部传入的参数 是否需要处理溢出时的情况
    if (carry && !handleOverflow)
      // rootRetain_overflow 方法实际上就是递归调用了当前方法只是将handleOverflow
      // 置为yes
        return rootRetain_overflow(tryRetain);
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));
}

extra_rc不足以保存引用计数,并且handleOverflow =false

当方法传入的 handleOverflow =false 时(这也是通常情况),我们会调用 rootRetain_overflow 方法

NEVER_INLINE id 
objc_object::rootRetain_overflow(bool tryRetain)
{
    return rootRetain(tryRetain, true);
}

这个方法其实就是重新执行rootRetain 方法,并传入handleOverflow =true

  • 拓展一个面试题: 下面打印是多少?
    NSObject *objc = [NSObject alloc];
    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc));// 1
    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc)); // 1
    NSLog(@"%ld",CFGetRetainCount((__bridge CFTypeRef)objc)); // 1

    // alloc 引用计数为多少 : 0
    // extrc
    // + 1

这个题实际上是问objc的引用计数是多少,答案是都是1

分析流程如下:

- (NSUInteger)retainCount {
    return _objc_rootRetainCount(self);
}

uintptr_t
_objc_rootRetainCount(id obj)
{
    ASSERT(obj);

    return obj->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) {
        //alloc创建的对象引用计数为0,包括sideTable,所以对于alloc来说,是 0+1=1,这也是为什么通过retaincount获取的引用计数为1的原因
        uintptr_t rc = 1 + bits.extra_rc;
        if (bits.has_sidetable_rc) {
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }
    //如果不是,则正常返回
    sidetable_unlock();
    return sidetable_retainCount();
}

因为alloc 不做引用计数加1的操作,为0 ,但是在读取器引用计数的时候会出现调用CFGetRetainCount时实际调用了retainCount,最后到rootRetainCount,其中rc = 1 + bits.extra_rc。只做了读取没有写入,所以后面读取的都是1。为了防止alloc创建的对象被释放(引用计数为0会被释放),所以在编译阶段,程序底层默认进行了+1操作。实际上在extra_rc中的引用计数仍然为0

3.4 rootRetain(处理引用计数的溢出)

当传入的 handleOverflow=true 时,我们就会在 rootRetain方法中处理引用计数的溢出。

//简化rootRetain 
objc_object::rootRetain(bool tryRetain, bool handleOverflow) {

    .....省略......
    isa_t oldisa;
    isa_t newisa;

    do {
       transcribeToSideTable = false;
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
        .....省略......
        uintptr_t carry;

        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
       // 如果 isa中的extra_rc 溢出
        if (slowpath(carry)) {
            .....省略......
            // 保留isa中extra_rc一半的值 将另一半转移到sidetable中
            // 如果不需要尝试 +1 并且 sidetable 表未加锁,就将其加锁
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            // sidetable加锁
            sideTableLocked = true;
            // 需要将引用计数转移到sidetable
            transcribeToSideTable = true;
            // 将newisa中的引用计数置为之前的一半 # define RC_HALF  (1ULL<<18)
            // 我们都知道NSTaggedPointer预留了19个bit位用来存放引用计数,RC_HALF的值刚好为 2^19 次方的一半。
            newisa.extra_rc = RC_HALF;
           // isa中是否使用sidetable存储retiancount的标志位置为1
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits)));

// 如果需要转移引用计数到sidetable中
 if (slowpath(transcribeToSideTable)) {
        // Copy the other half of the retain counts to the side table.
     //拷贝 另外一半的 引用计数到 side table
     //分一半的原因: 减少散列表的操作次数,如果release的时候可以直接在ExtraRC 上操作了。
        sidetable_addExtraRC_nolock(RC_HALF);
    }
     .....省略......
// 返回当前对象 引用计数已完成+1操作
    return (id)this;
}

当调用这个方法,并且 handleOverflow = true 时,我们就可以确定carry一定是存在的了,

因为extra_rc 已经溢出了,所以要更新它的值为 RC_HALF(define RC_HALF (1ULL<<7)),extra_rc总共为 8 位,RC_HALF = 0b10000000

此时设置 has_sidetable_rc 为真,存储新的isa的值之后,调用 sidetable_addExtraRC_nolock 方法。

// Move some retain counts to the side table from the isa field.
// Returns true if the object is now pinned.
// 将isa中的引用计数移动到sidetable中 当引用计数达到最大值(溢出)是返回true
bool 
objc_object::sidetable_addExtraRC_nolock(size_t delta_rc)
{
// 根据对象地址获取到存放引用计数对应的sidetable
    SideTable& table = SideTables()[this]; //散列表数组
// 从table.refcnts中获取当前对象的引用计数
    size_t& refcntStorage = table.refcnts[this];
// 声明一个局部变量存储旧的引用计数
    size_t oldRefcnt = refcntStorage;

// 这时候 引用计数已经超过了三十二位所能表达的最大值 直接返回true
    if (oldRefcnt & SIDE_TABLE_RC_PINNED) return true;
// 溢出标志位
    uintptr_t carry;
//------- 这里我们将溢出的一位 RC_HALF 添加到 oldRefcnt 中 ----
    size_t newRefcnt = 
        addc(oldRefcnt, delta_rc << SIDE_TABLE_RC_SHIFT, 0, &carry);
// 如果引用计数溢出 设置标识为已满
    if (carry) {
        // 如果是32位的情况 SIDE_TABLE_RC_PINNED = 1<< (32-1)
        // int的最大值 SIDE_TABLE_RC_PINNED = 2147483648
        //  SIDE_TABLE_FLAG_MASK = 3
        // refcntStorage = 2147483648 | (oldRefcnt & 3)
        // 如果溢出,直接把refcntStorage 设置成最大值
        refcntStorage =
            SIDE_TABLE_RC_PINNED | (oldRefcnt & SIDE_TABLE_FLAG_MASK);
        return true;
    }
    else {
      // 如果没有溢出 那么直接将新的引用计数赋值给refcntStorage
        refcntStorage = newRefcnt;
        return false;
    }
}

进入散列表的操作: SideTables()

static objc::ExplicitInit<StripedMap<SideTable>> SideTablesMap;

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

class StripedMap {
#if TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
    enum { StripeCount = 8 };  // iPhone时这个值为8
#else
    enum { StripeCount = 64 }; //否则为64
#endif
   struct PaddedT {
        T value alignas(CacheLineSize);
    };
 
    PaddedT array[StripeCount];
 
    static unsigned int indexForPointer(const void *p) {
        //这里是做类型转换
        uintptr_t addr = reinterpret_cast<uintptr_t>(p);
 
        //这就是哈希算法了
        return ((addr >> 4) ^ (addr >> 9)) % StripeCount;
    }
public:
    T& operator[] (const void *p) { 
        //返回sideTable
        return array[indexForPointer(p)].value; 

}
  • 可以看到,通过indexForPointe r,在对StripeCount取余后,通过哈希函数来获取到了sideTable的下标,然后再根据value取到所需的sideTable

  • 我们通过 table.refcnts[this]StripeCount = 8 可以看出是用SideTables去管理多个SideTable,那么可以提出疑问:为什么为什么不直接用一张SideTable,而是用SideTables去管理多个SideTable

回答 : SideTable里有一个自旋锁,如果把所有的类都放在同一个SideTable,有任何一个类有改动都会对整个table做操作,并且在操作一个类的同时,操作别的类会被锁住等待,这样会导致操作效率和查询效率都很低。而有多个SideTable的话,操作的都是单个Table,并不会影响其他的table,这就是分离锁。

流程图如下:

综上分析:在 iOS 的内存管理中,我们使用了isa结构体中的extra_rcSideTable 来存储某个对象的自动引用计数。

rootRetain分析中我们看到如果tryRetain && newisa.deallocating 为真,就会ClearExclusive(&isa.bits) 并且return nil,自此我们开始引入dealloc的分析

4. dealloc

dealloc执行时机 :当一个对象retain count为0 (不再有强引用指向)时会触发dealloc。
dealloc 方法的作用是什么 ?就是释放当前接收器占用的内存空间,暴露出来是提供给开发者重写的 ,用来释放编码过程中用到的通知、Observer 等需要手动管理释放的操作方法。

dealloc 的源码分析:

void
_objc_rootDealloc(id obj)
{
    ASSERT(obj);

    obj->rootDealloc();
}

核心源码 : rootDealloc

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return;  // isTaggedPointer 为真直接返回

    if (fastpath(isa.nonpointer  &&   //是否优化过isa指针
                 !isa.weakly_referenced  &&   //是否存在弱引用指向
                 !isa.has_assoc  &&  //是否设置过关联对象
                 !isa.has_cxx_dtor  &&   //是否有cpp的析构函数(.cxx_destruct)
                 !isa.has_sidetable_rc))  //引用计数器是否过大无法存储在isa中
    {
        assert(!sidetable_present());
        free(this); // 都为空直接释放
    } 
    else {
        object_dispose((id)this);  // 如果不是进入object_dispose
    }
}

直接free 这个比较简单,我们直接分析 object_dispose()源码:

id 
object_dispose(id obj)
{
    if (!obj) return nil;

    objc_destructInstance(obj);     // 销毁实例
    free(obj);  // 释放对象
    return nil;
}
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); //有析构函数就释放,清楚成员变量
        if (assoc) _object_remove_assocations(obj); //移除当前对象的关联对象
        obj->clearDeallocating(); //将当前对象的指针指为nil
    }

    return obj;
}
inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {
        // Slow path for raw pointer isa.
        //如果是优化过的isa 先取出sideTable拿到引用表和当前对象 ,然后执行weak_clear_no_lock()
        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.
        //如果引用计数表中还有数据,就会擦除引用计数表中的数据 执行 table.refcnts.erase()
        clearDeallocating_slow();
    }
}

整体流程图如下:


dealloc流程图

5.release

release作用 : 使引用计数器值-1
release源码如下:

inline void
objc_object::release()
{
    ASSERT(!isTaggedPointer());
   // 如果没有自定义的release方法 就直接系统默认调用rootRelease
    if (fastpath(!ISA()->hasCustomRR())) {
        sidetable_release();
        return;
    }
   // 如果有自定义的release方法那么调用对象的release方法
    ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
}

objc_object::rootRelease()
{
    return rootRelease(true, false);
}

核心源码rootRelease

// 两个参数分别是 是否需要调用dealloc函数,是否需要处理 向下溢出的问题
ALWAYS_INLINE bool 
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
    // 如果是TaggedPointer 不需要进行release操作
    if (isTaggedPointer()) return false;
    // 局部变量sideTable是否上锁 默认false
    bool sideTableLocked = false;
   // 两个局部变量用来记录这个对象的isa指针
    isa_t oldisa;
    isa_t newisa;

 retry:
    do {
     // 加载这个isa指针
        oldisa = LoadExclusive(&isa.bits);
        newisa = oldisa;
       // 如果没有进行nonpointer优化
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
           // 如果是类对象直接返回false 不需要释放
            if (rawISA()->isMetaClass()) return false;
           // 如果sideTableLocked 则解锁 这里默认是false
            if (sideTableLocked) sidetable_unlock();
           // 调用sidetable_release 进行引用计数-1操作
            return sidetable_release(performDealloc);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
       // 溢出标记位
        uintptr_t carry;
       // newisa 对象的extra_rc 进行-1操作
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
       // 如果-1操作后 向下溢出了 结果为负数
        if (slowpath(carry)) {
            // don't ClearExclusive()
            // 调用underflow 进行向下溢出的处理
            goto underflow;
        }
    //  循环,直到 isa.bits 中的值被成功更新成 newisa.bits
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, 
                                             oldisa.bits, newisa.bits)));

   //走到这说明引用计数的 -1 操作已完成
    if (slowpath(sideTableLocked)) sidetable_unlock();
    return false;

underflow:
    {
       ......省略........
   }
}

我们发现rootRelease里的内容基本和rootRetain反过来,主要是由两个内部方法retry,underflow组成,下面我们来一步步的整理下引用计数-1操作的具体步骤

同理rootDealloc ,分为以下几种情况

5.1 如果没有采用isa优化

假设条件:如果这个对象没有nonpointer优化,且不是一个类对象,那么我们直接通过对sidetable进行-1操作 ,直接return sidetable_release(performDealloc);

// 将 SideTable 表中的引用计数 -1
uintptr_t
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    // 根据对象地址获取SideTable
    SideTable& table = SideTables()[this];
    // 是否需要执行dealloc方法 默认是false
    bool do_dealloc = false;

    table.lock();
    // 获取当前对象的销毁状态 方法的返回值有2个
    // 引用计数和当前对象是否已存在与map中
    auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
    auto &refcnt = it.first->second;
    // 如果当前对象之前不存在与map中
    if (it.second) {
        do_dealloc = true;
    } else if (refcnt < SIDE_TABLE_DEALLOCATING) {
        // 如果引用计数的值小于 SIDE_TABLE_DEALLOCATING = 2(0010)
        // refcnt 低两位分别是SIDE_TABLE_WEAKLY_REFERENCED 0  SIDE_TABLE_DEALLOCATING 1
        // 这个对象需要被销毁
        do_dealloc = true;
        refcnt |= SIDE_TABLE_DEALLOCATING;
    } else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
        // 如果引用计数有值且未溢出那么-1
        refcnt -= SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    // 如果需要执行dealloc 那么就调用这个对象的dealloc
    if (do_dealloc  &&  performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return do_dealloc;
}
5.2 如果有采用isa优化

假设条件:如果该对象做了nonpointer优化,那么我们直接对extra_rc进行-1操作,操作如下:

// newisa 对象的extra_rc 进行-1操作
newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc--
// 如果-1操作后 向下溢出了 结果为负数
if (slowpath(carry)) {
    // don't ClearExclusive()
    // 调用underflow 进行向下溢出的处理
    goto underflow;
}

其中subc()extra_rc计数-1

  • 【情况1】: 如果发现-1操作之后,extra_rc的个数为0,那么就出现了向下溢出,执行goto underflow;,我们需要将sideTable中的部分引用计数拿到extra_rc中记录。
  • 【情况2】: 如果没有向下溢出,执行sidetable_unlock(),那么我们就直接将修改后的newisa同步到isa中即完成了release操作。
underflow的具体流程分析
underflow:
    //newisa的extra_rc在执行-1操作后导致了向下溢出
    // 放弃对newisa的修改 使用之前的oldisa
    newisa = oldisa;

    // 如果 isa 的 has_sidetable_rc 标志位标识引用计数已溢出
    // has_sidetable_rc 用于标识是否当前的引用计数过大,无法在isa中存储,
    // 而需要借用sidetable来存储。(这种情况大多不会发生)
    if (slowpath(newisa.has_sidetable_rc)) {
        // 是否需要处理下溢
        if (!handleUnderflow) {
            // 清除原 isa 中的数据的原子独占
            ClearExclusive(&isa.bits);
            // 如果不需要处理下溢 直接调用 rootRelease_underflow方法
            return rootRelease_underflow(performDealloc);
        }

        // 如果sidetable是上锁状态
        if (!sideTableLocked) {
            // 解除清除原 isa 中的数据的原子独占
            ClearExclusive(&isa.bits);
            // sidetable 上锁
            sidetable_lock();
            sideTableLocked = true;
            // 跳转到 retry 重新开始,避免 isa 从 nonpointer 类型转换成原始类型导致的问题
            goto retry;
        }

        // sidetable_subExtraRC_nolock 放回要从sidetable移动到isa的extra_rc的值
        // 默认是获取extra_rc可存储的长度一半的值
        size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

        //  为了避免冲突 has_sidetable_rc 标志位必须保留1的状态 及时sidetable中的个数为0
        if (borrowed > 0) {
            // 将newisa中引用计数值extra_rc 设置为borrowed - 1
            // -1 是因为 本身这次是release操作
            newisa.extra_rc = borrowed - 1;
            // 然后将修改同步到isa中
            bool stored = StoreReleaseExclusive(&isa.bits, 
                                                oldisa.bits, newisa.bits);
            // 如果保存失败
            if (!stored) {
                // dropped the reservation.
                // 从新装载isa
                isa_t oldisa2 = LoadExclusive(&isa.bits);
                isa_t newisa2 = oldisa2;
                // 如果newisa2是nonpointer类型
                if (newisa2.nonpointer) {
                    // 下溢出标志位
                    uintptr_t overflow;
                    // 将从 SideTables 表中获取的引用计数保存到 newisa2 的 extra_rc 标志位中
                    newisa2.bits = 
                        addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);
                    //
                    if (!overflow) {
                        // 如果没有溢出再次将 isa.bits 中的值更新为 newisa2.bits
                        stored = StoreReleaseExclusive(&isa.bits, oldisa2.bits, 
                                                       newisa2.bits);
                    }
                }
            }

            // 如果重试之后依然失败
            if (!stored) {
                // 将从sidetable中取出的引用计数borrowed 重新加到sidetable中
                sidetable_addExtraRC_nolock(borrowed);
                // 重新尝试
                goto retry;
            }
            // 完成对 SideTables 表中数据的操作后,为其解锁
            sidetable_unlock();
            return false;
        }
        else {
            // 在从Side table拿出一部分引用计数之后 Side table为空
            // Side table is empty after all. Fall-through to the dealloc path.
        }
    }

    // 如果当前的对象正在被释放
    if (slowpath(newisa.deallocating)) {
        ClearExclusive(&isa.bits);
        // 如果sideTableLocked被锁 那么解锁
        if (sideTableLocked) sidetable_unlock();
        // 兑现被过度释放
        return overrelease_error();
        // does not actually return
    }
    // 将对象被释放的标志位置为true
    newisa.deallocating = true;
    // 将newisa同步到isa中 如果失败 进行重试
    if (!StoreExclusive(&isa.bits, oldisa.bits, newisa.bits))
        goto retry;

    // 如果sideTableLocked= true
    if (slowpath(sideTableLocked))
        // Side table解锁
        sidetable_unlock();

    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    // 如果需要执行dealloc方法 那么调用该对象的dealloc方法
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
  • 先判断has_sidetable_rc是否有sidetable引用计数,如果有我们要确认是否需要处理向下溢出,如果不需要处理向下溢出,那么我们直接调用rootRelease_underflow方法,
NEVER_INLINE uintptr_t
objc_object::rootRelease_underflow(bool performDealloc)
{
    return rootRelease(performDealloc, true);
}

很明显这个方法实际上与retain操作时处理溢出逻辑相同,将rootRelease方法中的handleUnderflow参数置为true,要处理向下溢出。

  • 需要处理向下溢出时,如果当前的sidetable处于未上锁的状态时,将sidetable上锁然后进行重试,如果sidetable已经上锁了,那么我们会执行下面这句代码
size_t borrowed = sidetable_subExtraRC_nolock(RC_HALF);

sidetable_subExtraRC_nolock 返回要从sidetable移动到isaextra_rc的值,默认是获取extra_rc可存储的长度一半的值。

  • 如果此时从sidetable中拿到的值 > 0,那么我们要将这部分值放到isaextra_rc中进行存储,如果取到的borrowed的值为0,那么说明sidetable中的引用计数为0,那么我们直接释放该对象即可。
StoreReleaseExclusive

上面说到如果从sidetable中获取到的值borrowed大于0,那么我们直接将newisa.extra_rc设置为borrowed - 1即可。然后我们在调用StoreReleaseExclusive方法将newisa同步到isa中。

  1. 如果这里StoreReleaseExclusive方法保存失败了,那么我们需要重新调用LoadExclusive重新声明两个变量newisa2,oldisa2。通过addc方法将extra_rc置为borrowed-1

  2. 执行:newisa2.bits = addc(newisa2.bits, RC_ONE * (borrowed-1), 0, &overflow);

  3. 然后再次调用StoreReleaseExclusive方法将newisa2的改动同步到isa中。

  4. 如果StoreReleaseExclusive方法依然保存失败,那么我们就把从sidetable中获取的borrowed重新加到sideTable中。然后调用retry方法。

  5. 经过StoreReleaseExclusive这一步,引用计数更新操作完成。

  • 如果引用计数更新成功,那么我们需要先判断,当前对象是否正在被释放,如果正在被释放 那么调用过度释放方法overrelease_error

  • 当一个用户正在被释放时,再次调用release方法时会导致crash。 (过渡释放crash的由来)

overrelease_error

这个方法主要是定义了crash信息:

NEVER_INLINE uintptr_t
objc_object::overrelease_error()
{
    _objc_inform_now_and_on_crash("%s object %p overreleased while already deallocating; break on objc_overrelease_during_dealloc_error to debug", object_getClassName((id)this), this);
    objc_overrelease_during_dealloc_error();
    return 0; // allow rootRelease() to tail-call this
}
  • 如果当前对象没有被正在释放,那么我们将当前对象正在被释放标志位置为true newisa.deallocating = true; 同时将状态的更新同步到isa中。如果同步失败,那么会重复走一次retry。

  • 最后,更新状态成功后,对sidetable的操作也结束了,我们就可以将sidetable解锁,如果需要执行dealloc方法,那么我们调用dealloc进行对象释放

6.autorelease

----- 下一篇着重分析 --------

相关文章

网友评论

    本文标题:iOS 底层探索:内存管理 (上)

    本文链接:https://www.haomeiwen.com/subject/amliiktx.html