iOS内存管理原理

209 阅读8分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第3天,点击查看活动详情

一、内存五大区

1.堆区(Heap)

堆区是由程序员动态分配和释放的,如果程序员不释放,程序结束后,可能由操作系统回收。
只要用于存放:

  • OC中使用alloc或者使用new开辟空间创建的对象。
  • C语言中使用malloc、calloc、realloc分配的空间,需要free释放。
优缺点
  • 优点:方便灵活,数据适应面广泛。
  • 缺点:需要手动管理速度慢、容易产生内存碎片
    当需要访问堆中内存时,一般需要先通过对象读取到栈区的指针地址,然后通过指针地址访问堆区

2.栈区(Stack)

栈是由编译器自动分配并释放的,它是一块连续的内存区域系统数据结构,遵循先进先出(FILO)原则,其对应的进程或线程是唯一的。
栈一般在运行时分配,向高地址向底地址扩展的数据结构,地址空间在iOS是以0X7开头

存储
  • 栈是由编译器自动创建和释放的
  • 存储局部变量,一旦离开作用于就会销毁释放
  • 存储函数参数,包括隐藏函数,比如(id self, SEL _cmd)
优缺点
  • 优点:由于栈是由编译器自动分配并释放的,不会产生内存碎片,不需要手动管理,所以快速高效
  • 缺点:由于是一块连续的内存区域,所以栈的内存大小有限制,数据不灵活
    iOS主线程大小是1MB,其他线程是512KBMAC只有8M。实际上我们也可以通过线程的stackSpace去修改,但是成本有些大。

以上内存大小的说明,在Threading Programming Guide中有相关说明

3.全局区 (静态区,即.bss & .data)

全局区是编译时分配的内存空间,在iOS中一般以0X1开头,在程序运行过程中,此内存中的数据一直存在,程序结束后由系统释放
主要存放:

  • 未初始化全局变量静态变量,即BSS区(.bss)。
  • 已初始化全局变量静态变量,即数据区(.data)。
    其中,全局变量是指变量值可以在运行时被动态修改,而静态变量是static修饰的变量,包含静态局部变量和静态全局变量。

4.常量区

一般存储的是 常量-const 0x1 常量区的内存在编译阶段完成分配,程序运行时会一直存在内存中,只有当程序结束后才会由操作系统释放,主要存放已经使用了的,且没有指向的字符串常量
字符串常量因为可能在程序中被多次使用,所以在程序运行之前就会提前分配内存。

5.代码区

代码区是编译时分配,主要用于存放程序运行时的代码,代码会被编译成二进制存进内存的。

一般存储的是编译生成的二进制代码.

关系图: image.png

iOS中的内存管理方案:

有三种方案: NONPOINTER_ISA , Tagged Pointer , SideTable

二、tagged point

tagged point (64位下的概念)占8个字节,小对象 NSDate NSString NSNumber 也是一个指针 是被打上了 tagged标记的指针 ,是iphone5s后的概念。

    NSString *firstString = @"helloworld";
    NSString *secondString = [NSString stringWithFormat:@"helloworld"];
    NSString *thirdString = @"hello";
    NSString *fourthSting = [NSString stringWithFormat:@"hello"];

    NSLog(@"%p %@",firstString,[firstString class]);
    NSLog(@"%p %@",secondString,[secondString class]);
    NSLog(@"%p %@",thirdString,[thirdString class]);
    NSLog(@"%p %@",fourthSting,[fourthSting class]);

image.png 我们看到NSString在不同方式的创建下,打印的string类型不一样。有这样3种:__NSCFConstantString(0x1 在常量区)、__NSCFString(0x6 在堆区)、NSTaggedPointerString(0xa 栈区)。

[NSString stringWithFormat:@"hello"];在字符小于9的时候,str是一个NSTaggedPointerString对象。

我在看NSTaggedPointerString的指针打印结果(通过如下代码):

uintptr_t ny_objc_obfuscatedTagToBasicTag(uintptr_t tag) {

    for (unsigned i = 0; i < 7; i++)

        if (objc_debug_tag60_permutations[i] == tag)

            return i;

    return 7;

}

