iOS底层 -- 内存管理底层分析

1,052 阅读27分钟

欢迎阅读iOS底层系列(建议按顺序)

iOS底层 - alloc和init探索

iOS底层 - 包罗万象的isa

iOS底层 - 类的本质分析

iOS底层 - cache_t流程分析

iOS底层 - 方法查找流程分析

iOS底层 - 消息转发流程分析

iOS底层 - dyld是如何加载app的

iOS底层 - 类的加载分析

iOS底层 - 分类的加载分析

iOS探索 - 多线程之相关原理

iOS探索 - 多线程之GCD应用

iOS探索 - 多线程之底层篇

iOS探索 - block原理

本文主要说明iOS的内存优化方案,从底层探索系统优化内存的方式等。

1. ROM和RAM

ROM只读存储器,是内部存储器的一种。它用来存储手机系统文件、图片、软件等等,不会随着掉电而丢失数据,ROM越大存储的数据就越多。

RAM随机存取存储器,是内部存储器最重要的一种,我们常称为运行内存(物理内存地址)。它的运行速度是比较快的,什么时候需要数据,就从ROM读取数据加入内存,但同时RAM断电会丢失数据,所以手机断电了会丢失原来正在运行的数据。RAM内存越大,能同时执行的程序就越多,性能一般是越好的。

我们常说的内存管理,内存优化,指的是RAM

2.内存分区

16101390-f1ab0d01f640b860.webp

  • 内核区:内核模块使用的区域。一般4GB的设备,系统会使用1GB留给内核区。

  • 栈区:从高地址向低地址延伸,所以汇编中开辟栈空间使用sub指令,且地址空间是连续的。它用来存储局部变量,函数跳转时的现场保护多余参数等。它是由系统管理的,在app启动时就确定了大小,压栈超过固定大小会报栈溢出错误。所以大量的局部变量,深递归可能耗尽栈内存而造成程序崩溃。

  • 堆区:从低地址向高地址延伸,系统使用链表来管理此空间,所以它的地址空间是不连续的。堆的空间比较大且是动态变化的, 一般由程序员管理。

  • 全局区:初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后由系统自动释放。

  • 常量区:存放一些常量字符串,程序结束后由系统自动释放。

  • 代码区:存放编译后的代码数据。

  • 保留区:系统保留区域

其中,代码区常量区全局区在APP启动时地址已固定,因此不会因为这些区的指针为空而产生崩溃性的错误。而堆的创建销毁,栈的压栈出栈,导致堆栈的空间时刻都在变化,所以当使用一个指针指向这两个区里面的内存时,一定要注意内存是否已经被释放,否则会产生程序崩溃。

那为什么栈区的速度比堆区快?

因为当访问一个常规对象时,堆栈都参与了工作,需要先找到栈区存储的指向对象地址的指针,根据指针在找到堆区的对象。

而栈区的数据是直接通过寄存器访问的,所以栈区的速度比堆区快。

3.系统内存管理方案

内存是所有程序都使用的重要系统资源,系统一定会采取诸多的内存管理方案来优化内存。比如VM,NONPOINTER_ISA,TaggedPointer,ARC,autoreleasePool等。

3.1 虚拟内存(VM)

在早期,程序是被完整的加载到物理内存,后来渐渐意识到这种做法的弊端:

  • 内存地址是连续的,黑客可以很轻松的从一个进程地址获取到其他进程的地址

  • 每个时刻只会使用到程序的一小部分内存,可是却把全部内存预先加载,明显存在内存浪费

为了解决这些严重的问题,就引入了虚拟内存的概念:

虚拟内存允许操作系统摆脱物理RAM的限制。虚拟内存管理器创建一个虚拟地址空间,然后将其划分为大小统一的内存块,称为页数。处理器及其内存管理单元(MMU)维护一个页面表,将程序虚拟地址空间中的页面映射到计算机RAM中的硬件地址。当程序的代码访问内存中的地址时,MMU使用页表将指定的虚拟地址转换为实际的硬件内存地址。该转换自动发生,并且对于正在运行的应用程序是透明的。

虚拟内存 -> 映射表 -> 物理内存

截屏2021-04-19 上午11.38.33.png

映射需要的地址

截屏2021-04-19 上午11.39.36.png

简单来说,程序被预先加载到虚拟内存空间,当程序的虚拟内存地址被访问时,MMU使用页表将这个虚拟地址映射到物理地址,保证程序的正常运行。

通过引入虚拟内存,被使用的数据才会被映射到物理内存,不仅解决了内存浪费的问题,也解决了物理地址连续易破解的问题。

但在虚拟内存中地址依然是连续的,可以查看编译后在macho中的虚拟内存地址是确定且连续的。

连续的地址始终是不安全的,苹果为了解决这个问题,又引入了ASLR的概念。

  • ASLR(Address space layout randomization):地址空间布局随机化,虚拟地址在内存中会发生的偏移量。增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。

