iOS-底层原理29:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析

本文主要是分析内存管理中的内存管理方案,以及retainretainCountreleasedealloc的底层源码分析

1. ARC & MRC

iOS中的内存管理方案,大致可以分为两类:MRC(手动内存管理)和ARC(自动内存管理)

1.1 MRC

在MRC时代,系统是通过对象的引用计数来判断一个是否销毁,有以下规则:

  • 对象被创建时引用计数都为1
  • 当对象被其他指针引用时,需要手动调用[objc retain],使对象的引用计数+1
  • 当指针变量不再使用对象时,需要手动调用[objc release]释放对象,使对象的引用计数-1
  • 当一个对象的引用计数为0时,系统就会销毁这个对象

所以,在MRC模式下,必须遵守:谁创建,谁释放,谁引用,谁管理

1.2 ARC

ARC模式是在WWDC2011和iOS5引入的自动管理机制,即自动引用计数。是编译器的一种特性。其规则与MRC一致,区别在于,ARC模式下不需要手动retain、release、autorelease。编译器会在适当的位置插入release和autorelease

2. 内存管理方案

内存管理方案除了上面提及的MRCARC,还有以下三种

  • Tagged Pointer:专门用来处理小对象,例如NSNumber、NSDate、小NSString等

  • Nonpointer_isa:非指针类型的isa,主要是用来优化64位地址,这个在iOS-底层原理7:isa与类关联的原理一文中,已经介绍了

  • SideTables散列表,在散列表中主要有两个表,分别是引用计数表弱引用表

这里主要着重介绍Tagged PointerSideTables,我们通过一个面试题来引入Tagged Pointer

2.1 面试题

//*********代码1*********
- (void)taggedPointerDemo {
  self.queue = dispatch_queue_create("com.lbh.cn", DISPATCH_QUEUE_CONCURRENT);
    
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"iOS"];  // alloc 堆 iOS优化 - taggedpointer
             NSLog(@"%@",self.nameStr);
        });
    }
}

运行以上代码,一切正常。

将上面额代码稍微改动下

//*********代码2*********
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event{
    NSLog(@"来了");
    for (int i = 0; i<10000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"iOS_努力学习底层,不做代码搬运工"];
            NSLog(@"%@",self.nameStr);
        });
    }
}

运行程序,点击屏幕,程序会崩溃

问题: 为什么会崩溃?

解答:
崩溃的原因是多条线程同时对一个对象进行释放,导致了 过渡释放所以崩溃。其根本原因是因为nameStr在底层的类型不一致导致的,我们可以通过调试看出

iOS-底层原理29:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析_第1张图片
iOS-底层原理29:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析_第2张图片
  • taggedPointerDemo方法中的nameStr类型是NSTaggedPointerString,存储在常量区。因为nameStralloc分配时在堆区,由于较小,所以经过xcode中iOS的优化,成了NSTaggedPointerString类型,存储在常量区

  • touchesBegan方法中的nameStr类型是NSCFString类型,存储在堆上

2.2 NSString的内存管理

我们可以通过NSString初始化的两种方式,来测试NSString的内存管理

  • 通过 WithString + @""方式初始化
  • 通过 WithFormat方式初始化
#define KLog(_c) NSLog(@"%@ -- %p -- %@",_c,_c,[_c class]);

- (void)testNSString{
    //初始化方式一:通过 WithString + @""方式
    NSString *s1 = @"1";
    NSString *s2 = [[NSString alloc] initWithString:@"222"];
    NSString *s3 = [NSString stringWithString:@"33"];
    
    KLog(s1);
    KLog(s2);
    KLog(s3);
    
    //初始化方式二:通过 WithFormat
    //字符串长度在9以内
    NSString *s4 = [NSString stringWithFormat:@"123456789"];
    NSString *s5 = [[NSString alloc] initWithFormat:@"123456789"];
    
    //字符串长度大于9
    NSString *s6 = [NSString stringWithFormat:@"1234567890"];
    NSString *s7 = [[NSString alloc] initWithFormat:@"1234567890"];
    
    KLog(s4);
    KLog(s5);
    KLog(s6);
    KLog(s7);
}

运行结果

所以,从上面可以总结出,NSString的内存管理主要分为3种

  • __NSCFConstantString:字符串常量,是一种编译时常量,retainCount值很大,对其操作,不会引起引用计数变化,存储在字符串常量区

  • __NSCFString:是在运行时创建的NSString子类,创建后引用计数会加1,存储在堆上

  • NSTaggedPointerString:标签指针,是苹果在64位环境下对NSString、NSNumber等对象做的优化。对于NSString对象来说

    • 当字符串是由数字、英文字母组合且长度小于等于9时,会自动成为NSTaggedPointerString类型,存储在常量区

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

3. Tagged Pointer 小对象

由一个NSString的面试题,引出了Tagged Pointer,为了探索小对象的引用计数处理,所以我们需要进入objc源码中查看retainrelease源码 中对 Tagged Pointer小对象的处理

3.1 小对象的引用计数处理分析

step1: 查看setProperty -> reallySetProperty源码,其中是对新值retain,旧值release

iOS-底层原理29:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析_第3张图片

