iOS底层原理-内存管理

334 阅读5分钟

1、内存分布

当程序运行时,系统会开辟 内核区、程序使用的内存五大区和保留区

1.1、数据类型

  • 值类型

    • 基本数据类型
    • 存入栈
    • 无需内存管理
  • 引用类型

    • 继承自NSObject的
    • 存入堆
    • 需要内存管理

1.2 内核区

  • 操作系统分为两种运行级别,分别是内核态与用户态,当程序运行,系统会分配出4G虚拟内存,其中内核区占用1G,用于进行内核处理操作的区域,剩余3G,会预留给程序使用的内存五大区和保留区

1.3、内存5大区

  • 堆区 Heap
    • OC 中,使用allocnew创建的对象
    • C 中,使用malloccallocrealloc开辟的空间,需要手动调用free函数对其释放
    • 一般以0x6开头
  • 栈区 Stack
    • 局部变量、方法的参数、对象的指针
    • 栈是系统数据结构,栈所对应的进程或线程是唯一的主线程的栈区为1M子线程为512K
    • 一般以0x7开头
  • 常量区 Constant
    • 编译阶段确定后不再改变
    • 常量区存储了程序中定义的死值,如 int a = 10;
    • 存储在.rodata
    • 一般以0x1开头
  • 全局区 Global(静态区 Static)
    • 全局变量、静态变量
    • 编译阶段确定后不再改变
    • 一般也以0x1开头
    • 未初始化的变量存储在BSS区(.bss),已初始化的变量存储在数据区(.data
  • 代码区
    • 编译生成的二进制文件
    • 存储在.text

2、iOS内存管理方案

2.1、Tagged Pointer(无需引用计数)

  • 针对小对象(存储内容小):NSDateNSNumberNSStringNSIndexPath
  • 使用后不再存储地址,而是真正的值;3倍的空间效率,106倍的访问速度
  • 在0的位置打上标记(Intel最低位为1,ARM最高位为1)
  • 初始化时会与一个随机值进行混淆
概述
  • NSString 为例,读取一个常规的 NSString,通过 栈区存储的指针地址,找到堆区空间,再从堆区读取到字符串的值,整个读取流程效率较低,所以,系统对其进行优化,如果 NSString 存储的 字符串长度较短(<= 9,会使用 Tagged Pointer 存储

  • Tagged Pointer 也是一个指针,指针中包含Tagged标记,用于区分存储的数据类型,同时 将值也存储在指针中,通过位运算将其编码成一个指针格式

NSString 内存管理的三种类型
  • NSTaggedPointerString:字符串优化类型,存储在 常量区
  • __NSCFConstantString:字符串常量,是一种 编译时常量retainCount 值很大,对其操作不会引起引用计数变化,存储在 字符串常量区
  • __NSCFString运行时 创建的 NSString 子类,创建后引用计数默认为 1,存储在 堆区
示例
- (void)viewDidLoad {
    [super viewDidLoad];

    NSString *str1 = @"Thank you!!!";
    NSString *str2 = @"Thank";
    NSString *str3 = @"谢谢你!";
    NSString *str4 = [NSString stringWithFormat:@"Thank you!!!"];
    NSString *str5 = [NSString stringWithFormat:@"Thank"];
    NSString *str6 = [NSString stringWithFormat:@"谢谢你!"];
    NSNumber *num  = [NSNumber numberWithShort:5];

    NSLog(@"%p %@",str1,[str1 class]);
    NSLog(@"%p %@",str2,[str2 class]);
    NSLog(@"%p %@",str3,[str3 class]);
    NSLog(@"%p %@",str4,[str4 class]);
    NSLog(@"%p %@",str5,[str5 class]);
    NSLog(@"%p %@",str6,[str6 class]);
    NSLog(@"%p %@",num,[num class]);
    
    // 打印结果
    TaggedPointerTest[1951:76449] 0x10766c028 __NSCFConstantString
    TaggedPointerTest[1951:76449] 0x10766c048 __NSCFConstantString
    TaggedPointerTest[1951:76449] 0x10766c068 __NSCFConstantString
    TaggedPointerTest[1951:76449] 0x60000026f760 __NSCFString
    TaggedPointerTest[1951:76449] 0xb8814bf3e7169112 NSTaggedPointerString
    TaggedPointerTest[1951:76449] 0x60000026f6a0 __NSCFString
    TaggedPointerTest[1951:76449] 0xa8814d4501001406 __NSCFNumber
    TaggedPointerTest[1951:76449] 0xa00006b6e6168545
}
  • 字符串直接通过 =号 赋值的不管中文还是英文都是 __NSCFConstantString 类型,存在 字符串常量区

  • 当字符串是由 数字、英文字母(存的是ASCII码,所以ASCII表中的都可以) 组合且长度<= 9,会自动成为NSTaggedPointerString类型,存储在常量区

  • 当字符串中包含 中文或者其他特殊符号,会直接成为__NSCFString类型,存储在堆区

2.1.1、tagged pointer解码
  • _objc_encodeTaggedPointer:objc源码中tagged pointer的 混淆编码方法

    static inline void * _Nonnull
    _objc_encodeTaggedPointer(uintptr_t ptr)
    {
        // 与随机值 objc_debug_taggedpointer_obfuscator 进行异或来混淆
        uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
    
    // ARM64架构会进行额外操作    
    #if OBJC_SPLIT_TAGGED_POINTERS
        if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
            return (void *)ptr;
        uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
        uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag);
        value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
        value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;
    #endif
        return (void *)value;
    }
    
  • 与随机值objc_debug_taggedpointer_obfuscator进行异或来混淆,而这个随机数是在以前探索dyld读取image时的_read_images方法中的 initializeTaggedPointerObfuscator() 方法进行的生成

    void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
    {
        ......
    
        if (DisableTaggedPointers) {
            disableTaggedPointers();
        }
        // 进程启动时初始化一个随机的值,用于混淆tagged pointer指针
        initializeTaggedPointerObfuscator();
    
        ......
    }
    
2.1.2、tagged pointer解码
  • _objc_decodeTaggedPointer:objc源码中tagged pointer的 混淆解码方法

    static inline uintptr_t
    _objc_decodeTaggedPointer(const void * _Nullable ptr)
    {
        uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr);
    #if OBJC_SPLIT_TAGGED_POINTERS
        uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
    
        value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
        value |= _objc_obfuscatedTagToBasicTag(basicTag) << _OBJC_TAG_INDEX_SHIFT;
    #endif
        return value;
    }
    
  • 随机数objc_debug_taggedpointer_obfuscator为全局静态变量,我们使用extern修饰,将其导出,自己实现一个解码函数,使用相同的值,将指针再次按位 异或 即可还原

  • objc源码中的解码我们无法拿来直接用,因此我们将内容拷贝出来加以修改,在自己的项目中当做一个解码函数,但要注意因为模拟器与真机架构不同,上边代码中也提到了ARM64架构下的处理更多,因此这个解码函数在真机下的修改方式更繁琐,直接给出使用代码(粘贴就完事了~):

    // 模拟器下解码函数
    extern uintptr_t objc_debug_taggedpointer_obfuscator;
    uintptr_t
    
    // lz_objc_decodeTaggedPointer() 解码函数
    lz_objc_decodeTaggedPointer(id ptr)
    {
        return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
    }
    
    // 真机下解码函数
    #define lz_OBJC_TAG_INDEX_MASK 0x7UL
    #define lz_OBJC_TAG_INDEX_SHIFT 0
    
    extern uintptr_t objc_debug_taggedpointer_obfuscator;
    extern uint8_t objc_debug_tag60_permutations[8];
    
    uintptr_t lz_objc_obfuscatedTagToBasicTag(uintptr_t tag) {
        for (unsigned i = 0; i < 7; i++)
            if (objc_debug_tag60_permutations[i] == tag)
                return i;
        return 7;
    }
    
    // lz_objc_decodeTaggedPointer() 解码函数
    uintptr_t
    lz_objc_decodeTaggedPointer(id ptr)
    {
        uintptr_t value = (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
    
        uintptr_t basicTag = (value >> lz_OBJC_TAG_INDEX_SHIFT) & lz_OBJC_TAG_INDEX_MASK;
    
        value &= ~(lz_OBJC_TAG_INDEX_MASK << lz_OBJC_TAG_INDEX_SHIFT);
        value |= lz_objc_obfuscatedTagToBasicTag(basicTag) << lz_OBJC_TAG_INDEX_SHIFT;
    
        return value;
    }
    
    static inline uintptr_t lz_objc_basicTagToObfuscatedTag(uintptr_t tag) {
        return objc_debug_tag60_permutations[tag];
    }
    
    // lz_objc_encodeTaggedPointer() 编码函数
    void *
    lz_objc_encodeTaggedPointer(uintptr_t ptr)
    {
        uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
    
        uintptr_t basicTag = (value >> lz_OBJC_TAG_INDEX_SHIFT) & lz_OBJC_TAG_INDEX_MASK;
        uintptr_t permutedTag = lz_objc_basicTagToObfuscatedTag(basicTag);
        value &= ~(lz_OBJC_TAG_INDEX_MASK << lz_OBJC_TAG_INDEX_SHIFT);
        value |= permutedTag << lz_OBJC_TAG_INDEX_SHIFT;
        return (void *)value;
    }
    
  • 使用p/t命令打印二进制内容(图中为模拟器下读取方式,真机读取规则与模拟器不同) image.png

    • 0b表示二进制,将二进制数据分段打印(po命令)时也要在前边加上 0b
    • 而且我们从图中可以看出,存储值的区域能容纳7个字符,>=7、<=9个时虽然还是 NSTaggedPointerString 类型,但会改变存储方式: image.png
    • Tagged Pointer 结构,在ARM64架构下:
      • 高地址第一位,标志位,表示该 isa 为 Tagged Pointer 类型,可在 objc_tag_index_t 中找到对应
        enum objc_tag_index_t : uint16_t
        #else
        typedef uint16_t objc_tag_index_t;
        enum
        #endif
        {
            // 60-bit payloads
            OBJC_TAG_NSAtom            = 0OBJC_TAG_1                 = 1OBJC_TAG_NSString          = 2OBJC_TAG_NSNumber          = 3OBJC_TAG_NSIndexPath       = 4OBJC_TAG_NSManagedObjectID = 5OBJC_TAG_NSDate            = 6,
        
            // 60-bit reserved
            OBJC_TAG_RESERVED_7        = 7,
        
            ......
        }
        
      • 低地址最后三位,表示存储的类型,如:NSNumberNSIndexPathNSDateNSString
      • 低地址4~7位,记录扩展信息,例如:存储 NSString 记录字符串长度、存储 NSNumber 记录基本数据类型的枚举值
      • 其余56位,用于存储值

2.2、引用计数

  • 在前边探索alloc原理时我们探索过创建isa的方法initIsa,搜索流程:objc_alloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone --> initIsa,在里边有一个extrc_rc就是用于记录对象的引用计数的

    inline void 
    objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
    { 
        ASSERT(!isTaggedPointer()); 
    
        isa_t newisa(0);
    
        if (!nonpointer) {
            // 非nonpointer
            newisa.setClass(cls, this);
        } else {
            ASSERT(!DisableNonpointerIsa);
            ASSERT(!cls->instancesRequireRawIsa());
    
    #if SUPPORT_INDEXED_ISA
            ASSERT(cls->classArrayIndex() > 0);
            newisa.bits = ISA_INDEX_MAGIC_VALUE;
            // isa.magic is part of ISA_MAGIC_VALUE
            // isa.nonpointer is part of ISA_MAGIC_VALUE
            newisa.has_cxx_dtor = hasCxxDtor;
            newisa.indexcls = (uintptr_t)cls->classArrayIndex();
    #else
            newisa.bits = ISA_MAGIC_VALUE;
            // isa.magic is part of ISA_MAGIC_VALUE
            // isa.nonpointer is part of ISA_MAGIC_VALUE
    #   if ISA_HAS_CXX_DTOR_BIT
            newisa.has_cxx_dtor = hasCxxDtor;
    #   endif
            newisa.setClass(cls, this);
    #endif
            // 创建isa,引用计数赋值为1
            newisa.extra_rc = 1;
        }
    
        isa = newisa;
    }
    
  • 储存对象的引用计数有下面几种情况:

    • 非nonpointerIsa,直接存在sidetable
    • nonpointerIsa
      • 对象正在被释放,不做操作
      • extrc_rc 能存下,存在 extrc_rc 字段中
      • extrc_rc 存不下,将一半存到 sidetable 中,并将has_sidetable_rc标志位置为 true
2.2.1、nonpointerIsa(开启指针优化的指针)
  • 现在大部分指针都是nonpointerIsa
  • 非纯指针类型的 isaisa 中包含了 类信息对象的引⽤计数
2.2.2、retain、release原理
  • retain 底层搜索 objc_retain

    __attribute__((aligned(16), flatten, noinline))
    objc_retain(id obj)
    {
        // 如果是 tagged pointer 就直接返回对象,也就是说无需引用计数管理
        if (obj->isTaggedPointerOrNil()) return obj;
        return obj->retain();
    }
    
  • objc_retain 中的关键方法是 rootRetain

    ALWAYS_INLINE id
    objc_object::rootRetain(bool tryRetain, objc_object::RRVariant variant)
    {
        if (slowpath(isTaggedPointer())) return (id)this;
    
        bool sideTableLocked = false;
        bool transcribeToSideTable = false;
    
        isa_t oldisa;
        isa_t newisa;
    
        oldisa = LoadExclusive(&isa.bits);
    
        if (variant == RRVariant::FastOrMsgSend) {
            // These checks are only meaningful for objc_retain()
            // They are here so that we avoid a re-load of the 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));
            }
        }
    
        if (slowpath(!oldisa.nonpointer)) {
            // a Class is a Class forever, so we can perform this check once
            // outside of the CAS loop
            if (oldisa.getDecodedClass(false)->isMetaClass()) {
                ClearExclusive(&isa.bits);
                return (id)this;
            }
        }
    
        do {
            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;
            
            // 是nonpointerIsa,且 extra_rc 能容纳
            newisa.bits = addc(newisa.bits, RC_ONE, 0, &carry);  // extra_rc++
    
            if (slowpath(carry)) {
                // 是nonpointerIsa,但 extra_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;
                // extra_rc改为只存一半引用计数
                newisa.extra_rc = RC_HALF;
                // 一半存到 sidetable 中,要打上标记
                newisa.has_sidetable_rc = true;
            }
        } while (slowpath(!StoreExclusive(&isa.bits, &oldisa.bits, newisa.bits)));
    
        if (variant == RRVariant::Full) {
            if (slowpath(transcribeToSideTable)) {
                // Copy the other half of the retain counts to the side table.
                // sidetable中存入剩余一半引用计数
                sidetable_addExtraRC_nolock(RC_HALF);
            }
    
            if (slowpath(!tryRetain && sideTableLocked)) sidetable_unlock();
        } else {
            ASSERT(!transcribeToSideTable);
            ASSERT(!sideTableLocked);
        }
    
        return (id)this;
    }
    
  • release 底层搜索 objc_release,流程与retain相反,无需赘述,需要注意的是当引用计数减为0时,会向对象 发送dealloc消息

    __attribute__((aligned(16), flatten, noinline))
    void 
    objc_release(id obj)
    {
        // 如果是 tagged pointer 就不做任何操作,无需引用计数管理
        if (obj->isTaggedPointerOrNil()) return;
        return obj->release();
    }
    
    ALWAYS_INLINE bool
    objc_object::rootRelease(bool performDealloc, objc_object::RRVariant variant)
    {
        ......
    
    deallocate:
        // Really deallocate.
        ASSERT(newisa.isDeallocating());
        ASSERT(isa.isDeallocating());
    
        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;
    }
    
2.2.3、dealloc
  • dealloc 底层搜索 _objc_rootDealloc
    inline void
    objc_object::rootDealloc()
    {
        if (isTaggedPointer()) return// fixme necessary?
    
        // 是nonpointer 并且同时满足以下条件时才会free
        if (fastpath(isa.nonpointer                     &&
                     // 没有弱引用
                     !isa.weakly_referenced             &&
                     // 没有关联对象
                     !isa.has_assoc                     &&
    #if ISA_HAS_CXX_DTOR_BIT
                     // 没有C++的析构函数
                     !isa.has_cxx_dtor                  &&
    
    #else
                     !isa.getClass(false)->hasCxxDtor() &&
    
    #endif
                     // 没有 sidetable 借位
                     !isa.has_sidetable_rc))
        {
            assert(!sidetable_present());
            // 释放对象
            free(this);
        } 
        else {
            // 调用C++析构函数、删除关联对象、清空散列表中引用计数信息、清空弱引用表中相关数据
            object_dispose((id)this);
        }
    }
    
    对象为nonpointerIsa,并且没有弱引用、没有关联对象、没有C++析构函数、没有sidetable借位 时才对对象进行释放,否则先将这些内容抹掉再free对象