TaggedPointer 概念
2013 年 9 月苹果推出了首个采用 64 位架构的 A7 双核处理器的手机 iPhone5s,为了改进从 32 位 CPU
迁移到 64 位 CPU
的内存浪费和效率问题,在 64 位 CPU
环境下,苹果工程师提出了 Tagged Pointer
的概念。采用这一机制,系统会对 NSString
、NSNumber
和 NSDate
等对象进行优化。建议大家看看 WWDC2020 这个视频的介绍。
一般我们存储一个对象都是通过指针找到堆区的内存地址,Tagged Pointer
其实也是一个指针,但是与普通的指针相比会有一点特殊,它相当于在 pointer
的基础上加上 tagged
标记,作用就是针对一些小对象 NSString
、NSNumber
和 NSDate
等,以 NSString *str = [NSString stringWithFormat:@"cx"]
这句代码为例,当只存储 cx
这两个字符的时候,其实是用不了 64 位的,如果还用 指针->堆区
的存储形式就会造成内存上的浪费。所以这时候苹果工程师提出了用 Tagged Pointer
来存储这些小对象,Tagged Pointer
就相当于指针加上内容,这样的话这些数据就会存在栈区,既节约了内存又大大提升了这些对象的开辟及销毁速度。所以了解 Tagged Pointer
是很有必要的,而且在 Swift
中我们可以自己创建 Tagged Pointer
。
x86-64 下的 Tagged Pointer 结构
如图我们通过 stringWithFormat
创建一个字符串,打印可以看到是 NSTaggedPointerString
类型,而且地址既不是 0x6
开头也不是 0x7
开头,可以看得出这是一个小对象类型。
在 objc
源码中搜索 TaggedPointer
可以看到这样一段注释,在 WWDC2020 这个视频也介绍了 payload
代表有效负载,是字符串真正存储的地方,这里可以看出 payload
需要经过 decoded_obj
加上一些位运算操作得到,说明是一个加密解密的过程,下面我们来搜索一下 decod
。
搜索可以看到 _objc_decodeTaggedPointer_noPermute
函数,这里 ptr
就是传入的地址,最后返回的 value
等于 value ^ objc_debug_taggedpointer_obfuscator
,而 objc_debug_taggedpointer_obfuscator
代表混淆,在 initializeTaggedPointerObfuscator
中可以看到,会判断 DisableTaggedPointerObfuscation
(是否开启混淆),是的话 objc_debug_taggedpointer_obfuscator
会被赋值一个随机数,不是的话 objc_debug_taggedpointer_obfuscator
就是 0。这说明我们在上面案例中输出的 str
地址是 encode
过的,所以我们要进行一次 decode
。
如图可以看出在 encode
之前会根据架构的不同,会进行一些位移操作。
如图 $0
中的 0b
代表二进制,1
代表 Tagged Pointer
类型指针,我们将 $0
右移 3 位,然后分别输出最右边的两个字节,然后通过 ASCII
码对照表查看就是 c
, x
这两个字符。
在 WWDC2020 这个视频也介绍了第二位到第四位是表示类型的,这里我们新增一种 NSNumber
类型,打印可以看到分别是 2 和 3,与源码对照也是正确的。这么我们分析的是 x86-64
下的 Tagged Pointer
的结构,下面我们分析一下 arm64
架构下的结构。
arm64 下的 Tagged Pointer 结构
如图可以看到如果真机环境下需要多处理一些红色圈中部分,会比较麻烦一点。下面我们用一个比较取巧的方式。
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
// PAYLOAD_LSHIFT and PAYLOAD_RSHIFT are the payload extraction shifts.
// They are reversed here for payload insertion.
// ASSERT(_objc_taggedPointersEnabled());
if (tag <= OBJC_TAG_Last60BitPayload) {
// ASSERT(((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT) == value);
uintptr_t result =
(_OBJC_TAG_MASK |
((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |
((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
} else {
// ASSERT(tag >= OBJC_TAG_First52BitPayload);
// ASSERT(tag <= OBJC_TAG_Last52BitPayload);
// ASSERT(((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT) == value);
uintptr_t result =
(_OBJC_TAG_EXT_MASK |
((uintptr_t)(tag - OBJC_TAG_First52BitPayload) << _OBJC_TAG_EXT_INDEX_SHIFT) |
((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >> _OBJC_TAG_EXT_PAYLOAD_LSHIFT));
return _objc_encodeTaggedPointer(result);
}
}
在 _objc_makeTaggedPointer
函数中我们可以看到,在 _objc_encodeTaggedPointer
之前需要遵循一个原则,就是 _objc_taggedPointersEnabled
。
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;
#if OBJC_SPLIT_TAGGED_POINTERS
// The obfuscator doesn't apply to any of the extended tag mask or the no-obfuscation bit.
objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK);
// Shuffle the first seven entries of the tag permutator.
int max = 7;
for (int i = max - 1; i >= 0; i--) {
int target = arc4random_uniform(i + 1);
swap(objc_debug_tag60_permutations[i],
objc_debug_tag60_permutations[target]);
}
#endif
} else {
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
objc_debug_taggedpointer_obfuscator = 0;
}
}
在 initializeTaggedPointerObfuscator
函数中可以看到是通过 DisableTaggedPointerObfuscation
来对 objc_debug_taggedpointer_obfuscator
赋值,当 objc_debug_taggedpointer_obfuscator
等于 0 的时候就不需要再进行复杂的位运算。
所以我们搜索 DisableTaggedPointerObfuscation
可以看到 OBJC_DISABLE_TAG_OBFUSCATION
,代表关闭混淆,我们在项目中进行配置就可以关闭混淆。这样我们就不需要再进行一些位运算操作了。
在真机环境下可以看出有些不同,现在左边第一位还是代表 Tagged Pointer
类型,但是原来是左边第二位到第四位是代表类型的,但是 arm64
下是最右边 3 位代表类型,第 4 位到第七位代表字符串的长度,arm64
是小端模式,从第 8 位开始才是内容的存储位。
但是 NSNumber
类型,第 4 位到第七位代表的意义就有些变化,是代表 NSNumber
承载的类型的,总结一下就是 0 代表 char
,1 代表 short
,2 代表 int
,3 代表 long
,4 代表 float
,5 代表 double
。大家也可以自己验证下。
Tagged Pointer 相关问题
类似这样一个案例,taggedPointerDemo
方法执行没有问题,当 touchesBegan
执行的时候就会崩溃,这是因为 taggedPointerDemo
中 self.nameStr
是 NSTaggedPointerString
类型,而 touchesBegan
中 self.nameStr
是 __NSCFString
类型。所以 touchesBegan
就涉及到多线程的写和读,当对 self.nameStr
赋值时就会涉及到对旧值的 release
和对新值的 retian
。这就可能涉及到对一个已经 release
过的内存进行访问,就会出现坏地址的访问,所以会崩溃。而 touchesBegan
中 self.nameStr
为 __NSCFString
类型是因为字符串的长度超过了有效负载位能承载的最大长度,所以就需要开辟堆空间来存储。
通过源码也可以看到,当是 taggedPointer
类型时 rootRetain
与 rootRelease
函数不进行任何操作,所以不涉及堆内存的释放与回收以及一些下层方法的处理,所以 taggedPointer
类型速度会很快,效率会更高。
网友评论