前言:在之前分析
isa
结构,还有引用计数的管理时,会遇到isTaggedPointer
的判断,这里就来学习一下Tagged Pointer
到底是个啥?
1. Tagged Pointer介绍
1.1 为什么要使用 Tagged Pointer
在 2013
年,苹果推出了iPhone 5s
,配备了首个采用64 位架构
的 A7 双核处理器
,指针变量的内存占用由32 位(4字节)
变成了 64 位(8字节)
。OC 对象
都是存储在堆
上面的,并且在栈上有一个指针指向了堆的内存地址,对于一些变量,比如@(1)
,@"abc"
,如果同时在栈和堆上都取分配内存空间的话,就很浪费了,另外还要维护它的引用计数,管理它的生命周期,这些都给程序增加了额外的逻辑,影响运行效率。
为了改进上面上面所提出的问题,苹果提出了Tagged Pointer
对象。由于 NSNumber、NSDate、NSString
一类的变量本身的值占用的内存大小多数情况下不需要 8 个字节,就比如整数,4 个字节所能表示的有符号整数就可以达到2147483648,对于绝大多数情况都是可以处理的。
1.2 Tagged Pointer到底是是什么?
所以苹果将一个对象的指针拆成了两部分:本身数据 + 特殊标记
,表示这是一个特别的指针,不指向任何一个地址,这个特别的指针就是 Tagged Pointer
,如下图所示:
由于Tagged Pointer
不指向任何一个地址,所以在对象进行retain
或者release
操作时,都会判断isTaggedPointer
,如果为true
,则不进行操作,也就是说我们并需要去管理它的生命周期,如下:
__attribute__((aligned(16), flatten, noinline))
id
objc_retain(id obj)
{
if (!obj) return obj;
if (obj->isTaggedPointer()) return obj;
return obj->retain();
}
__attribute__((aligned(16), flatten, noinline))
void
objc_release(id obj)
{
if (!obj) return;
if (obj->isTaggedPointer()) return;
return obj->release();
}
2. 判断是否为Tagged Pointer
我们运行下面的代码,去看下 Tagged Pointer
指针地址的含义:
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
NSNumber *number1 = @1;
NSLog(@"number1 %p %@", number1, [number1 class]);
NSNumber *number2 = @2;
NSLog(@"number2 %p %@", number2, [number2 class]);
NSString *a = [[@"a" mutableCopy] copy];
NSLog(@"a %p %@", a, [a class]);
NSString *ab = [[@"ab" mutableCopy] copy];
NSLog(@"ab %p %@", ab, [ab class]);
}
如果在之前,输出的结果会如下所示:
number1 0xb000000000000012 __NSCFNumber
number2 0xb000000000000022 __NSCFNumber
a 0xa000000000000611 NSTaggedPointerString
ab 0xa000000000062612 NSTaggedPointerString
去除前后的标识,剩下的中间值就是我们设置的变量的值。
但是实际上,输出如下:
number1 0xe629750fa0a06e3c __NSCFNumber
number2 0xe629750fa0a06e0c __NSCFNumber
a 0xf629750fa0a0683f NSTaggedPointerString
ab 0xf629750fa0a6483c NSTaggedPointerString
我们可以看到字符串的类型仍然为NSTaggedPointerString
,但是对象地址看起来跟正常的一样了,这是因为在某个时间结点(不太清楚啥时候,但不重要)之后,对于 TaggedPointer
地址进行了混淆操作,那我们可以通过查看isTaggedPointer
方法实现,去判断一个针是否为TaggedPointer
,源码如下:
#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __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_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
#endif
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
从上面可以看到判断是否Tagged Pointer
,仅仅是将地址和_OBJC_TAG_MASK
进行一个与运算,将其结果和_OBJC_TAG_MASK
进行比对。_OBJC_TAG_MASK
在Mac OS下为1UL
,其他平台下为1UL<<63
。
这个方法其实相当于判断地址最高位是否为 1,为 1 则说明是Tagged Pointer
,为啥是判断地址最高位是否为 1?需要去学习一下&
与操作了。
我们将上面打印出的第一个字符串的地址:0xf629750fa0a0683f
,转成二进制:
可以看到最高位为 1,说明确实是Tagged Pointer
。
我们将 NSNumber 对象的地址转成 2 进制之后,首位也是 1,说明也是Tagged Pointer
,但是打印出的类型不像字符串那样NSTaggedPointerString
直接能看出来是Tagged Pointer
,可能是苹果并没有为 NSNumber 类设计一个单独的 class 来表示Tagged Pointer
。
3. Tagged Pointer类型区分
上面我们知道了如何判断一个对象指针是否为Tagged Pointer
,但是NSNumber 、NSString
都有可能是Tagged Pointer
,怎么去区分其类型呢?我们需要获取到混淆之前的地址,可以在源码中通过objc_debug_taggedpointer_obfuscator
查找TaggedPointer
的编码和解码,来查看底层是如何混淆处理的:
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;
}
我们看到在编码时是将指针地址和objc_debug_taggedpointer_obfuscator
进行了一次异或,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.
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
// # define _OBJC_TAG_MASK (1UL<<63) 就是判断时 & 的 _OBJC_TAG_MASK
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}
为了获取小对象的真实地址,我们可以将解码的源码拷贝到外面,将混淆之后的地址进行解码,如下所示:
image.jpeg
观察解码后的小对象地址,其中的62
表示b
的ASCII
码,再以NSNumber
为例,同样可以看出,1就是我们实际的值:
到这里,我们验证了小对象指针地址中确实存储了值,那么小对象地址高位其中的0xa、0xb
又是什么含义呢?
//NSString
0xa000000000000621
//NSNumber
0xb000000000000012
0xb000000000000025
0xa
转换成二进制为 1 010
(64为为1,63~61后三位表示 tagType类型 - 2),表示NSString
类型
0xb
转换为二进制为 1 011
(64为为1,63~61后三位表示 tagType类型 - 3),表示NSNumber
类型,这里需要注意一点,如果NSNumber
的值是-1,其地址中的值是用补码
表示的。
这里可以通过_objc_makeTaggedPointer
方法的参数tag
类型objc_tag_index_t
进入其枚举,其中 2表示NSString
,3表示NSNumber
:
4. 总结
-
Tagged Pointer
其实是地址 + 值
,并不是真正的对象,只是一个披着对象皮的普通变量。可以直接进行读取,占用空间小,节省内存; -
Tagged Pointer
在retain
和release
时,都直接返回了,不需要管理其生命周期; -
Tagged Pointer
并不会存储在堆中,而是在常量区
,也不需要malloc和free,所以可以直接读取,相比存储在堆区的数据读取,效率上快了3倍左右,创建的效率相比堆区快了近100倍左右; -
Tagged Pointer
的64位地址中,前4位代表类型
,后4位主要适用于系统做一些处理,中间56位用于存储值
; - 对于
NSString
来说,当字符串较小时,建议直接通过@""初始化,因为存储在常量区
,可以直接进行读取,会比WithFormat
初始化方式更加快速。
网友评论