底层原理-31-内存管理之TaggedPointer/retain/release/dealloc分析

程序运行分配的内存是一定的,因此内存管理就是很有必要,开发中避免内存泄漏,我们因及时释放。那么内存是如何管理的?

1. ARC和MRC

在iOS开发中,我们有大致2种内存管理方式:MRC(手动管理)和ARC(自动管理)

  • MRC:在手动内存管理环境下遵循以下规则
  1. 对象被创建,引用计数+1
  2. 对象被其它指针引用时,引用计数+1,需要手动调用【objc retain】
  3. 当指针变量不再引用对象时,要手动释放,调用【objc release】,引用计数-1;
  4. 当引用计数为0时,系统会自动销毁这个对象。

大体上就是在MRC环境下,对象谁创建,谁释放;谁引用,谁管理

  • ARC:自动内存管理,编译器帮我操作引用计数,进行 retainrelease等操作。

2. 内存五大区

我们之前知道程序运行的时候系统会给程序分配虚拟内存,32位操作系统最大分配4G内存,64位操作系统最多最大分配8G内存,为了保持兼容通常程序还是申请4g虚拟内存。在iOS开发中,以4g内存为例,程序加载的内存分布如下

image.png

  • 内核区占有1GB,主要系统进行内核处理操作的区域。
  • 五大区
    • 栈区:函数,方法;栈区的内存地址一般是0x7开头
    • 堆区:通过alloc分配的对象,block copy;堆区的内存地址一般为0x6开头
    • BBS段:未初始化的全局变量,静态变量;内存地址一般为0x1开头
    • 数据段:已初始化的全局变量,静态变量;内存地址一般为0x1开头
    • 代码段:程序代码,加载到内存中。
  • 保留区:主要原因是0x00000000表示nil,不能直接用nil表示一个段,所以单独给了一段内存用于处理nil等情况

image.png

3. 内存管理方案

内存管理方案还有:

  • taggedPointer: 专门用来处理小对象,例如NSNumber、NSDate、小NSString等
  • Nonpointer_isa: 非指针类型的isa
  • SideTables:散列表,包括引用计数列表和弱引用表

3.1 TaggedPointer

我们先看看原有的对象为什么会浪费内存。假设我们要存储一个 NSNumber 对象,其值是一个整数。正常情况下,如果这个整数只是一个 NSInteger 的普通变量,那么它所占用的内存是与 CPU 的位数有关,在 32 位 CPU 下占 4 个字节,在 64 位 CPU 下是占 8 个字节的。而指针类型的大小通常也是与 CPU 位数相关,一个指针所占用的内存在 32 位 CPU 下为 4 个字节,在 64 位 CPU 下也是 8 个字节。
为了改进上面提到的内存占用和效率问题,苹果提出了Tagged Pointer对象。由于 NSNumber、NSDate 一类的变量本身的值需要占用的内存大小常常不需要 8 个字节

image.png
我们也可以在 WWDC2013 的《Session 404 Advanced in Objective-C》视频中,看到苹果对于Tagged Pointer特点的介绍:

  1. Tagged Pointer专门用来存储小的对象,例如NSNumberNSDate
  2. Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。
  3. 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。

由此可见,苹果引入Tagged Pointer,不但减少了 64 位机器下程序的内存占用,还提高了运行效率。完美地解决了小内存对象在存储和访问效率上的问题
之后在 WWDC2020年 上对arm64上标记指针格式进行了更改
由于对齐要求,低位总是为零。指针大小的倍数,地址高位总是为零,因为地址空间有限。我们实际上不会一直到2到64这些高位和低位总是零位。因此,让我们从这些总是零的位中选择一个,并把它变成一个这可以立即告诉我们这不是一个真正的对象指针然后我们可以为所有其他位分配一些其他含义。我们称之为标记指针

3.1.1 模拟器环境下TaggedPointer

我们运行下面代码,地址是0x9开头和我们之前说的在堆区栈区都不同,类型是NSTaggedPointerString

image.png

  • NSCFConstantString:字符串常量,是一种编译时常量,retainCount值很大,对其操作,不会引起引用计数变化,存储在字符串常量区
  • NSCFString:是在运行时创建的NSString子类,创建后引用计数会加1,存储在堆上
  • NSTaggedPointerString:标签指针,是苹果在64位环境下对NSStringNSNumber等对象做的优化。对于NSString对象来说
    • 当字符串是由数字、英文字母组合且长度小于等于9时,会自动成为NSTaggedPointerString类型,存储在常量区
    • 当有中文或者其他特殊符号时,会直接成为__NSCFString类型,存储在堆区
  • 对于NSString来说,当字符串较小时,建议直接通过@""初始化,因为存储在常量区,可以直接进行读取。会比WithFormat初始化方式更加快速

