16 - 小对象类型TaggedPointer

682 阅读11分钟

OC底层原理探索文档汇总

主要内容:

1、小对象类型的认识

2、字符串的内存存储类型

3、小对象类型的结构

4、一个面试题的简单分析

1、小对象类型的认识

什么是小对象类型?

TaggedPointer字面意思是带标签的指针,本质是一个指针,但是不像NSString那样单纯的指针,而是会带有数据本身。也就是说小对象类型其实就是带有数据信息的指针。

对于NSNumber、NSDate、小NSString,他们所需要存储的信息特别少,如果使用对象来存放,会有很大的浪费。 所以出现了小对象类型,它不是一个对象,不存储在堆中,而是存储在常量区,使用的效率较高,占用内存小。

哪些是小对象类型? NSNumber、NSData、小NSString

为什么需要小对象?

  • 一个对象最少需要8个字节,占用内存,且对其的操作比较复杂,消耗性能
  • 所以使用小对象类型可以减少性能的消耗
  • 可以减少内存的消耗

2、字符串的内存存储类型

通过字符串的存储类型查看小对象类型与对象类型的区别。

代码:

#define KLog(_c) NSLog(@"%@ -- %p -- %@",_c,_c,[_c class]);

- (void)testNSString{
    //初始化方式一:通过 WithString + @""方式
    //这种是常量类型
    //事实上,WithString就等同于直接赋值,没有区别,所以苹果建议直接使用字符串赋值
    NSString *s1 = @"1";
    NSString *s2 = [[NSString alloc] initWithString:@"222"];
    NSString *s3 = [NSString stringWithString:@"33"];
    NSString *s22 = @"fjkdfjkdajfadkfjadlk;fj看对方金阿奎射流风机安抚巾";
    
    KLog(s1);
    KLog(s2);
    KLog(s3);
    KLog(s22);
    
    //初始化方式二:通过 WithFormat,且字符串长度在9以内
    //这种是小对象类型
    NSString *s4 = [NSString stringWithFormat:@"123456789"];
    NSString *s5 = [[NSString alloc] initWithFormat:@"123456789"];
    KLog(s4);
    KLog(s5);
    
    //初始化方式二:通过 WithFormat,且字符串长度大于9
    //这种是引用类型
    NSString *s6 = [NSString stringWithFormat:@"1234567890"];
    NSString *s7 = [[NSString alloc] initWithFormat:@"1234567890"];
    KLog(s6);
    KLog(s7);
}

输出结果:

2021-11-10 18:56:25.869875+0800 小对象类型[27549:1517240] 1 -- 0x10300d0c0 -- __NSCFConstantString
2021-11-10 18:56:25.869970+0800 小对象类型[27549:1517240] 222 -- 0x10300d0e0 -- __NSCFConstantString
2021-11-10 18:56:25.870056+0800 小对象类型[27549:1517240] 33 -- 0x10300d100 -- __NSCFConstantString
2021-11-10 18:56:25.870130+0800 小对象类型[27549:1517240] fjkdfjkdajfadkfjadlk;fj看对方金阿奎射流风机安抚巾 -- 0x10300d120 -- __NSCFConstantString
2021-11-10 18:56:25.870204+0800 小对象类型[27549:1517240] 123456789 -- 0xad2962987a031b99 -- NSTaggedPointerString
2021-11-10 18:56:25.870282+0800 小对象类型[27549:1517240] 123456789 -- 0xad2962987a031b99 -- NSTaggedPointerString
2021-11-10 18:56:25.870358+0800 小对象类型[27549:1517240] 1234567890 -- 0x600003a8a5c0 -- __NSCFString
2021-11-10 18:56:25.870420+0800 小对象类型[27549:1517240] 1234567890 -- 0x600003a8a580 -- __NSCFString

说明:

  • 根据输出结果可以得到字符串有三种存储方式,根据传入的方式不同,所提供的内存管理也不同
  • __NSCFConstantString
    • 表示是一个字符串常量,存储在常量区中
    • 它不会引起计数变化,因为它并不是一个对象
  • __NSCFString
    • 它是运行时创建的NSString子类,因此这种方式创建的是一个对象
    • 作为对象存储在堆中,引用计数会+1
  • NSTaggedPointerString
    • 小对象类型的字符串,也存储在常量区
    • 也不是对象,不会引起计数变化

