准备
- objc4-818.2 源码
- WWDC 2020 视频
一、什么是 tagged pointer
tagged pointer 引入
自2013
年苹果推出iphone5s
之后,iOS
的寻址空间扩大到了64
位。我们可以用63
位来表示一个数字(一位做符号位)。那么这个数字的范围是2^63
,很明显我们一般不会用到这么大的数字,那么在我们定义一个数字时NSNumber *num = @10
,实际上内存中浪费了很多的内存空间。
为了节省内存和提高执行效率,于是就引入了tagged pointer
,tagged pointer
是一种特殊的”指针“,其特殊在于,其实它存储的并不是地址,而是真实的数据和一些附加的信息。
tagged pointer 特点
我们可以在 WWDC 2020 的视频中,看到苹果对于tagged pointer
特点的介绍,tagged pointer
部分从14:52
左右开始。
在[WWDC 2013]
的视频中有这样的一段介绍:
tagged pointer
专门用来存储小的对象,例如NSNumber
,NSDate
,NSString
。tagged pointer
指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc
和free
。- 在内存读取上有着
3
倍的效率,创建时比以前快106
倍。
二、tagged pointer 的数据混淆
先看下下面代码的执行:
- (void)test {
NSString *str = [NSString stringWithFormat:@"L"];
NSLog(@"%p - %@ - %@",str,str,str.class);
}
执行结果:
0xd2cb3e3937e0be52 - L - NSTaggedPointerString
- 字符串的地址是
0xd2cb3e3937e0be52
,通过NSTaggedPointerString
我们可以知道他是一个tagged pointer
,这个地址看上去就很奇怪并不是我们平时看到的样子,这是因为它做了数据混淆。
数据混淆 原理
打开 objc4-818.2 源码,可以找到下面的代码:
static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
uintptr_t value = _objc_decodeTaggedPointer_noPermute(ptr);
...
return value;
}
static inline uintptr_t
_objc_decodeTaggedPointer_noPermute(const void * _Nullable ptr)
{
uintptr_t value = (uintptr_t)ptr;
...
// 混淆
return value ^ objc_debug_taggedpointer_obfuscator;
}
- 可以看到混淆算法,是
地址
和objc_debug_taggedpointer_obfuscator
进行异或操作。
查找objc_debug_taggedpointer_obfuscator
初始化的地方:
/***********************************************************************
* initializeTaggedPointerObfuscator
* Initialize objc_debug_taggedpointer_obfuscator with randomness.
**********************************************************************/
static void
initializeTaggedPointerObfuscator(void)
{
// 是否开启混淆
if (!DisableTaggedPointerObfuscation) {
// 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;
...
} else {
objc_debug_taggedpointer_obfuscator = 0;
}
}
initializeTaggedPointerObfuscator()
这个初始化函数,是在_read_images
中调用的。- 是否开启混淆通过
DisableTaggedPointerObfuscation
来控制,如果开启了混淆会给objc_debug_taggedpointer_obfuscator
赋值一个随机数。 - 如果没有开启混淆会将
objc_debug_taggedpointer_obfuscator
赋值为0
,下面将通过两种方式来解决数据混淆。
解密函数 解决数据混淆
extern uintptr_t objc_debug_taggedpointer_obfuscator;
- (void)test {
NSString *str = [NSString stringWithFormat:@"L"];
NSLog(@"%p - %@ - %@ - 0x%lx",str,str,str.class,ssl_objc_decodeTaggedPointer(str));
}
uintptr_t
ssl_objc_decodeTaggedPointer(id ptr)
{
// 再次异或 解密
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
执行结果:
0xb9baff764fa37ce8 - L - NSTaggedPointerString - 0xa0000000000004c1
- 通过解密函数得到地址
0xa0000000000004c1
,是我们想要的结果。
环境配置 关闭数据混淆
通过设置环境变量OBJC_DISABLE_TAG_OBFUSCATION
为YES
,关闭数据混淆:
再次执行程序:
0xa0000000000004c1 - L - NSTaggedPointerString - 0xa0000000000004c1
- 现在直接打印
str
的地址和解密后的地址是一样的,都是0xa0000000000004c1
。
三、x86-64下的tagged pointer结构
创建一个MacOS
的工程,执行下面代码:
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"a"];
NSNumber *num1 = @3;
NSNumber *num2 = @(0xFFFFFFFFFFFFFFFF);
NSLog(@"%p- %p - %p",str,num1,num2);
}
return 0;
}
执行结果:
0x6115- 0x327 - 0x100529c70
- 从执行结果可以看出,
str
和num1
是tagged pointer
类型,值存储在了指针中,对应0x6115
和0x327
。num2
并不是tagged pointer
类型。 0x6115
的二进制表示为110000100010101
。0x327
的二进制表示为1100100111
。
3.1 MacOS下 tagged pointer标志位
下面的源码是判断是否为tagged pointer
类型:
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
#endif
这里用到了_OBJC_TAG_MASK
掩码,来看下它的定义:
#if __arm64__
# define OBJC_SPLIT_TAGGED_POINTERS 1
#else
# define OBJC_SPLIT_TAGGED_POINTERS 0
#endif
#if OBJC_SPLIT_TAGGED_POINTERS // arm64时为 1
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
- 在
x86-64
环境下_OBJC_TAG_MASK
的值为1
,ptr & 1
还是等于1
,也就是说指针地址最低位是1
的时候,代表这个指针是tagged pointer
类型。 - 前面的
num1
和str
二进制表示最低位都为1
,是tagged pointer
类型在这里得到验证。
3.2 MacOS下 类标志位
在objc4
源码中查看NSNumber
、NSString
等类的标志位:
// objc-internal.h
enum
{
// 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,
...
};
- 前面的
num1
二进制是1100100111
,第1-3
位是类标志位,值是011
,十进制是3
是NSNumber
类型,跟枚举值匹配。 - 前面的
str
二进制是110000100010101
,1-3
位是类标志位,值是010
,十进制是2
是NSString
类型,跟枚举值匹配。
3.3 MacOS下 数据类型&字符串长度
如果是NSNumber
类型,tagged pointer
的4-7
位对应数据类型,对应关系如下:
Tagged Pointer 4-7位 | 对应数据类型 |
---|---|
0 | char |
1 | short |
2 | int |
3 | long |
4 | float |
5 | double |
- 前面的
num1
二进制是1100100111
,4-7
对应的是0010
,0010
的十进制是2
,对应这里的int
类型。
如果是NSString
类型,tagged pointer
的4-7
位对应字符串长度
- 前面的
str
二进制是110000100010101
,4-7
位对应的是0001
,0001
的十进制是1
,代表字符串长度为1
。
3.4 MacOS下 数据存储
tagged pointer
的8-63
位用来存储数据:
- 前面的
num1
二进制是1100100111
,8-63
对应的是11
,11
的十进制是3
,符合这里的说法。 - 前面的
str
二进制是110000100010101
,8-63
对应的是1100001
,1100001
的十进制是97
,符合这里的说法,ASCII
码表:
3.5 MacOS下 tagged pointer结构图
四、arm64下的tagged pointer结构
在真机环境下,执行下面代码:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = [NSString stringWithFormat:@"a"];
NSNumber *num1 = @3;
NSLog(@"%p- %p - %p",str,num1,num2);
}
执行结果:
0x800000000000308a - 0x8000000000000193
str
地址0x800000000000308a
的二进制表示为:1000000000000000000000000000000000000000000000000011000010001010
num1
地址0x8000000000000193
的二进制表示为:1000000000000000000000000000000000000000000000000000000110010011
4.1 真机下 tagged pointer标志位
是否为tagged pointer
源码查看:
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
#if OBJC_SPLIT_TAGGED_POINTERS // arm64时
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
#endif
- 在
arm64
环境下_OBJC_TAG_MASK
的值为(1UL<<63)
,ptr & (1UL<<63)
还是等于(1UL<<63)
,也就是说指针地址第63
位是1
的时候,代表这个指针是tagged pointer
类型。 - 通过上面的打印结果,可以看到第
63
位确实为1
。
4.2 真机下 类标志位
- 通过上面的打印结果,可以看到真机下的
类标志位
放在了地址的前三位,即0-2
位。 str
的前三位是010
,十进制表示是1
,代表NSString
类。num1
的前三位是011
,十进制表示是3
,代表NSNumber
类。
4.3 真机下 数据类型&字符串长度
- 通过上面的打印结果,可以看到真机下的
数据类型&字符串长度
放在了地址的3-6
位。 str
的3-6
位是0001
,十进制是1
,代表字符串长度为1
。num1
的3-6
位是0010
,十进制是2
,代表存储的是int
类型。
4.4 真机下 数据存储
tagged pointer
的7
到62
位用来存储数据:
str
的7-62
位是1100001
,十进制是97
,也就是字符a
。num1
的7-62
位是11
,十进制是3
,就是int
类型的3
。
4.5 真机下 tagged pointer结构图
五、taggedPointer 面试题
1)执行下面两段代码,有什么区别?
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghij"];
});
}
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; i++) {
dispatch_async(queue, ^{
self.name = [NSString stringWithFormat:@"abcdefghi"];
});
}
两段代码基本就是一样的,区别只是第二段代码的字符串少了一位而已,这样运行能有什么区别吗?
运行代码,第二段代码正常运行没有问题,第一段代码竟然Crash
了。
分别打印两段代码的self.name
类型,第一段代码中self.name
是__NSCFString
类型,而第二段代码中为NSTaggedPointerString
类型。
来看一下第一段代码Crash
的地方:
- 看到这里我们就明白了,
__NSCFString
存储在堆上,它是个正常对象,需要维护引用计数的。self.name
通过setter
方法为其赋值,而setter
方法的实现如下:- (void)setName:(NSString *)name { if(_name != name) { [_name release]; _name = [name retain]; // or [name copy] } }
- 我们异步并发执行
setter
方法,可能就会有多条线程同时执行[_name release]
,连续release
两次就会造成对象的过度释放,导致Crash
。 - 我们可以使用加锁操作解决这个问题,使用
atomic
属性也能解决这个问题。
第二段代码中的NSString
为NSTaggedPointerString
类型,在objc_release
函数中会判断指针是不是TaggedPointer
类型,是的话就不对对象进行release
操作,也就避免了因过度释放对象而导致的Crash
,因为根本就没执行释放操作。
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (obj->isTaggedPointerOrNil()) return;
return obj->release();
}