app启动后,苹果在原先虚拟内存的基础上,又加上了一个随机的地址值,且每次启动不一致,保证了内存地址无法被直接定位。

这里存在两个虚拟地址的概念:

1.编译后的虚拟地址:就是macho文件中固定的地址

2.运行后的虚拟地址macho文件中固定的地址加上ASLR,就是运行后调试输出的地址

3.2 NONPOINTER_ISA

关于NONPOINTER_ISAd的优化在包含万象的isa中已经说明,这里直接给出结论:

isa是串联对象,类,元类和根元类的重要线索,采用联合体加位域的数据结构使有限的空间充分利用,存储了丰富的信息

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采用联合体的特点优化了内存; 而且类中有大量的信息需要记录,如果都额外声明属性存储,需要不少内存,NONPOINTER_ISA采用了位域的形式,使用每个二进制位分别定义存储的内容。

isa做为对象和类的固有属性,在内存中是大量存在的,任何一点优化带来的价值是很可观的。

关于isa存储的内容:

nonpointer:表示是否对 isa 指针开启指针优化(0:纯isa指针,1:不⽌是类对象地址,isa 中包含了类信息、对象的引⽤计数等)

has_assoc:关联对象标志位(0没有,1存在)

has_cxx_dtor:该对象是否有 C++ 或者 Objc 的析构器,如果有析构函数,则需要做析构逻辑,如果没有,则可以更快的释放对象

shiftcls:存储类指针的值。开启指针优化的情况下,在arm64架构下有33位用来存储类指针

magic:用于调试器判断当前对象是真的对象还是没有初始化的空间 

weakly_referenced:对象是否被指向或者曾经指向⼀个 ARC 的弱变量,没有弱引⽤的对象可以更快释放。

unused(之前是deallocating):标志对象是否正在释放内存 

extra_rc:当表示该对象的引⽤计数值,实际上是引⽤计数值减 1,例如,如果对象的引⽤计数为 10,那么 extra_rc 为 9。如果引⽤计数⼤于 10,则需要使⽤到下⾯的 has_sidetable_rc。

has_sidetable_rc:当对象引⽤技术⼤于 10 时,则需要借⽤该变量存储进位

其中nonpointerweakly_referencedunusedextra_rchas_sidetable_rc都和内存息息相关。

3.3 TaggedPointer

关于TaggedPointer包含万象的isa中也做过介绍:

早期64位系统时,当我们存储基础数据类型 , 底层封装成NSNumber对象 , 也会占用8字节内存 , 32位机器占用4字节。为了存储和访问一个NSNumber对象,需要在堆上分配内存,另外还要维护它的引用计数,管理它的生命期 。这些都给程序增加了额外的逻辑,造成运行效率上的损失 。因此如果没有额外处理 , 会造成很大空间浪费 .因此苹果引入了TaggedPointer,当对象为指针为TaggedPointer类型时,指针的值不是地址了,而是真正的值,直接优化了存储,提升了获取速度。

TaggedPointer的特点:

  • 专门用来存储小对象,例如NSNumber和部分NSString

  • 指针不再存储地址,而是直接存储对象的值(异或后的值)。所以,它不是一个对象,而是一个伪装成对象的普通变量。内存不在堆,而是在栈,由系统管理,不需要mallocfree

  • 不需要处理引用计数,少了retainrelease的流程

  • 在内存读取上有着3倍的效率,创建时比以前快106倍。(少了malloc流程,获取时直接从地址提取值)

iOS10.14以下的版本,TaggedPointer的地址存储着真正的值,这对于攻击者来说和明文没有区别。因此iOS10.14之后,苹果加入了TaggedPointerObfuscator(混淆器)的概念,TaggedPointer的指针存储的值就不是原始的值的了。

3.4 ARC

关于ARC只要牢记这几个规则:

  1. 自己生成的对象,自己持有
  2. 非自己生成的对象,自己也能持有
  3. 不再需要自己持有的对象时释放
  4. 非自己持有的对象无法释放

ARC的其他相关部分大家已经足够熟悉,不再赘述,后面只会从相关的源码进行解读。

3.5 autoreleasePool

自动释放池提供了一种机制:可以放弃对象的所有权,但又避免其被提早释放的可能性

默认情况下,自动释放池在runloop的一个迭代周期结束时,会自动释放这个周期所对应的哨兵对象的指针之后的所有对象。

大部分情况下,默认的情况足以保证内存的合理分配,但是某些特殊时刻,比如一个周期产生大量的临时对象,会产生内存峰值,这时候就需要手动添加自动释放池了。

手动添加的自动释放池,在自动释放池的作用域结束时,就会销毁其中产生的对象。这个时间点是快于runloop的一个迭代周期,也就减少了峰值内存产生的可能。

4.相关源码探索

简单介绍了几种内存优化机制,下面从源码的角度看看它们相关实现。

4.1 TaggedPointer相关

之前在类的加载分析中,分析过libobjc会从dyld那边接手map_imagesload_imagesunmap_image三件事,其中map_images时会来到_read_images这个重要函数,