uintptr_t
ny_objc_decodeTaggedPointer(id ptr)
{
    uintptr_t value = (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
    uintptr_t basicTag = (value >> ny_OBJC_TAG_INDEX_SHIFT) & lg_OBJC_TAG_INDEX_MASK;

    value &= ~(lg_OBJC_TAG_INDEX_MASK << lg_OBJC_TAG_INDEX_SHIFT);
    value |= ny_objc_obfuscatedTagToBasicTag(basicTag) << lg_OBJC_TAG_INDEX_SHIFT;
    return value;

}

NSNumber *number = [NSNumber numberWithInt:7];

NSLog(@"0x%lx",ny_objc_decodeTaggedPointer(fourthSting));

NSLog(@"0x%lx",ny_objc_decodeTaggedPointer(number));

A9772DB189D57F4BFB549D62C5F34FDF.png 什么是tagged point?在总是为0的位置大上了标记的指针。 可以看到tagged point 的指针地址直接保存了值信息。 tagged point 它会与初始化的进程随机值进行混淆,intel最低位为1 arm最高位为1 在iOS13后用最低位的3位来标识。 未位010就等于2在objC源码中(fourthSting): image.png

未位011就等于3在objC源码中(NSNumber): image.png 0:char 1:short 2:int 4:float 5:double

三、retain&release 函数

在源码中搜索"objc_retain"然后进入代码: image.png 一步一步跟踪进入: objc_retain->objc_object::retain()->objc_object::rootRetain() image.png 这里是存储对象的引用计数的三种情况

  1. 不是nonpointerisa 存储在sidetable
  2. 是nonpointerisa 存储在extrc_rc并且能够存的下
  3. 是nonpointerisa 存储在extrc_rc存不下的情况,需要借存储在sidetable中extrc_rc的一半
//直接上objc_object::rootRetain() 核心代码
do {
        //不是nonpointerisa -- 存储在sidetable
        transcribeToSideTable = false;
        newisa = oldisa;
        if (slowpath(!newisa.nonpointer)) {
            ClearExclusive(&isa.bits);
            if (tryRetain) return sidetable_tryRetain() ? (id)this : nil;
            else return sidetable_retain(sideTableLocked);
        }
        // don't check newisa.fast_rr; we already called any RR overrides
        if (slowpath(newisa.isDeallocating())) {//判断对象是否正在被释放,引用计数不操作。
            ClearExclusive(&isa.bits);
            if (sideTableLocked) {
                ASSERT(variant == RRVariant::Full);
                sidetable_unlock();
            }
            if (slowpath(tryRetain)) {
                return nil;
            } else {
                return (id)this;
            }
        }
        uintptr_t carry;
        newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // 存得下直接extra_rc++ 

        if (slowpath(carry)) {//是nonpointerisa 存储在extrc_rc存不下的情况
            // newisa.extra_rc++ overflowed
            if (variant != RRVariant::Full) {
                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;//只保存一半的extra_rc
            newisa.has_sidetable_rc = true;
        }
    } while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));

retain核心流程梳理如下:

  1. 首次进入rootRetain(tryRetain, variant):参数tryRetain=falsevariant=FastOrMsgSend
  2. 判断如果是isTaggedPointer,则直接return this;反之继续3.
  3. variant=FastOrMsgSend,执行objc_msgSend(this, @selector(retain)),继续4。
  4. 二次进入rootRetain(tryRetain, variant):参数tryRetain=falsevariant=Fast
  5. 判断如果isa.nonpointer==0,执行sidetable_retain():引用计数全部全部存储在sidetable中;直接根据当前对象找到存储该对象的table,然后找到原有的refcntStorage+=SIDE_TABLE_RC_ONE即可。
  6. 判断如果isa.nonpointer==1,应用计数存储在isa.extra_rcsidetable中。引用计数+1会优先添加到isa.extra_rc上。如果存满了,则先保存一半RC_HALFextra_rc中,并标记has_sidetable_rc=true已使用引用计数表,处理完isa后更新isa的数据。再将另一半RC_HALF追加到sidetable中,保存到side_table的流程与2同。
  7. retain的最后返回this指针。