我们查看源码TaggedPointer搜索
看到这里有关于playload的位置运算,说明这里有加密和解密的过程。

// payload = (decoded_obj << payload_lshift) >> payload_rshift

// Payload signedness is determined by the signedness of the right-shift.
复制代码

那么接下来就去搜索decoded:加密。

static inline uintptr_t

_objc_decodeTaggedPointer_noPermute(const void * _Nullable ptr)

{

    uintptr_t value = (uintptr_t)ptr;

#if OBJC_SPLIT_TAGGED_POINTERS

    if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)

        return value;

#endif

    return value ^ objc_debug_taggedpointer_obfuscator;//异或处理

}


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进行异或处理,我们继续查看混淆器起初始化我们曾经在探索源码readImags中也初始化过

image.png

static void

initializeTaggedPointerObfuscator(void)

{

    if (!DisableTaggedPointerObfuscation) {

        // 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;//将随机数据放入变量中,然后移走所有非有效负载因子。



#if OBJC_SPLIT_TAGGED_POINTERS

        // The obfuscator doesn't apply to any of the extended tag mask or the no-obfuscation bit.

        objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK);

        // Shuffle the first seven entries of the tag permutator.

        int max = 7;

        for (int i = max - 1; i >= 0; i--) {

            int target = arc4random_uniform(i + 1);

            swap(objc_debug_tag60_permutations[i],

                 objc_debug_tag60_permutations[target]);

        }

#endif

    } else {

        // Set the obfuscator to zero for apps linked against older SDKs,

        // in case they're relying on the tagged pointer representation.

        objc_debug_taggedpointer_obfuscator = 0;

    }

}
复制代码

这里如果开启了混淆,那么就会得到一个随机的objc_debug_taggedpointer_obfuscator值,否则就为0。
继续看下解码encode,进行一次异或

static inline void * _Nonnull

_objc_encodeTaggedPointer(uintptr_t ptr)

{

    uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);

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

}
复制代码

通过实现,我们可以得知,在编码和解码部分,经过了两层异或,其目的是得到小对象自己,例如以 1010 0001为例,假设mask为 0101 1000

    1010 0001 
   ^0101 1000 mask(编码)
    1111 1001
   ^0101 1000 mask(解码)
    1010 0001
复制代码

我们之前打印的就是标记指针解码后的值

image.png
而之前的左移右移操作,则是在_objc_makeTaggedPointer里面。这里可以看出来先进行移动,在进行加密。

image.png

因此我们要向右移动3位,去掉tag,首位是空的因此不存储值,从第二位开始每8位存储一个字节表示一个字母,小端模式从左到右

image.png
对照ASCII表:test

image.png
taggedPointer的是特殊的指针,我们看下64位为1表示taggedPointer的标记,其中63~61表示类型。上图中的NSString类型就是1010其中63-61位就是2

{
    // 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_NSMethodSignature = 20,
    OBJC_TAG_UTTypeRecord      = 21,

    // When using the split tagged pointer representation
    // (OBJC_SPLIT_TAGGED_POINTERS), this is the first tag where
    // the tag and payload are unobfuscated. All tags from here to
    // OBJC_TAG_Last52BitPayload are unobfuscated. The shared cache
    // builder is able to construct these as long as the low bit is
    // not set (i.e. even-numbered tags).
    OBJC_TAG_FirstUnobfuscatedSplitTag = 136, // 128 + 8, first ext tag with high bit set

    OBJC_TAG_Constant_CFString = 136,

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

    OBJC_TAG_RESERVED_264      = 264
};
复制代码

3.1.2 真机下TaggedPinter

image.png
原理和模拟器类似,储存位置有些不一样

  • 1位是标记taggedPointer
  • 类型是存储低三位 char 0,short 1,int 2,3 long,4 float,5 double。
  • 再住走四位,存储的是数据的长度
  • 再往后是数据的内容

3.1.3 面试题


//MARK: - taggedPointer 面试题1

- (void)taggedPointerDemo {

  

    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);

    

    for (int i = 0; i<10000; i++) {

        dispatch_async(queue, ^{

            self.nickName = [NSString stringWithFormat:@"test"];

             NSLog(@"%@",self.nickName);

        });

    }

}

//MARK: - taggedPointer 面试题2

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    NSLog(@"来了");

    // 多线程 读和写

    // setter -> retian release

    dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);

    for (int i = 0; i<100000; i++) {

        dispatch_async(queue, ^{

            self.nickName = [NSString stringWithFormat:@"无敌超级海景哈哈哈哈哈"];

            NSLog(@"%@",self.nickName);

        });

    }

}