void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses){

    ...
    initializeTaggedPointerObfuscator();
    ...
}

省略处的代码分析过,直接来看initializeTaggedPointerObfuscator()

截屏2021-04-23 下午5.30.19.png

iOS10.14之前,objc_debug_taggedpointer_obfuscator默认为0,之后等于一个随机数。

然后系统对TaggedPointer的赋值和获取采用了较简单且快速的方式:异或同一个数

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

赋值时异或生成的随机数,获取时在异或这个随机数。两次异或后就会得到原来的值。

4.2 ARC

引用计数是如果存储的?

为什么优先存isa?

retain如何处理引用计数的?

release如何处理引用计数的?

alloc的对象引用计数是多少?

什么时候调用dealloc,流程是怎样的?

以上常见的面试题可以从源码的角度一一找到答案。本文使用的源码版本是objc4-818.2

4.2.1 retain

跟一下retain的调用,(跟踪流程比较简单就跳过了,可以直接搜下面关键字)

retain -> _objc_rootRetain -> rootRetain,

最后会来到rootRetain:

objc_object::rootRetain()
{
    return rootRetain(false, RRVariant::Fast);
}

传入的参数会影响处理流程,需要注意下。

其中,rootRetain传入的tryRetainfalsevariant传入的是Fast

ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    //TaggedPointer拦截
    if (slowpath(isTaggedPointer())) return (id)this;

    //散列表是否加锁
    bool sideTableLocked = false;
    //是否转录到散列表
    bool transcribeToSideTable = false;

    isa_t oldisa;
    isa_t newisa;

    //获取isa
    oldisa = LoadExclusive(&isa.bits);
    
    //variant不等于FastOrMsgSend所以不会走
    if (variant == RRVariant::FastOrMsgSend) {
        ///它们在这里是为了避免重新加载isa
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR())) {
            ClearExclusive(&isa.bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                return swiftRetain.load(memory_order_relaxed)((id)this);
            }
            return ((id(*)(objc_object *, SEL))objc_msgSend)(this, @selector(retain));
        }
    }
    
    //不是nonpointer的isa时
    if (slowpath(!oldisa.nonpointer)) {
        //isa是指向元类时
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa.bits);
            return (id)this;
        }
    }
    
    //dowhile处理引用计数
    do {
        transcribeToSideTable = false;
        newisa = oldisa;
        //不是nonpointer的isa时
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            //tryRetain是false,所以走sidetable_retain直接存散列表中的引用计数表
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain(sideTableLocked);
        }
        //是否正在析构,都在析构了,还想retain我?
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa.bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            if (slowpath(tryRetain)) {
                return nil;
            } else {
                return (id)this;
            }
        }
        
        /*
        到这里说明是NONPOINTER_ISA了
        */
        
        //carry 进位标识,表示是否溢出
        uintptr_t carry;
        
        //isa对应的表示引用计数的位extra_rc做加1操作,同时返回是否carry
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  
        
        //如果carry,说明isa对应的表示引用计数的位装满了,溢出了
        if (slowpath(carry)) {
            //传入的RRVariant是Fast,所以走rootRetain_overflow
            if (variant != RRVariant::Full) {
                ClearExclusive(&isa.bits);
                return rootRetain_overflow(tryRetain);
            }

            //rootRetain_overflow传入是RRVariant是Full,就间接来到这里
            if (!tryRetain && !sideTableLocked) sidetable_lock();
            
            //散列表需要加锁,需要转录到散列表,
            sideTableLocked = true;
            transcribeToSideTable = true;
            
            //isa的extra_rc位存储的值拿走一半,isa的has_sidetable_rc位开启
            newisa.extra_rc = RC_HALF;
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

    //当发生进位溢出时,走sidetable_addExtraRC_nolock,isa的extra_rc位存储的值存进去一半
    if (variant == RRVariant::Full) {
        if (slowpath(transcribeToSideTable)) {
            sidetable_addExtraRC_nolock(RC_HALF);
        }

        if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!transcribeToSideTable);
        ASSERT(!sideTableLocked);
    }
    
    //处理完引用计数返回自己
    return (id)this;
}
  1. 如果不是nonpointerisa,引用计数直接存引用计数表

  2. nonpointerisa时,如果isa的指向正在析构,就不需要处理引用计数

  3. 如果不再析构,isa的引用计数的位做加1操作,同时返回是否carry

  4. 如果carry,调用rootRetain_overflow把一半存储到引用计数表

分析其中几个比较重要的地方:

  • sidetable_retain
id
objc_object::sidetable_retain(bool locked)
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    //根据当前对象在SideTables取出对应的SideTable
    SideTable& table = SideTables()[this];
    
    //加锁
    if (!locked) table.lock();
    //得到已经存储的引用计数
    size_t& refcntStorage = table.refcnts[this];
    //如果引用计数没有越界,还够存储
    if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
        //对应的引用计数的的位做加1操作
        refcntStorage += SIDE_TABLE_RC_ONE;
    }
    //解锁
    table.unlock();

    return (id)this;
}

