runtime 由浅入深--Tagged Pointer 小解

448 阅读9分钟

前情提要

关于在上一篇文章中最后我们做了一个拓展,在这里我们会将他进行展开,让小伙伴们更了解什么是Tagged Pointer ,在考虑是否把它加到 runtime 由浅入深 里,确实犯了一些小纠结,他应该属于内存管理 相关,但是为了便于读完上一篇文章以后,能够马上热乎了解拓展,这里行了一个方便。(其实是我怕自己画了饼给忘记了🙇)

前言

在iOS中我们知道 Pointer 表示指针,而 Tagged在这边表示被标记的,也就是说他虽然是一个指针但是他被标记了一些东西,让他在指针的身份下,多了一重能力。这重能力我们在上一章节的扩展中我们也说了:

在存储一些简单的数据时,他们本身值大的小范围需要占用的内存大小常常不需要8个字节,对于绝大多数情况都是可以处理的。 为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念。对于64位程序,引入Tagged Pointer后,将值的信息直接存储到了指针本身里面

Tagged Pointer

在探索 Tagged Pointer以前,我们一定听说过,当我们创建了一个对象,也就是在 上申请了一块内存,指针存储了这块区域的地址(在Tagged Pointer之前可以认为是对的)。 随着手机的设备性能不断提高,iOS的寻址空间也扩大到了64位,用63位表示数字的大小,在平常的开发中远远够了,当然苹果也意识到了这个问题,于是 Tagged Pointer应运而生。

Tagged Pointer是一个特殊的指针,他保存的不再是地址,而是实的数据和一些附加的信息。通常他可以用来保存一些小的数据 比如 NSString,NSNumber, NSDate。注意!!,当8个字节可以承载的数值时,系统就会以Tagged Pointer的方式生成指针,如果8字节承载不了,则又用以前的方式来生成普通的指针,将对象存储在堆上。当然由于他的数据是直接存储在指针上上,所以不用也不需要mallocfree

原理以及例子

可能单从文字上叙说,有一些无力,让我们从实际例子来说一下! 首先我们必须关闭数据混淆,为了数据安全,苹果对Tagged Pointer做了数据混淆。 在 Scheme--> Edit Scheme --> Run --> Argument --> Environment Variables 添加环境变量 OBJC_DISABLE_TAG_OBFUSCATIONYES。(如图)

image.png

MacOSiOS 上所呈现的地址形式是不相同的。MacOS下采用 LSB(Least Significant Bit,即最低有效位)为Tagged Pointer标识位; iOS下则采用 MSB(Most Significant Bit,即最高有效位)为Tagged Pointer标识位。

我们这里直接以 大家最经常用的NSString说明一下吧。他的Tagged Pointer位视图如下:

image.png

image.png

通过对比我们发现,位视图差异性不大,只是相关标识位类标识位不相同。因为平常我们主要做iOS开发,这里我们仅以iOS举例说明,但是注意他们的道理是一样的.(还是以NSString为例)


    NSString *a = @"a";

    NSMutableString *b = [a mutableCopy];

    NSString *c = [a copy];

    NSString *d = [[a mutableCopy] copy];

    NSString *e = [NSString stringWithString:a];

    NSString *f = [NSString stringWithFormat:@"f"];

    NSString *string1 = [NSString stringWithFormat:@"abcdefg"];

    NSString *string2 = [NSString stringWithFormat:@"abcdefghi"];

    NSString *string3 = [NSString stringWithFormat:@"abcdefghij"];
    NSLog(@"a === %p 类型为%@ ",a,[a class]);

    NSLog(@"b === %p 类型为%@",b,[b class]);

    NSLog(@"c === %p 类型为%@",c,[c class]);

    NSLog(@"d === %p 类型为%@",d,[d class]);

    NSLog(@"e === %p 类型为%@",e,[e class]);

    NSLog(@"f === %p 类型为%@",f,[f class]);

    NSLog(@"string1 === %p 类型为%@",string1,[string1 class]);

    NSLog(@"string2 === %p 类型为%@",string2,[string2 class]);

    NSLog(@"string3 === %p 类型为%@",string3,[string3 class]);

输出结果

image.png

从打印结果来看,有三种NSString类型:

类型描述
__NSCFConstantString1. 存储在常量区,常量字符串,继承于 __NSCFString。相同内容的对象的地址相同,可通过 == 判断字符串内容是否相同。 2. 这种对象一般通过字面量@"..."进行创建。
__NSCFString1. 存储在堆区,需维护其引用计数,继承于 NSMutableString。 2. 通过stringWithFormat:等方法创建的NSString对象(字符串如果过大无法使用Tagged Pointer存储时候,我们上面说过)。
NSTaggedPointerStringTagged Pointer,值直接存储在指针上。

打印结果分析:

NSString 对象类型分析
a__NSCFConstantString字面量创建
b__NSCFStringa拷贝到堆区,指向堆区的一个地址
c__NSCFConstantString对不可变字符串 a 进行 copy 操作,不会创建新的对象,而是返回一个指向相同内容的 NSString 对象。因为 a 是不可变的,所以 copy 返回的 c 仍然是同一个对象。
dNSTaggedPointerString[a mutableCopy] 创建一个 NSMutableString 对象。对 NSMutableString 再进行 copy 操作,返回一个不可变字符串,因为是短字符串转为 NSTaggedPointerString
e__NSCFConstantString这种方式通常会返回一个与 a 相同的字符串对象,如果 a 是不可变字符串,则 stringWithString: 不会创建新的对象。
fNSTaggedPointerString通过stringWithFormat:方法创建,指针够存储字符串的值。
string1NSTaggedPointerString通过stringWithFormat:方法创建,指针够存储字符串的值。
string2NSTaggedPointerString通过stringWithFormat:方法创建,指针够存储字符串的值。
string3__NSCFString通过stringWithFormat:方法创建,指针不够存储字符串的值。
objc4源码可查出各个类的标识位
// objc-internal.h
{
   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,
   // ......
}

