TaggedPointer是苹果推出的一种通过指针来存储少量数据的技术,可以有效避免内存浪费。
本文所用源码为objc4-756.2
,macOS版本为10.15.2
TaggedPointer的辨别
在objc-internal.h
中有这么一段用来判断是否为TaggedPointer的代码:
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
_OBJC_TAG_MASK
又是啥?
显然,对于iOS而言这个值是1<<63
,对于macOS而言这个值是1
。在64位机器上,一个指针占8字节,共64位。
因此,_objc_isTaggedPointer
函数中的算法即:在iOS中,若指针最高位为1,则检测值是TaggedPointer。在macOS中,若指针最低位为1,则检测值是TaggedPointer。
TaggedPointer的编码与解码
在objc-internal.h
中有如下代码:
extern uintptr_t objc_debug_taggedpointer_obfuscator;
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
是个全局变量,这里的encode与decode利用了异或的特性实现编码解码。
在objc-runtime-new.mm
中有如下代码:
#if !SUPPORT_TAGGED_POINTERS
uintptr_t objc_debug_taggedpointer_obfuscator = 0;
#else
uintptr_t 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));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}
- 不支持TaggedPointer,则这个值为零
- macOS版本低于10.14,则这个值为零
- iOS版本低于12.0,则这个值为零(TVOS、watchOS、bridgeOS不再赘述)
- 否则采用else中的方法给
objc_debug_taggedpointer_obfuscator
赋值
控制台中作如下操作:
对NSNumber
而言:对TaggedPointer解码后,得到的真实指针最后两位始终是27
,用来作为标识符,从第三位起用来存储真实数据。
对NSString
而言:对TaggedPointer解码后,得到的真实指针最后一位始终是5
,倒数第二位是字符串长度。用最后两位来标识NSString的TaggedPointer,而剩下的位数用来存储数据。以a字符为例,剩下的是0x61对应十进制为97,也就是a字符的ASCII码。当字符串长度大于11
时,字符串从TaggedPointer存储变成OC对象存储。
非ASCII码的字符串验证
以汉字你好
为例:
显然并没有使用TaggedPointer存储
TaggedPointer的有效位数
上文已经说了NSString的有效位为字符串长度小于等于11位
,下面来看NSNumber的有效位:
void number_test() {
u_long n = 1;
for (int bit = 1; bit < 100; bit++) {
NSNumber *num = @(n);
NSLog(@"bit: %d---num: %@", bit, num);
NSLog(@"ptr: %p", num);
n = (n<<1)+1;
}
}
55位的时候,num仍然是TaggedPointer类型,从56位及以后就变成了OC类型。由于n始终是满位状态,将代码改进一下,进一步验证第56位是OC类型:
void number_test() {
u_long n = 1;
for (int bit = 1; bit < 100; bit++) {
NSNumber *num = @(n);
NSLog(@"bit: %d---num: %@", bit, num);
NSLog(@"ptr: %p", num);
if (bit == 55) {
num = @(n+1);
NSLog(@"num:%@---ptr: %p", num, num);
}
n = (n<<1)+1;
}
}
可见,确实从56位起num就变成了OC对象,所以TaggedPointer的有效位为2^55 -1
(2^55
是第56位)。
Have fun!