总的来看,sidetable_retain是对散列表中的引用计数表做加1操作

先根据当前对象从SideTables取出SideTable,那为什么不把所有对象都存在一张全局的SideTable,而是需要SideTables来获取对应的SideTable呢?看下SideTable的结构:

struct SideTable {
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;
}

SideTable有三个属性:

  • slock,锁
  • refcnts,引用计数表
  • weak_tableweak

因为每次操作SideTable都需要加锁解锁,如果所有的对象都存在一张全局的SideTable,每次增删改查时都需要操作这一整张大表,后续的操作都要等待前一次操作的完成,非常影响性能。

所以苹果使用一张SideTables存储64张SideTable的形式,每个对象通过hash找到自己的SideTable进行操作,以空间换时间的方式提高了性能。

  • RC_ONE
#     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 unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 19
        //1+1+1+33+6+1+1+1 = 45
#     define RC_ONE   (1ULL<<45)

RC_ONE根据不同的架构做位移,虽然各种架构的值不同,但最终都是得到isaextra_rc所在的位

  • rootRetain_overflow
objc_object::rootRetain_overflow(bool tryRetain)
{   
    //再次调用rootRetain,标识为Full
    return rootRetain(tryRetain, RRVariant::Full);
}

rootRetain_overflow再次调用rootRetain,只是改变了RRVariant的标识为Full,保证内部来到sidetable_addExtraRC_nolock做溢出时存值到引用计数表的操作。

retain如何处理引用计数 总结:

如果retain的对象是TaggedPointerretain的对象isa指向是元类不作处理。如果是普通的isa,引用计数只存引用计数表中,如果是NONPOINTER_ISA,如果retain的对象正在析构不作处理,否则引用计数先存isaextra_rc中,如果extra_rc存满了,拿一半存引用计数表中,依次反复。

4.2.2 release

明白了retain的流程,release也就好理解了。

跟一下release的调用,

release -> _objc_rootRelease -> rootRelease,

最后会来到rootRelease:

ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    //TaggedPointer拦截
    if (slowpath(isTaggedPointer())) return false;

    //散列表是否加锁标识
    bool sideTableLocked = false;

    isa_t newisa, oldisa;

    //获取isa
    oldisa = LoadExclusive(&isa.bits);

    //RRVariant等于FastOrMsgSend时才走,传的是Fast所以不走
    if (variant == RRVariant::FastOrMsgSend) {
        if (slowpath(oldisa.getDecodedClass(false)->hasCustomRR()){
            ClearExclusive(&isa.bits);
            if (oldisa.getDecodedClass(false)->canCallSwiftRR()) {
                swiftRelease.load(memory_order_relaxed)((id)this);
                return true;
            }
            ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(release));
            return true;
        }
    }

    //不是NONPOINTER_ISA时
    if (slowpath(!oldisa.nonpointer)) {
        //判断是不是元类,元类就不处理了
        if (oldisa.getDecodedClass(false)->isMetaClass()) {
            ClearExclusive(&isa.bits);
            return false;
        }
    }