源码中release的核心流程objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)中: image.png release核心流程梳理如下:

  1. 首次进入rootRelease(performDealloc, variant):参数performDealloc=true, variant= FastOrMsgSend
  2. 判断如果是isTaggedPointer,则直接return false;反之继续3
  3. variant=FastOrMsgSend,执行objc_msgSend)(this, @selector(release)),继续4。
  4. 二次进入rootRelease(performDealloc, variant):参数performDealloc=true, variant= Fast
  5. 判断如果isa.nonpointer==0,执行sidetable_release():引用计数全部存在sidetable中;根据当前的对象找到存储该对象引用计数的table,然后找到原有的refcnt -= SIDE_TABLE_RC_ONE;即可。满足dealloc条件的,继续执行dealloc流程。
  6. 判断如果isa.nonpointer==1,应用计数存储在isa.extra_rcsidetable中。引用继续-1会先从isa.extra_rc上减。如果不够减了,会进入underflow流程7.
  7. 判断如果该对象有has_sidetable_rc,执行rootRelease_underflow流程,三次进入rootRelease(performDealloc, variant):参数performDealloc=true, variant= Full
  8. 执行auto borrow = sidetable_subExtraRC_nolock(RC_HALF);也就是问sidetableRC_HALF,返回借到的数量和剩余的数量。
  9. 如果借到了则将借到的数量-1保存到isa.extrac_rc中。如果sidetable中剩余为0则标记isa.has_sidetable_rc=0,再存储新的isa.bits的数据。处理存储失败的情况。
  10. 如果没有借到或者根本就没有再sidetable中存储则执行dealloc相关流程。

四、dealloc流程

源码中release的核心流程objc_object::rootDealloc()中:

inline void
objc_object::rootDealloc()
{
    if (isTaggedPointer()) return// fixme necessary?
    if (fastpath(isa.nonpointer                     &&//是否是nonpointer
                     !isa.weakly_referenced             &&//是否是弱引用
                     !isa.has_assoc                     &&//是否有关联对象
    #if ISA_HAS_CXX_DTOR_BIT
                     !isa.has_cxx_dtor                  &&//是否有cxx析构函数
    #else
                     !isa.getClass(false)->hasCxxDtor() &&
    #endif
                     !isa.has_sidetable_rc))//是否有sidetable
    {
        assert(!sidetable_present());
        free(this);//直接释放内存
    } 
    else {
        object_dispose((id)this);//上述条件不成立,执行object_dispose
    }
}

rootDealloc函数:先判断了是否TaggedPointer?是直接返回。接着判断(是否是nonpointer,是否是弱引用,是否有关联对象,是否有cxx析构函数,是否有sidetable)如果都没有的情况就执行free(this)直接释放内存。

上述条件不成立,执行object_dispose函数:

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

}

//进入objc_destructInstance(obj);    
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);//arc模式下释放成员变量
        if (assoc) _object_remove_assocations(obj, /*deallocating*/true);//移除关联对象
        obj->clearDeallocating();
    }
    return obj;
}

//进入clearDeallocating();
inline void 
objc_object::clearDeallocating()
{
    if (slowpath(!isa.nonpointer)) {//是否是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());
}

dealloc核心流程梳理如下:

  1. 先判断了如果是否TaggedPointer?是直接返回。
  2. 如果是一个nonpointerISA而且没有弱引用,没有关联对象,没有cxx析构函数,没有散列表引用计数 就直接释放。
  3. 如果是一个nonpointerISA,并且不是第二步的情况。就执行objc_destructInstance函数。
  4. 删除关联对象,调用C++析构函数,清空弱引用表里面的数据,清空散列表里面的引用计数的信息。

五、总结

  1. 内存五大区:堆区(Heap),栈区(Stack),全局区 (静态区,即.bss & .data),常量区,代码区。
  2. tagged point:可以看到tagged point 的指针地址直接保存了值信息。 tagged point 它会与初始化的进程随机值进行混淆,intel最低位为1 arm最高位为1 在iOS13后用最低位的3位来标识。
  3. retain&release 函数:
    • retain核心流程:(1)如果是isTaggedPointer直接返回,(2)是nonpointerisa 存储在extrc_rc并且能够存的下,(3)是nonpointerisa 存储在extrc_rc存不下的情况,需要借存储在sidetable中extrc_rc的一半
    • release核心流程:(1)如果是isTaggedPointer直接返回,(2)如果是nonpointerisa会在isa.extra_rc上减,如果不够减了或者到0从sidetable中取。(3)如果isa.extrac_rc=0了,isa.has_sidetable_rc=0了,则执行dealloc相关流程。
  4. dealloc流程:
    (1)如果是isTaggedPointer直接返回,
    (2)是nonpointerISA而且没有弱引用,没有关联对象,没有cxx析构函数,没有散列表引用计数 就直接释放。
    (3)是nonpointerISA并不是2情况,删除关联对象,调用C++析构函数,清空弱引用表里面的数据,清空散列表里面的引用计数的信息。