前情提要
关于在上一篇文章中最后我们做了一个拓展,在这里我们会将他进行展开,让小伙伴们更了解什么是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字节承载不了,则又用以前的方式来生成普通的指针,将对象存储在堆上。当然由于他的数据是直接存储在指针上上,所以不用也不需要malloc和free。
原理以及例子
可能单从文字上叙说,有一些无力,让我们从实际例子来说一下!
首先我们必须关闭数据混淆,为了数据安全,苹果对Tagged Pointer做了数据混淆。
在 Scheme--> Edit Scheme --> Run --> Argument --> Environment Variables 添加环境变量 OBJC_DISABLE_TAG_OBFUSCATION为YES。(如图)
在MacOS和 iOS 上所呈现的地址形式是不相同的。MacOS下采用 LSB(Least Significant Bit,即最低有效位)为Tagged Pointer标识位; iOS下则采用 MSB(Most Significant Bit,即最高有效位)为Tagged Pointer标识位。
我们这里直接以 大家最经常用的NSString说明一下吧。他的Tagged Pointer位视图如下:
通过对比我们发现,位视图差异性不大,只是相关标识位和类标识位不相同。因为平常我们主要做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]);
输出结果
从打印结果来看,有三种NSString类型:
| 类型 | 描述 |
|---|---|
| __NSCFConstantString | 1. 存储在常量区,常量字符串,继承于 __NSCFString。相同内容的对象的地址相同,可通过 == 判断字符串内容是否相同。 2. 这种对象一般通过字面量@"..."进行创建。 |
| __NSCFString | 1. 存储在堆区,需维护其引用计数,继承于 NSMutableString。 2. 通过stringWithFormat:等方法创建的NSString对象(字符串如果过大无法使用Tagged Pointer存储时候,我们上面说过)。 |
| NSTaggedPointerString | Tagged Pointer,值直接存储在指针上。 |
打印结果分析:
| NSString 对象 | 类型 | 分析 |
|---|---|---|
| a | __NSCFConstantString | 字面量创建 |
| b | __NSCFString | 将a拷贝到堆区,指向堆区的一个地址 |
| c | __NSCFConstantString | 对不可变字符串 a 进行 copy 操作,不会创建新的对象,而是返回一个指向相同内容的 NSString 对象。因为 a 是不可变的,所以 copy 返回的 c 仍然是同一个对象。 |
| d | NSTaggedPointerString | [a mutableCopy] 创建一个 NSMutableString 对象。对 NSMutableString 再进行 copy 操作,返回一个不可变字符串,因为是短字符串转为 NSTaggedPointerString |
| e | __NSCFConstantString | 这种方式通常会返回一个与 a 相同的字符串对象,如果 a 是不可变字符串,则 stringWithString: 不会创建新的对象。 |
| f | NSTaggedPointerString | 通过stringWithFormat:方法创建,指针够存储字符串的值。 |
| string1 | NSTaggedPointerString | 通过stringWithFormat:方法创建,指针够存储字符串的值。 |
| string2 | NSTaggedPointerString | 通过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 为例,把上面那张图移动下来结合着,说明一下。
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 = 11 AND 0 = 00 AND 1 = 00 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 = 11 OR 0 = 10 OR 1 = 10 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 << nx是左操作数,它的二进制表示将被移动。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解释分析 我们这里也可以拓展一下,其实也很简单。
我们举例一下
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]);
输出结果
从上面输出内容可以知道常量值地址都是一样的,例如
numer2 和 numer3。
虽然numer1 和 numer5输出的类为 __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 倒数第一位 | 对应数据类型 |
|---|---|
| 0 | char |
| 1 | short |
| 2 | int |
| 3 | long |
| 4 | float |
| 5 | double |
numer6由于超限了变成了堆上实例。