retry:
    //dowhile处理引用计数
    do {
        newisa = oldisa;
        //不是NONPOINTER_ISA时,说明isa没有存储引用计数
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            //直接调用sidetable_release减少sidetable中引用计数表的引用计数
            return sidetable_release(sideTableLocked, performDealloc);
        }
        //如果正在析构,release没有意义,不处理
        if (slowpath(newisa.isDeallocating())) {
            ClearExclusive(&isa.bits);
            //如果表被锁了,就解锁
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            return false;
        }
        
        //到这里说明是NONPOINTER_ISA了

        //carry 进位标识,表示是否溢出
        uintptr_t carry;
        
        //isa对应的表示引用计数的位extra_rc做减1操作,同时返回是否carry,这里的进位是指isa中的引用计数减完时
        newisa.bits = subc(newisa.bits, RC_ONE, 0, &carry); 
        if (slowpath(carry)) {
            // don't ClearExclusive()
            //发生溢出时,跳转underflow分支
            goto underflow;
        }
    } while (slowpath(!StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

    //isa存在析构标识时,跳转deallocate分支
    if (slowpath(newisa.isDeallocating()))
        goto deallocate;

    if (variant == RRVariant::Full) {
        if (slowpath(sideTableLocked)) sidetable_unlock();
    } else {
        ASSERT(!sideTableLocked);
    }
    return false;

//underflow分支

 underflow:

    newisa = oldisa;

    //判断isa的has_sidetable_rc位是否使用,使用说明引用计数表里有引用计数
    if (slowpath(newisa.has_sidetable_rc)) {
        //RRVariant不是Full时
        if (variant != RRVariant::Full) {
            ClearExclusive(&isa.bits);
            //调用rootRelease_underflow,再走一次rootRelease流程,RRVariant标识传Full
            return rootRelease_underflow(performDealloc);
        }

        //再走一次的rootRelease流程时来到这里,下面的操作就是isa中的引用计数减完时,把引用计数表中的引用计数转移到isa中
        
        //如果sideTable没加锁,需要对sideTable加锁,重新开始获取isa以避免指针转换,然后再去retry分支
        if (!sideTableLocked) {
            ClearExclusive(&isa.bits);
            sidetable_lock();
            sideTableLocked = true;
            oldisa = LoadExclusive(&isa.bits);
            goto retry;
        }

        // 尝试从引用计数表中借一些引用计数
        auto borrow = sidetable_subExtraRC_nolock(RC_HALF);

        //如果借完剩下是0,标识清除引用计数表
        bool emptySideTable = borrow.remaining == 0; 
        
        //如果借出来的引用计数大于0,
        if (borrow.borrowed > 0) {
            //是否过渡释放的标识
            bool didTransitionToDeallocating = false;
            
            //isa中存借出来的引用计数-1
            //为什么需要减1,看下上面给的extra_rc位的定义就知道了
            newisa.extra_rc = borrow.borrowed - 1;  
            //has_sidetable_rc位是否开启取决于引用计数表中是否还有引用计数
            newisa.has_sidetable_rc = !emptySideTable;

            //StoreReleaseExclusive内部调用__c11_atomic_compare_exchange_weak
            //当前值与期望值相等时,修改当前值为设定值,返回true
            //当前值与期望值不等时,将期望值修改为当前值,返回false
            
            //isa更新是否成功标识
            bool stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);

            //更新失败且是NONPOINTER_ISA时,往isa中回填引用计数
            if (!stored && oldisa.nonpointer) {
                uintptr_t overflow;
                newisa.bits =
                    addc(oldisa.bits, RC_ONE * (borrow.borrowed-1), 0, &overflow);
                newisa.has_sidetable_rc = !emptySideTable;
                if (!overflow) {
                    stored = StoreReleaseExclusive(&isa.bits, &oldisa.bits, newisa.bits);
                    if (stored) {
                        didTransitionToDeallocating = newisa.isDeallocating();
                    }
                }
            }

            //更新失败且不是NONPOINTER_ISA时,把引用计数放回引用计数表,再走retry分支
            if (!stored) {
                ClearExclusive(&isa.bits);
                sidetable_addExtraRC_nolock(borrow.borrowed);
                oldisa = LoadExclusive(&isa.bits);
                goto retry;
            }

            //走到这里,就是从引用计数表借值后减量成功
            
            //如果emptySideTable为空,就清除引用计数表
            if (emptySideTable)
                sidetable_clearExtraRC_nolock();

            if (!didTransitionToDeallocating) {
                if (slowpath(sideTableLocked)) sidetable_unlock();
                return false;
            }
        }
        else {
        }
    }
    
//deallocate分支

deallocate:

    ASSERT(newisa.isDeallocating());
    ASSERT(isa.isDeallocating());

    if (slowpath(sideTableLocked)) sidetable_unlock();

    //在同一个线程(函数)中执行的线程和信号处理程序之间的栅栏,dealloc要一个个来
    __c11_atomic_thread_fence(__ATOMIC_ACQUIRE);

    //来到deallocate分支时,发送dealloc消息
    if (performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return true;
}

其中几个关键的地方说明下:

  • sidetable_release
uintptr_t
objc_object::sidetable_release(bool locked, bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
    ASSERT(!isa.nonpointer);
#endif
    //依然是在SideTables中找到对应SideTable
    SideTable& table = SideTables()[this];

    //是否要做dealloc操作的标识符
    bool do_dealloc = false;

    //加锁
    if (!locked) table.lock();
    //判断该引用计数是否为引用计数表中的最后一个,如果是则表示release后需要执行dealloc清除对象所占用的内存
    auto it = table.refcnts.try_emplace(this, SIDE_TABLE_DEALLOCATING);
    auto &refcnt = it.first->second;
    if (it.second) {
        do_dealloc = true;
    } else if (refcnt < SIDE_TABLE_DEALLOCATING) {
        do_dealloc = true;
        refcnt |= SIDE_TABLE_DEALLOCATING;
    } else if (! (refcnt & SIDE_TABLE_RC_PINNED)) {
        refcnt -= SIDE_TABLE_RC_ONE;
    }
    table.unlock();
    //发送dealloc消息
    if (do_dealloc  &&  performDealloc) {
        ((void(*)(objc_object *, SEL))objc_msgSend)(this, @selector(dealloc));
    }
    return do_dealloc;
}
  • rootRelease_underflow
objc_object::rootRelease_underflow(bool performDealloc)
{
    //再次调用rootRelease,标识为Full
    return rootRelease(performDealloc, RRVariant::Full);
}

release如何处理引用计数总结:

如果release的对象是TaggedPointerrelease的对象isa指向是元类不作处理。如果是普通的isa,从引用计数表中删除引用计数,删完了发送dealloc消息;如果是NONPOINTER_ISA,如果release的对象正在析构不作处理,否则先从isa中删除引用计数,不够删时,从引用计数表取出来到isa中删除,两者都删完时发送dealloc消息。

4.2.3 retainCount

alloc的对象引用计数是多少?

现在来分析下引用计数是如何计算的。

跟一下retainCount的调用,

retainCount -> _objc_rootRetainCount -> rootRetainCount

最后会来到rootRetainCount:

需要注意的是,objc4-818.2的源码和objc4-7系列rootRetainCount实现有比较大的区别,下面给出两个版本的源码做对比避免入坑

4.2.3.1 objc4-818.2

inline uintptr_t 
objc_object::rootRetainCount()
{
    //TaggedPointer拦截
    if (isTaggedPointer()) return (uintptr_t)this;

    //加锁
    sidetable_lock();
    //获取isa
    isa_t bits = __c11_atomic_load((_Atomic uintptr_t *)&isa.bits, __ATOMIC_RELAXED);
    //如果是NONPOINTER_ISA
    if (bits.nonpointer) {
        //引用计数等于extra_rc存的值
        uintptr_t rc = bits.extra_rc;
        //如果has_sidetable_rc位有值,说明引用计数表有存储
        if (bits.has_sidetable_rc) {
            //引用计数加上引用计数表中存的值
            rc += sidetable_getExtraRC_nolock();
        }
        //解锁
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    return sidetable_retainCount();
}

当是NONPOINTER_ISA时,引用计数的值等于isaextra_rc的值加上引用计数表中存的值,不是NONPOINTER_ISA时,来到sidetable_retainCount

uintptr_t
objc_object::sidetable_retainCount()
{
    //依然是SideTables找到对应的SideTable
    SideTable& table = SideTables()[this];

    //至少为1
    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;
}

如果是普通的isa时,引用计数只等于引用计数表中的值。

4.2.3.2 objc4-781

inline uintptr_t 
objc_object::rootRetainCount()
{
    //TaggedPointer拦截
    if (isTaggedPointer()) return (uintptr_t)this;
    
    //加锁
    sidetable_lock();
    isa_t bits = LoadExclusive(&isa.bits);
    ClearExclusive(&isa.bits);
    //如果是NONPOINTER_ISA
    if (bits.nonpointer) {
        //引用计数为extra_rc的值 + 1
        uintptr_t rc = 1 + bits.extra_rc;
        ////如果has_sidetable_rc位有值,说明引用计数表有存储
        if (bits.has_sidetable_rc) {
            //引用计数加上引用计数表中存的值
            rc += sidetable_getExtraRC_nolock();
        }
        sidetable_unlock();
        return rc;
    }

    sidetable_unlock();
    //源码一样
    return sidetable_retainCount();
}

当是NONPOINTER_ISA时,引用计数的值等于isaextra_rc的值加---1---在加上引用计数表中存的值,不是NONPOINTER_ISA时,来到sidetable_retainCount

4.2.3.3 读二者区别引发的思考

有不少面试题会问 alloc的对象引用计数是多少

这题乍看简单,但实际是有坑的。

先分析二者最本质的区别:

在于NONPOINTER_ISA时,是否有手动加1的操作。

因为这个操作会直接决定了 alloc的对象引用计数是多少 的答案!

如果是老版本的源码,alloc的对象引用计数是0

如果是新版本的源码,alloc的对象引用计数是1

关于alloc的对象引用计数是0,相信不少同学也看过相关文章里面有这个结论:

alloc后当获取引用计数时,引用计数打印1,这只是因为调用retainCount时,内部手动做了个+1的操作,extra_rc中实际存储的是0,所以对象实际的引用计数是0

所以早期的文章说alloc的对象引用计数是0是正确的。

那新版的retainCount为什么不需要加1呢?既然这里不加1,那可能是alloc时就做了操作。于是我回头看了新版的alloc流程,发现在initIsa多了一个操作:

截屏2021-06-04 下午3.44.19.png

果然,新版的alloc在开辟空间时就把extra_rc赋值为1。感兴趣的同学可以看下旧版的alloc流程确认下有没有这个赋值。

所以现在alloc的对象引用计数是1才是正确答案!

4.2.4 dealloc

什么时候调用dealloc,流程是怎样的?

现在来分析下这个面试常见题。

根据release的流程已经得出,无论是哪种isa,引用计数为0时都会发送dealloc消息。

至于流程,直接来到dealloc的源码:

dealloc -> _objc_rootDealloc -> rootDealloc

inline void objc_object::rootDealloc()
{
    //TaggedPointer拦截
    if (isTaggedPointer()) return;  
    
    //是NONPOINTER_ISA时
    //weakly_referenced,has_assoc,has_cxx_dtor,has_sidetable_rc都不存在直接free
    if (fastpath(isa.nonpointer                     &&
                 !isa.weakly_referenced             &&
                 !isa.has_assoc                     &&
#if ISA_HAS_CXX_DTOR_BIT
                 !isa.has_cxx_dtor                  &&
#else
                 !isa.getClass(false)->hasCxxDtor() &&
#endif
                 !isa.has_sidetable_rc))
    {
        assert(!sidetable_present());
        free(this);
    } 
    else {
        //其中之一存在,就走object_dispose
        object_dispose((id)this);
    }
}
  1. TaggedPointer拦截,不作处理
  2. 如果是NONPOINTER_ISA时,且弱引用表,关联对象表,c++析构函数,引用计数表都没有使用到这个对象,直接free快速释放对象
  3. 其中之一存在时,走object_dispose慢速释放
id 
object_dispose(id obj)
{
    if (!obj) return nil;
    
    objc_destructInstance(obj);    
    free(obj);

    return nil;
}

调用objc_destructInstance后,free释放对象

void *objc_destructInstance(id obj) 
{
    if (obj) {
        //是否存在c++析构标识
        bool cxx = obj->hasCxxDtor();
        //是否有添加到关联对象表标识
        bool assoc = obj->hasAssociatedObjects();

        //存在,调用c++析构函数
        if (cxx) object_cxxDestruct(obj);
        //存在,从关联对象表中删除
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true);
        obj->clearDeallocating();
    }
    return obj;
}
  1. 获取是否存在c++析构标识和添加到关联对象表标识
  2. 哪个存在,就调用c++析构函数或者从关联对象表中删除
  3. 调用clearDeallocating
inline void objc_object::clearDeallocating()
{
    //是普通的isa,调用sidetable_clearDeallocating
    if (slowpath(!isa.nonpointer)) {
        sidetable_clearDeallocating();
    }
    //是NONPOINTER_ISA,且weakly_referenced或has_sidetable_rc存在时,调用clearDeallocating_slow
    else if (slowpath(isa.weakly_referenced  ||  isa.has_sidetable_rc)) {
        clearDeallocating_slow();
    }

    assert(!sidetable_present());
}
  1. 是普通的isa,调用sidetable_clearDeallocating
  2. NONPOINTER_ISA,且weakly_referencedhas_sidetable_rc存在时,调用clearDeallocating_slow
  3. sidetable_clearDeallocatingclearDeallocating_slow的处理是一样的,只是因为isa类型不同,获取标志位的方式也不同,所以写成了两个函数表示,所以只看多数情况下的isa处理
NEVER_INLINE void objc_object::clearDeallocating_slow()
{
    ASSERT(isa.nonpointer  &&  (isa.weakly_referenced || isa.has_sidetable_rc));
    //依然是从SideTables获取对应的SideTable
    SideTable& table = SideTables()[this];
    //加锁
    table.lock();
    //如果有weak标识
    if (isa.weakly_referenced) {
        //清空weak表中对应的entry数组
        weak_clear_no_lock(&table.weak_table, (id)this);
    }
    //如果有has_sidetable_rc标识
    if (isa.has_sidetable_rc) {
        //清空引用计数表
        table.refcnts.erase(this);
    }
    table.unlock();
}
  1. SideTables获取对应的SideTable,然后加锁
  2. 如果有weak标识,清空weak表中对应的entry数组,entry数组中存在所有对该对象的弱引用
  3. 如果有has_sidetable_rc标识,抹去引用计数表,注意是引用计数表,而不是引用计数(表都删了也不需要处理数据了)

dealloc流程总结:

如果TaggedPointer类型,不作处理。如果对象的弱引用,关联对象,c++析构函数,引用计数标识都不存在,直接释放。否则存在c++析构函数标识就调用c++析构函数,存在关联对象标识就从关联对象表中删除对象,存在弱引用标识就从弱引用表中删除对应的数组,存在引用计数标识就抹去对应的引用计数表。最后释放对象。

骑枪弓策剑

4.2.5 剩余面试题解答

根据以上所有的流程分析,还剩下的两道面试题也就有了答案:

  • 引用计数是如果存储的?

如果不是NONPOINTER_ISA,引用计数只存在散列表中的引用计数表中;如果是NONPOINTER_ISA,引用计数优先存储在isaextra_rc位中,存满后,在使用引用计数表存储。

  • 为什么优先存isa中?

引用计数存入引用计数表时,需要加锁解锁,hash查找一系列过程,而存入isa时,只需要位操作改变对应的值,所以优先存isa中效率更高。

5.内存管理之循环引用和强引用

以下代码是否会引起循环引用?

 self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(fireHome) userInfo:nil repeats:YES];
 [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

很明显,答案是会。那继续追问:

以下方式可以解决循环引用嘛?

__weak typeof(self) weakSelf = self; 
 self.timer = [NSTimer timerWithTimeInterval:1 target:weakSelf selector:@selector(fireHome) userInfo:nil repeats:YES];
 [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];

大部分同学看到__weak关键字第一反应就会觉得__weak可以解决循环引用。

其实,__weak可以用来解决block带来的循环引用,但是timer的却不行。这是为什么?

看下关于timerapi的官方文档:

截屏2021-06-07 下午3.15.35.png

译: 当定时器触发时将Selector指定的消息发送到的对象。计时器保持对该对象的强引用,直到它(计时器)失效。

target会对传入的参数进行一次强引用。那强引用为什么不能使用__weak解决呢?

要搞清楚这个问题,先看下相关打印:

截屏2021-06-07 下午3.23.19.png

很明显,selfweakSelf指向的是同一个地址空间,也就是同一个对象,timer操作weakSelf实际上就是操作self,所以使用weak解决timer的循环引用是无效的。

那为什么block的循环引用解决不存在这个这个问题呢?继续打印:

截屏2021-06-07 下午3.23.14.png

虽然selfweakSelf指向的地址相同,但它们自身的指针地址是不同的。

截屏2021-06-07 下午4.19.50.png

看下block捕获对象的源码:

截屏2021-06-07 下午4.22.16.png

block捕获对象的不同之处在于:

block捕获的是**类型,也就是捕获指向传入对象destArg的指针地址

所以,block持有的是weakself的指针地址,不是指向的对象本身,而timer持有的是对象本身,这造成二者循环引用有本质的区别的。

至于timer的解决方案也比较多,不是本文重点,这里只推荐使用NSProxy的方案,有兴趣的同学可以自行研究下。

6.内存相关的代码面试题

1.内存分区之全局静态变量面试题

关于全局静态变量有个相关的面试题:

定义一个值为100的全局静态变量,如下操作后输出:

    //直接打印主类的personNum,对应第一次打印
    NSLog(@"ViewController内部,&personNum = %p--personNum = %d\n",&personNum,personNum);
    
    personNum = 10000;
    
    //修改personNum等于10000后打印,对应第二次打印
    NSLog(@"ViewController内部,&personNum = %p--personNum = %d\n",&personNum,personNum);
    
    //内部对personNum做加1操作,内部也打印personNum,对应第三次打印
    [[CJPerson new] add];
    
    //打印personNum,对应第四次打印
    NSLog(@"ViewController内部,&personNum = %p--personNum = %d\n",&personNum,personNum);
    
    //内部无特殊操作,直接打印personNum,对应第五次打印
    [[CJPerson alloc] cate_method];

截屏2021-06-07 上午10.57.28.png

—————————————————————————————————————————————

  1. 第一次输出为100,地址为0x10ea9d338

  2. 第二次输出为10000,地址为0x10ea9d338

  3. 第三次输出为101,地址为0x10ea9d328

  4. 第四次输出为10000,地址为0x10ea9d338

  5. 第五次输出100,地址为0x10ea9d33c

—————————————————————————————————————————————

由①到②,可得:全局静态变量是可以被修改的。

由②到③,可得:生成新的全局静态变量地址,修改只针对自身文件的全局静态变量有效。

由③到④,可是:其他文件的全局静态变量修改不对自身文件的产生影响。

由④到⑤,可得:分类也是新文件,也会生成新的全局静态变量地址。

由以上可得总结:

全局静态变量的值是可以被修改的,全局静态变量的值只针对文件有效,每个文件都生成自己的变量。

2.TaggedPointer面试题

以下两段代码执行后分别有什么结果?

    self.queue = dispatch_queue_create("com.juejin.cn", DISPATCH_QUEUE_CONCURRENT);

    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"juejin"];
             NSLog(@"%@",self.nameStr);
        });
    }
    self.queue = dispatch_queue_create("com.juejin.cn", DISPATCH_QUEUE_CONCURRENT);

    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"juejin_iOS底层--内存管理分析"];
            NSLog(@"%@",self.nameStr);
        });
    }

第一段正常执行,第二段执行的过程中崩溃

为什么代码看似简单,内容也基本一样,结果却不一样?

因为这题其实还涉及了多线程下setter的使用。

先说下第二种崩溃的原因如下:

setter方法主要做的事是对newvalue进行retian,对oldvalue进行realase,在多线程的环境中,对写操作没有加锁,那么一定会有多条线程竞争使用资源。某一时刻,当某条线程已经realase了旧值,另一条线程这时候也开始setter,对已经释放的值进行retain,这也就导致了程序的崩溃。

而第一种情况的特殊之处在于:

因为nameStr的长度较短,系统把它设为TaggedPointer对象,虽然也是处于多线程未加锁竞争资源的条件下,但因为realaseretain在操作时,第一步就对TaggedPointer进行了拦截,自然也就不会奔溃了。

ALWAYS_INLINE id 
objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return false;
    ...
}

ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
{
    if (slowpath(isTaggedPointer())) return (id)this;
    ...
}

7.写在后面

以上是对iOS内存相关的一些分析,而内存分析远不止于此,比如autoreleasePool也是内存优化的重要组成部分。但因为担心行文过长影响阅读,就到此为止吧。

所以下一章是autoreleasePool的底层分析。

敬请关注。