我们综合上面的说法,以 d === 0xa000000000000611 为例,把上面那张图移动下来结合着,说明一下。 image.png

0x 表示16进制
a 先换算成十进制为10,再换算成二进制(16进制每一位表示四位二进制)1010第一位 1表示Tagged Pointer标识位,010换算成十进制为 2 按照 objc-internal.h源码得出为 OBJC_TAG_NSString
611 最后以为表示字符串长度,也就是说长度为1。61对应字符串的ASCII

如何判断 Tagged Pointer

其实我们上面已经说了 就是按照标识位进行判断。实现在代码上也就是

 // objc-object.h
 **static** **inline** **bool******

_objc_isTaggedPointer(**const** **void** * **_Nullable** ptr)

{

    **return** ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;

}

看源码的意思就是和掩码 _OBJC_TAG_MASK 进行 按位与运算

按位与运算

每个位按照如下逻辑规则执行与运算:

  • 1 AND 1 = 1
  • 1 AND 0 = 0
  • 0 AND 1 = 0
  • 0 AND 0 = 0

换句话说,只有当输入的两个位都为 1 时,结果位才为 1,否则结果位为 0

    unsigned char a = 0xAC;  // 二进制表示: 10101100
    unsigned char b = 0x56;  // 二进制表示: 01010110

    unsigned char result = a | b;

得到结果为

a = 10101100 
b = 01010110 
----------- 
a & b = 00000100
按位或运算

每个位按照如下逻辑规则执行或运算:

  • 1 OR 1 = 1
  • 1 OR 0 = 1
  • 0 OR 1 = 1
  • 0 OR 0 = 0

换句话说,只要输入的两个位中有一个为 1,结果位就为 1,只有当两个输入位都为 0 时,结果位才为 0

unsigned char a = 0xAC;  // 二进制表示: 10101100
unsigned char b = 0x56;  // 二进制表示: 01010110

unsigned char result = a | b;

得到结果为

  a = 10101100
  b = 01010110
-----------
a | b = 11111110
左移运算
  • 语法x << n

    • x 是左操作数,它的二进制表示将被移动。
    • n 是右操作数,表示要将 x 左移的位数。
  • 左移运算的效果

    • 对每一位进行左移:x 的每一个位向左移动 n 位。
    • 右侧填零:新生成的位从右侧补充 n 个零。
    • 超出部分丢弃:如果存在超出操作数表示范围的高位,结果通常不会再保留这些高位。
1UL << 63

1UL:表示无符号长整型值 1,即在 64 位机器上,它的二进制形式是 0000000000000000000000000000000000000000000000000000000000000001
左移 63 位:将这个值的所有位向左移动 63 位,结果就是:

   1 (0000000000000000000000000000000000000000000000000000000000000001)
   <<
   63
   =
   1000000000000000000000000000000000000000000000000000000000000000

新生成的最高有效位(第64位)为 1,其余全部为 0

结论

按照上面判断方法结合关于运算的理解,我们对于判断是Tagged Pointer有了更深层的了解。

  • MacOS下采用 LSB(Least Significant Bit,即最低有效位)为Tagged Pointer标识位;
  • iOS下则采用 MSB(Most Significant Bit,即最高有效位)为Tagged Pointer标识位。

而存储在堆空间的对象由于内存对齐,它的内存地址的最低有效位为 0。由此可以辨别Tagged Pointer和一般对象指针。

附赠内容

我们在看别的文章里面可能经常看到举例都是通过NSNumber解释分析 我们这里也可以拓展一下,其实也很简单。

image.png

image.png 我们举例一下


    NSNumber *numer1 = [NSNumber numberWithInt:1];

    NSNumber *numer2 = @1;

    NSNumber *numer3 = @1;

    NSNumber *numer4 = @2;

    NSNumber *numer5 = [NSNumber numberWithInt:1];

    NSNumber *numer6 = [[NSNumber alloc]initWithUnsignedLong:0xFFFFFFFFFFFFFFFF];

    

    NSLog(@"numer1 === %p 类型为%@ ",numer1,[numer1 class]);

    NSLog(@"numer2 === %p 类型为%@",numer2,[numer2 class]);

    NSLog(@"numer3 === %p 类型为%@",numer3,[numer3 class]);

    NSLog(@"numer4 === %p 类型为%@",numer4,[numer4 class]);

    NSLog(@"numer5 === %p 类型为%@",numer5,[numer5 class]);

    NSLog(@"numer6 === %p 类型为%@",numer6,[numer6 class]);

输出结果

image.png 从上面输出内容可以知道常量值地址都是一样的,例如 numer2numer3

虽然numer1numer5输出的类为 __NSCFNumber 但是通过上面_objc_isTaggedPointer 方法,我们可以得知他们为 Tagged Pointer类型。

b 先换算成十进制为11,再换算成二进制(16进制每一位表示四位二进制)1011第一位 1表示Tagged Pointer标识位,011换算成十进制为 3 按照 objc-internal.h源码得出为 OBJC_TAG_NSNumber
最后一位的2表示数据类型。具体表示如下表。

Tagged Pointer倒数第二位对应数据类型:

Tagged Pointer 倒数第一位对应数据类型
0char
1short
2int
3long
4float
5double

numer6由于超限了变成了堆上实例。