简介:
在objc4源码中,我们经常会在函数中看到Tagged Pointer
;Tagged Pointer
究竟是何方神圣?有什么作用呢?本篇文章用于记录我对Tagged Pointer
的理解。
Tagged Pointer主要为了解决两个问题:
-
内存资源浪费,堆区需要额外的开辟空间:
例如NSNumber
对象,其值是一个整数。正常情况下,如果这个整数只是一个NSInteger
的普通变量,那么它所占用的内存是与CPU
的位数有关,在32位CPU
下占4个字节,在64位CPU
下是占8个字节的。而指针类型的大小通常也是与CPU
位数相关,一个指针所占用的内存在32位CPU
下为 4 个字节,在64位CPU
下也是8个字节。
所以一个普通的 iOS 程序,如果没有Tagged Pointer
对象,从 32 位机器迁移到64位机器中后,虽然逻辑没有任何变化,但这种NSNumber
、NSDate
一类的对象所占用的内存会翻倍。 -
访问效率,每次set/get都需要访问堆区,浪费时间,而且需要管理堆区对象的声明周期,降低效率:
为了改进上述提到的内存占用和效率问题,所以苹果提出了Tagged Pointer
对象。对于某些占用内存很小的数据对象,不再单独开辟空间去存储,而是将实际的实例值存储在对象的指针中,同时对该指针进行标记,用于区分正常的指针指向!
由于NSNumber
、NSDate
类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4个字节所能表示的有符号整数就可以达到20多亿(注:2^31=2147483648,另外1位作为符号位),对于绝大多数情况都是可以处理的。所以苹果将一个对象的指针拆成两部分,一部分直接保存数据(即下面所说的Data部分),另一部分作为特殊标记(即下面所说的Tag部分),表示这是一个特别的指针,不指向任何一个对象的地址。
Tagged Pointer
内存管理:
普通类内存管理:
总空间 = 栈指针空间(isa普通类指针) + 堆中分配的空间
指针变量指向堆分配的内存空间,需要动态分配内存,进行引用计数机制,控制对象堆内存的管理。
Tagged Pointe内存管理:
总空间 = 栈指针空间(Tagged Pointer)
栈指针空间 = 对象的特殊标记(Tag) + 对象的值(Data)
对象的特殊标记(Tag),总共占有四个二进制位;在ARM64架构中最高位的二进制四位用于区分Tagged Pointer
和普通指针的区别,其它三位用于区分NSNumber
、NSDate
、NSString
等对象类型
Data为对象的值
指针变量包含值,不用进行引用计数机制对内存管理,所以不需要retain,release操作。
Tagged Pointer
总结:
-
Tagged Pointer
指向的并不是一个类,它是适用于64位处理器的一个内存优化机制,专门用来存储小对象,当存储不下时,则转为对象,例如NSNumber
,NSDate
,NSString
; - 指针的值不再是地址了,而是包含真正值且经过特殊处理过的指针。所以,实际上它不再是一个对象了,它只是一个披着对象外衣的普通变量而已。它的内存并不存储在堆中,不需要动态分配内存、维护引用计数、管理它的生命周期等
也不需要方法调用时执行objc_msgSend流程(消息发送、动态方法解析、消息转发); - 在内存读取上有着3倍的效率,创建时比以前快106倍;
-
Tagged Pointer
指针指向的不在是一个类,所以无法直接访问isa指针。
Tagged Pointer
原理和底层实现:
1.Xcode
设置环境变量
Xcode
默认情况下对Tagged Pointer
进行了混淆;设置环境变量OBJC_DISABLE_TAG_OBFUSCATION
为YES
, 可以关闭 Tagged Pointer
的数据混淆。
设置环境变量的步骤:
Edit Scheme -> Run Debug -> Arguments -> Environment Variables -> + -> OBJC_DISABLE_TAG_OBFUSCATION -> YES
代码如下:
- (void)taggedPointerNumber {
NSNumber *number1 = @(0x1);
NSNumber *number2 = @(0x20);
NSNumber *number3 = @(0x3F);
NSNumber *number4 = @(0xFFFFFFFFFFEFE);
NSNumber *maxNum = @(MAXFLOAT);
// 使用了Tagged Pointer,NSNumber对象的值直接存储在了指针上,不会在堆上申请内存。则使用一个NSNumber对象只需要指针的 8 个字节内存就够了,大大的节省了内存占用。
// NSLog(@"%zd", malloc_size(number1);
NSInteger number1Size = malloc_size((__bridge const void *)(number1));
NSLog(@"占用堆内存=%zd", number1Size);
NSLog(@"指针内存=%zd", sizeof(number1));
// NSNumber普通对象,会在堆上申请内存
NSInteger maxNumSize = malloc_size((__bridge const void *)(maxNum));
NSLog(@"占用堆内存=%zd", maxNumSize);
NSLog(@"指针内存=%zd", sizeof(maxNum));
NSLog(@"%p %@ %@", number1, number1, number1.class);
NSLog(@"%p %@ %@", number2, number2, number2.class);
NSLog(@"%p %@ %@", number3, number3, number3.class);
NSLog(@"%p %@ %@", number4, number4, number4.class);
NSLog(@"%p %@ %@", maxNum, maxNum, maxNum.class);
}
/// ARM64开启混淆打印:
占用堆内存=0
指针内存=8
占用堆内存=32
指针内存=8
0x8b33564c2d3526b5 1 __NSCFNumber
0x8b33564c2d3524a5 32 __NSCFNumber
0x8b33564c2d352555 63 __NSCFNumber
0x8bcca9b3d2cac944 4503599627370238 __NSCFNumber
0x282dc8760 3.402823e+38 __NSCFNumber
/// ARM64关闭混淆打印:
占用堆内存=0
指针内存=8
占用堆内存=32
指针内存=8
0xb000000000000012 1 __NSCFNumber
0xb000000000000202 32 __NSCFNumber
0xb0000000000003f2 63 __NSCFNumber
0xb0ffffffffffefe3 4503599627370238 __NSCFNumber
0x281a2afe0 3.402823e+38 __NSCFNumber
1.内存
number1
只有栈上的指针内存;而maxNum
不仅有指针内存,在堆中还分配了32字节的内存用于存储该变量的值。
2.指针
变量 | 指针值 | 10进制数值 |
---|---|---|
number1 | 0xb000000000000012 | 1 |
number2 | 0xb000000000000202 | 32 |
number3 | 0xb0000000000003f2 | 63 |
number4 | 0xb0ffffffffffefe3 | 4503599627370238 |
maxNum | 0x281a2afe0 | 3.402823e+38 |
通过观察发现,对象的number1
、number2
、number3
、number4
都存储在了对应的指针中;而maxNum
不同由于数据过大,导致无法 1 个指针 8 个字节的内存根本存不下,而申请了32字节堆内存。
3.Tagged Pointer
和isa
打断点,从上图可以看出,
number1
、number2
、number3
、number4
的isa指向了0x0(即nil),是Tagged Pointer
指针;maxNum
指向了NSNumber
类的isa
指针。4.
Tagged Pointer
位解析以
number1
的Tagged Pointer
指针为例:
高位 <-- 低位
0xb000000000000012
4.1最高位解析:
0x
为16进制标识符,在16进制中,一位数字代表二进制中的四位;
在ARM64
架构下,Tagged Pointer
的标识为二进制的最高位的四位,也就是16进制表示的从左向右的第一位:
/// 16进制的第一位,也是最高位
b
/// 16进制的b转换为二进制的四位,也是指针二进制最高四位
1011
其中二进制中最高位是Tagged Pointer
标识位,如例子中的的1
,表示该指针是Tagged Pointer
的指针;其它三位表示支持Tagged Pointer
的类标识位,如例子中011
,转化为10进制就是3,3在支持Tagged Pointer
的系统类数组中代表NSNumber
类。
runtime
源码objc-internal.h
中有关支持Tagged Pointer
类的标志定义如下:
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,
4.2末尾解析:
Tagged Pointer
16进制的末尾也就是二进制的低位的四位,表示的Tagged Pointer
存储数据的类型标识符,例如number1
末尾的2表示Tagged Pointer
存储的是int
的数据。
数据类型 | 标识符 |
---|---|
char | 0 |
short | 1 |
int | 2 |
long int | 3 |
float | 4 |
double | 5 |
除去高位和低位的标识位,中间这一部分才是真正存储值的区域。
注意:
-
NSString
类型的Tagged Pointer
指针与基本类型的指针是不一样的,末尾的数字为字符串的长度; -
NSString
类型的Tagged Pointer
指针存储char
类型,返回的是ASCII码(该值为16进制的,需要进行十进制转换)
- (void)taggedPointerString {
NSMutableString *mutableStr = [NSMutableString string];
NSString *immutable = nil;
#define _OBJC_TAG_MASK (1UL<<63)
char c = 'a';
do {
[mutableStr appendFormat:@"%c", c++];
immutable = [mutableStr copy];
NSLog(@"%p %@ %@", immutable, immutable, immutable.class);
}while(((uintptr_t)immutable & _OBJC_TAG_MASK) == _OBJC_TAG_MASK);
}
打印信息:
0xa000000000000611 a NSTaggedPointerString
0xa000000000062612 ab NSTaggedPointerString
0xa000000006362613 abc NSTaggedPointerString
0xa000000646362614 abcd NSTaggedPointerString
0xa000065646362615 abcde NSTaggedPointerString
0xa006665646362616 abcdef NSTaggedPointerString
0xa676665646362617 abcdefg NSTaggedPointerString
0xa0022038a0116958 abcdefgh NSTaggedPointerString
0xa0880e28045a5419 abcdefghi NSTaggedPointerString
0x280e3cb40 abcdefghij __NSCFString
Tagged Pointer
标识位、类标识、数据类型做代码验证
1.Tagged Pointer
标识位
Tagged Pointer
标识位即判断是否是Tagged Pointer
指针的表示方法。该篇文章源码版本为objc4-818.2。
在源码objc_internal.h
中可以找到判断Tagged Pointer
标识位的方法,如下代码:
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
上面的代码将指针ptr
和_OBJC_TAG_MASK
掩码进行位与操作。这个掩码_OBJC_TAG_MASK
的源码同样在objc_internal.h中可以找到:
#if (TARGET_OS_OSX || TARGET_OS_MACCATALYST) && __x86_64__
// 64-bit Mac - tag bit is LSB
# define OBJC_MSB_TAGGED_POINTERS 0
#else
// Everything else - tag bit is MSB
# define OBJC_MSB_TAGGED_POINTERS 1
#endif
#if OBJC_SPLIT_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#elif OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
根据源码得知:
MacOS
(x86_64和ARM64 M芯片)下采用 LSB(Least Significant Bit,即最低有效位)为Tagged Pointer标识位;(define _OBJC_TAG_MASK 1UL)
iOS
(ARM64 A芯片)下则采用 MSB(Most Significant Bit,即最高有效位)为Tagged Pointer标识位。(define _OBJC_TAG_MASK (1UL<<63))
2. Tagged Pointer
类标识
在源码objc_internal.h中可以查看到NSNumber、NSDate、NSString等类的标识位,如下:
// 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,
3. Tagged Pointer
数据类型
Tagged Pointer
16进制的最后一位(即2进制的最后四位)表示数据类型,例子如上述代码。见4.2末尾解析。
网友评论