复制代码

运行面试题1和2会怎么样,面试1不会发生崩溃,面试题2发生崩溃。我们之前的学习中知道,我们在多线程异步队列下,同时对一个值进行读写会发生崩溃,我们赋值的时候会对对新值进行retain旧值进行release操作,因为是异步的导致同一时间会发生2次release,再次读取就会因为野指针报错。

  • 我们看下为什么面试题1没有发生崩溃。

image.png
这里表明这个nickName是NSTaggedPointerString,我们上面也探讨过对于taggedPointer它的值直接存储在指针中,没有指针指向的内存,因此不对对指针指向的内存操作,因此不会发生崩溃。看下源码的说明:

image.png

  • 在demo2中我们看下NSString的类型

image.png
类型为NSCFString,存放在堆区不是taggedPointer类型因此会发生上述我们分析的情况。

3.2 NONPOINTER_ISA

我们之前isa学习的时候知道了这里简单说明下:

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

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

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

  • shiftcls:存储类指针的值(相当于taggedPointer的payload)。开启指针优化的情况下,在arm64架构中有33位⽤来存储类指针。

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

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

  • deallocating:标志对象是否正在释放内存

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

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

3.3 散列表SideTable

当对象引⽤技术⼤于10时,则需要借⽤该变量存储进位
我们就来继续探索引用计数retain的底层实现

3.1 retain

我们通过源码分析下retain的流程

  1. objc_retain

image.png
2. retain()

image.png
3. rootRetain

image.png
我们主要看下rootRetain

  • 先判断是否是taggedPointer,是的话直接返回。
  • 判断是否是nonpointer,不是的话 判断类的话,不是对象,不对引用计数操作
  • 进行dowhile循环
    • 不是nonpointer,即是纯的isa,直接对散列表操作
    • 正在释放,判断是不是在析构,如果在析构就没必要-1操作,在多线程的情况下,已经在释放,还有可能-1
    • bits中进行++操作,并给一个引用计数的状态标识carry,用于表示extra_rc是否满了
    • 如果carray的状态表示extra_rc的引用计数满了,此时需要操作散列表,即 将满状态的一半拿出来存到extra_rc,另一半存在 散列表的rc_half。这么做的原因是因为如果都存储在散列表,每次对散列表操作都需要开解锁,操作耗时,消耗性能大,这么对半分操作的目的在于提高性能

3.2 relseas

release流程和retain类似,通过setProperty -> reallySetProperty -> objc_release -> release -> rootRelease -> rootRelease顺序,进入rootRelease源码

image.png
其流程和retain相反

  • 先判断是否是taggedPointer,是的话直接返回。
  • 判断是否是nonpointer,不是的话 判断是类的话,不是对象,不对引用计数操作
  • 进行dowhile循环
    • 非nonpointer的话直接对散列表处理,引用计数-1
    • isDeallocating:正在析构,就不处理,去执行dealloc流程
    • extra_rc-- 进行引用计数-1操作,即extra_rc-1,如果此时extra_rc的值为0了,则走到underflow
  • underflow
    • has_sidetable_rc:判断散列表中是否存储了一半的引用计数,没有的话自动销毁
    • sidetable_subExtraRC_nolock:从散列表取出一半的引用计数,进行-1操作,然后存储到extra_rc

3.3 dealloc

查看dealloc -> _objc_rootDealloc -> rootDealloc源码实现

image.png

  • 根据条件判断是否有isa、cxx、关联对象、弱引用表、引用计数表,如果没有,则直接free释放内存
  • 如果有,则进入object_dispose方法

image.png

  • 继续查看objc_destructInstance

image.png

  • 查看clearDeallocating

image.png

4. 总结

  • 关于内存管理我们知道了苹果通过对ARCMRCtaggedPointer,引用计数的retainrelease进行管内存理释放对象。在ARC环境下,不需要我们手动管理对象的引用计数。
  • 对于一些小的对象比如NSStringNSNumberNSdate等进行了指针优化,把值存储到指针中,通过特定的标记进行区分TaggedPointer以及它的类型
  • 对象的isa中根据是否是纯的isa进行相对应处理,纯的isa引用计数存放在散列表中,非纯的isa则存在isa中的extra_rc中。在retain中,存满了取出一半存放在散列表中,extra_rc++
  • release中,extra_rc--后为0的话判断散列表是否为空,为空走delloc流程,不为空则取出散列表出一半的引用计数-1操作之后存储到extra_rc中。
  • delloc主要是对对象的引用计数列表关联对象列表,弱引用表进行销毁,释放内存。
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享