step2: 进入objc_retainobjc_release源码,在这里都判断是否是小对象,如果是小对象,则不会进行retain或者release,会直接返回。因此可以得出一个结论:如果对象是小对象,不会进行retain 和 release

//****************objc_retain****************
__attribute__((aligned(16), flatten, noinline))
id 
objc_retain(id obj)
{
    if (!obj) return obj;
    //判断是否是小对象,如果是,则直接返回对象
    if (obj->isTaggedPointer()) return obj;
    //如果不是小对象,则retain
    return obj->retain();
}

//****************objc_release****************
__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{
    if (!obj) return;
    //如果是小对象,则直接返回
    if (obj->isTaggedPointer()) return;
    //如果不是小对象,则release
    return obj->release();
}

3.2 小对象的地址分析

继续以NSString为例,对于NSString来说

  • 一般的NSString对象指针,都是string值 + 指针地址,两者是分开的

  • 对于Tagged Pointer指针,其指针+值,都能在小对象中体现。所以Tagged Pointer 既包含指针,也包含值

在之前的文章讲类的加载时,其中的_read_images源码有一个方法对小对象进行了处理,即initializeTaggedPointerObfuscator方法

step1: 进入_read_images --> initializeTaggedPointerObfuscator源码实现

static void
initializeTaggedPointerObfuscator(void)
{
   // sdkIsOlderThan(mac, ios, tv, watch, bridge)
    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;
    }
    //在iOS12之后,对小对象进行了混淆,通过与操作+_OBJC_TAG_MASK混淆
    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;
    }
}

在实现中,我们可以看出,在iOS12之后,Tagged Pointer采用了混淆处理,如下所示

iOS-底层原理29:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析_第4张图片

step2: 我们可以在源码中通过objc_debug_taggedpointer_obfuscator查找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;

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

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

所以在外界,为了获取小对象的真实地址,我们可以将解码的源码拷贝到外面,将NSString混淆部分进行解码,如下所示

iOS-底层原理29:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析_第5张图片

观察解码后的小对象地址,其中的62表示bASCII码,再以NSNumber为例,同样可以看出,1就是我们实际的值

iOS-底层原理29:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析_第6张图片

到这里,我们验证了小对象指针地址中确实存储了值,那么小对象地址高位其中的0xa0xb又是什么含义呢?

//NSString
0xa000000000000621

//NSNumber
0xb000000000000012
0xb000000000000025

需要去源码中查看_objc_isTaggedPointer源码,主要是通过保留最高位的值(即64位的值),判断是否等于_OBJC_TAG_MASK(即2^63),来判断是否是小对象

static inline bool 
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    //等价于 ptr & 1左移63,即2^63,相当于除了64位,其他位都为0,即只是保留了最高位的值
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

所以0xa0xb主要是用于判断是否是小对象taggedpointer,即判断条件,判断第64位上是否为1(taggedpointer指针地址即表示指针地址,也表示值)

  • 0xa 转换成二进制为 1 010(64为为1,63~61后三位表示 tagType类型 - 2),表示NSString类型
  • 0xb 转换为二进制为 1 011(64为为1,63~61后三位表示 tagType类型 - 3),表示NSNumber类型,这里需要注意一点,如果NSNumber的值是-1,其地址中的值是用补码表示的

这里可以通过_objc_makeTaggedPointer方法的参数tag类型objc_tag_index_t进入其枚举,其中 2表示NSString,3表示NSNumber

iOS-底层原理29:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析_第7张图片

同理,我们可以定义一个NSDate对象,来验证其tagType是否为6。通过打印结果,其地址高位是0xe,转换为二进制为1 110,排除64位的1,剩余的3位正好转换为十进制是6,符合上面的枚举值

iOS-底层原理29:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析_第8张图片

Tagged Pointer 总结

  • Tagged Pointer小对象类型(用于存储NSNumber、NSDate、小NSString),小对象指针不再是简单的地址,而是地址 + 值,即真正的值,所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以可以直接进行读取。优点是占用空间小 节省内存

  • Tagged Pointer小对象 不会进入retain 和 release,而是直接返回了,意味着不需要ARC进行管理,所以可以直接被系统自主的释放和回收

  • Tagged Pointer的内存并不存储在堆中,而是在常量区中,也不需要malloc和free,所以可以直接读取,相比存储在堆区的数据读取,效率上快了3倍左右。创建的效率相比堆区快了近100倍左右

  • 所以,综合来说,taggedPointer的内存管理方案,比常规的内存管理,要快很多

  • Tagged Pointer的64位地址中,前4位代表类型,后4位主要适用于系统做一些处理,中间56位用于存储值

优化内存建议:对于NSString来说,当字符串较小时,建议直接通过@""初始化,因为存储在常量区,可以直接进行读取。会比WithFormat初始化方式更加快速

4. SideTables 散列表

引用计数存储到一定值时,并不会再存储到Nonpointer_isa的位域的extra_rc中,而是会存储到SideTables散列表中

注意: 这里是针对源码objc781所讲

下面我们就来继续探索引用计数retain的底层实现

4.1 retain 源码分析

你可能感兴趣的:(iOS-底层原理29:内存管理(一)TaggedPointer/retain/release/dealloc/retainCount 底层分析)