字符串在什么条件下可以成为小对象类型?

  • 当字符串是由数字、英文字母组合且长度小于等于9时,会自动成为NSTaggedPointerString类型,存储在常量区。
  • 注意当有中文或者其他特殊符号时,会直接成为__NSCFString类型,存储在堆区。

总结:

  • 常量字符串存储在常量池中,不是一个对象,指针指向常量池中的地址
  • 对象字符串存储在堆中,是一个对象,指针指向堆中的地址。堆中的字符串指针指向常量区中的字符串
  • 小对象类型的字符串是一个小对象,既有对象的特征,又有常量的特征,它直接存储在常量池中。本身我们通过创建对象的方式来创建字符串,但是因为它是小字符串,所以系统会自动优化为存储在常量池的小字符串。

3、小对象类型的结构

3.1 查找小对象类型

在读取镜像文件的时候会加载小对象类型,在这里可以看到小对象类型是如何创建的。

通过注释可以看到是通过方法initializeTaggedPointerObfuscator进行对小对象类型初始化的。

initializeTaggedPointerObfuscator

* initializeTaggedPointerObfuscator
* Initialize objc_debug_taggedpointer_obfuscator with randomness.
*
* The tagged pointer obfuscator is intended to make it more difficult
* for an attacker to construct a particular object as a tagged pointer,
* in the presence of a buffer overflow or other write control over some
* memory. The obfuscator is XORed with the tagged pointers when setting
* or retrieving payload values. They are filled with randomness on first
* use.
 使用随机性初始化objc_debug_taggedpointer_obfuscator
 这种方式是为了使攻击者在存在缓存区移除或对某些内存进行其他写入控制的情况下,更难把特定对象构造为标记指针
 当设置或检索有效负载时,让模糊器与标记的指针进行异或,第一次使用时充满了随机性
static void
initializeTaggedPointerObfuscator(void)
{
    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;
    } else {
        // Pull random data into the variable, then shift away all non-payload bits.将随机数据拉入变量,然后移开所有非有效负载位
        //iOS14之后此处是进行了混淆,通过与操作数_OBJC_TAG_MASK进行异或来进行混淆,减少内存
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
    }
}

说明:

  • 为了防止被攻击,要进行混淆处理。
  • 通过异或来进行编码和解码

编解码(混淆)

extern uintptr_t objc_debug_taggedpointer_obfuscator;

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

说明:

  • 通过异或来进行编码和解码
  • 解码是再一次的异或,这样就可以得到原来的数据了
  • 指针会通过编码进行混淆,所以在取出时需要通过解码来得到原始数据

3.2 验证

代码:

extern uintptr_t objc_debug_taggedpointer_obfuscator;

//解码
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}

- (void)getTaggedPointer{
    NSString *str1 = [NSString stringWithFormat:@"a"];
    NSString *str2 = [NSString stringWithFormat:@"b"];
    NSLog(@"str1:%p-- %@ -- %@",str1,str1,str1.class);
    NSLog(@"str2:%p-- %@ -- %@",str2,str2,str1.class);
    
    NSLog(@"str1:0x%lx",_objc_decodeTaggedPointer(CFBridgingRetain(str1)));
    NSLog(@"str2:0x%lx",_objc_decodeTaggedPointer(CFBridgingRetain(str2)));
}

运行结果:

2021-11-10 20:19:07.646128+0800 小对象类型[29981:1567632] str10x985d7235397545c3-- a -- NSTaggedPointerString
2021-11-10 20:19:07.646268+0800 小对象类型[29981:1567632] str20x985d7235397545f3-- b -- NSTaggedPointerString
2021-11-10 20:19:07.646461+0800 小对象类型[29981:1567632] str10xa000000000000611
2021-11-10 20:19:07.646537+0800 小对象类型[29981:1567632] str20xa000000000000621

说明:

  • 通过手动解码就可以得到解码后的带数据指针
  • 解码方式就直接把源码拿出来就可以

3.3 指针的分析

上面我们拿到了带数据的指针,接下来就是看这个指针的构成。

NSNumber *number1 = @1;
NSNumber *number2 = @(-1); // 0xbffffffffffffff2
NSNumber *number3 = @2.0;
NSNumber *number4 = @3.2;
NSLog(@"%@-%p-%@ - 0x%lx",object_getClass(number1),number1,number1,_objc_decodeTaggedPointer_(number1));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number2)); // 0xb000000000000012
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number3));
NSLog(@"0x%lx",_objc_decodeTaggedPointer_(number4));

运行结果:

2021-11-10 20:31:11.136516+0800 002---taggedPointer[30344:1576016] __NSCFNumber-0x8734da3b082154d9-1 - 0xb000000000000012
2021-11-10 20:31:11.136585+0800 002---taggedPointer[30344:1576016] 0xbffffffffffffff2
2021-11-10 20:31:11.136648+0800 002---taggedPointer[30344:1576016] 0xb000000000000025
2021-11-10 20:31:11.136713+0800 002---taggedPointer[30344:1576016] 0x3734ba3b0a7dca8b

说明:

  • 小对象最高位的0xa和0xb用来判断是否是小对象类型
  • 数据存在于倒数第二位

举例:

//NSString
0xa000000000000621

//NSNumber
0xb000000000000012
0xb000000000000025

最高位.png

其他的tagType:

tagtype.png

3.4 总结

  • 小对象类型可以存储NSNumber、NSDate 、小NSString
  • Tagged Pointer存储在常量区,不存储在堆上,也不需要malloc和free,所以可以直接读取,相比存储在堆区的数据读取,效率上快了3倍左右。创建的效率相比堆区快了近100倍左右
  • 小对象类型的字符串不同于字符串常量,虽然都存储在常量池中,
  • 小对象类型是带有数据的指针,也就是地址+值
  • 小对象类型不会进行retain和release,也就是不需要ARC管理

4、验证是否会进行retain和release

通过objc_setProperty->reallySetProperty()进入,分别查看objc_retain()和objc_release()的具体实现,看是否对小对象类型进行特殊处理。

reallySetProperty的查看:

可以看到在设置属性时会对新值retain、旧值release

reallySetProperty.png

retain的查看:

__attribute__((aligned(16), flatten, noinline))
id 
objc_retain(id obj)
{
    if (!obj) return obj;
    if (obj->isTaggedPointer()) return obj;//此处可以看到如果是小对象类型是不会retain的
    return obj->retain();
}

说明: 当判断是小对象类型,就直接返回,而不去retain+1

release的查看:

__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;//此处可以看到如果是小对象类型是不会release的
    return obj->release();
}

说明: 当判断是小对象类型,就直接返回,而不去release+1

因此可以看到小对象类型并不会进行计数加减,也即不会执行retain和release。

注意:

  • 小对象类型在全局区
  • 小对象类型不是对象,是带有地址的字符串
  • 小对象类型不会调用retain/release,因为在常量区,直接释放回收,创建释放的效率都很高了。
  • 最好使用@""直接创建NSString,这样可以优化内存

总结:

  • Tagged Pointer小对象类型(用于存储NSNumber、NSDate、小NSString)
  • 小对象指针不再是简单的地址,而是地址 + 值,即真正的值,所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而以
  • 可以直接进行读取。优点是占用空间小 节省内存
  • 小对象并不是一个真正的对象,所以它的存储空间和创建与注销均与对象不一样,它的值存储在常量区中,不会进入retain 和 release,也不需要malloc和free
  • 可以直接读取,相比存储在堆区的数据读取,效率上快了3倍左右。创建的效率相比堆区快了近100倍左右

因此优化建议:

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

5、一个面试题简单分析

代码:

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

//*********代码2*********

- (void)test {
    self.queue = dispatch_queue_create("wy", DISPATCH_QUEUE_CONCURRENT);
    for (int i = 0; i<20000; i++) {
        dispatch_async(self.queue, ^{
            self.nameStr = [NSString stringWithFormat:@"wy的家福克斯的附近看到的数据开发机即可的房间奥施康定加法可的假发"];
            NSLog(@"%@",self.nameStr);
        });
    }
}

运行结果:

  • taggedPointerDemo中的nameStr是小对象类型,不存在retain和release,所以不会崩溃,正常执行
  • test中nameStr是NSString对象,当重新赋值时会进行retain和release,在异步并发队列会出现崩溃。

分析:

  • wy的字符串比较少,所以是小对象类型,下面的字符串比较大,所以是对象类型
  • taggedPointerDemo运行没有问题,因为它是小对象类型,是不会有release和retain的
  • 而test会崩溃,因为此时的字符串是一个对象,在方法中会进行retain和release,因为在异步并发线程中对同一个对象进行了多次释放,会造成过渡释放而崩溃