TaggedPointer介绍
在为了改进从 32位CPU 迁移到 64位CPU 的内存浪费和效率 问题,在 64位CPU 环境下,引入了 Tagged Pointer
。旨在提高内存效率和运行性能,尤其针对小的、频繁使用的对象,如NSNumber
, NSDate
, 和NSString
等。在64位处理器上,每个指针占用8字节,而某些对象可能只包含少量数据,因此使用完整对象和指针来管理这些小对象可能造成不必要的内存消耗和性能开销。
内存分布
以 NSInteger
封装成 NSNumber
为例,内存分布图如下:
未引入TaggedPointer时
在32位处理器中,对象占用的内存有12字节
在64位处理器中,对象占用的内存有24字节,可见翻了一倍
引入TaggedPointer时
在32位处理器中,对象占用的内存有12字节
在64位处理器中,对象占用的内存只有8字节,节省了4个字节的空间,而且引用计数 retainCount
为最大值。
引用了 Tagged Pointer
的对象,节省了分配在堆区的空间,将值存在指针区域的栈区。从而节省了内存空间以及大大提升了访问速度。
TaggedPointer特点
在WWDC2013中苹果介绍了Tagged Pointer有以下特点:
-
Tagged Pointer
专门用来存储小的对象,例如NSNumber
和NSDate
-
Tagged Pointer
指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。 -
在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。
在OC中每个对象都有isa指针,但Tagged Pointer
没有,因为Tagged Pointer
并不是真正的对象,它没有在堆上分配空间而是直接将值存在指针区域的栈区。如果直接访问Tagged Pointer`
类型的变量的isa指针的话就会在编译时发出警告:
只要避免在代码中直接访问对象的 isa 变量,即可避免这个问题。可以使用isKindOfClass
和 object_getClass
TaggedPointer数据混淆
运行下面代码:
objective-c
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"L"];
NSLog(@"%p - %@ - %@",str,str,str.class);
}
return 0;
}
通过数据类型可以看出是TaggedPointer,但是它的地址看着十分奇怪这是因为它做了数据混淆。
数据混淆可以防止恶意用户或逆向工程师直接从内存中读取并理解数据,特别是当这些数据包含敏感信息时。
通过查看源码可知异或一个objc_debug_taggedpointer_obfuscator
objective-c
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
//此处进行异或操作
uintptr_t value = (objc_debug_taggedpointer_obfuscator ^ ptr);
#if OBJC_SPLIT_TAGGED_POINTERS
if ((value & _OBJC_TAG_NO_OBFUSCATION_MASK) == _OBJC_TAG_NO_OBFUSCATION_MASK)
return (void *)ptr;
uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;
uintptr_t permutedTag = _objc_basicTagToObfuscatedTag(basicTag);
value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
value |= permutedTag << _OBJC_TAG_INDEX_SHIFT;
#endif
return (void *)value;
}
接着搜索可知这是一个随机值在iOS12之后引入的
objective-c
static void
initializeTaggedPointerObfuscator(void)
{
if (!DisableTaggedPointerObfuscation && dyld_program_sdk_at_least(dyld_fall_2018_os_versions)) {
//此处是随机值 arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
#if OBJC_SPLIT_TAGGED_POINTERS
objc_debug_taggedpointer_obfuscator &= ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK);
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 {
objc_debug_taggedpointer_obfuscator = 0;
}
}
是否开启混淆通过DisableTaggedPointerObfuscation
来控制,如果开启了混淆会给objc_debug_taggedpointer_obfuscator
赋值一个随机数。如果没有开启混淆会将objc_debug_taggedpointer_obfuscator
赋值为0
解决TaggedPointer数据混淆
这里提供两种方法解决数据混淆,第一种是通过解密函数,第二种是改变环境配置
解密函数
通过上面的源代码不难发现是原来的数据异或一个objc_debug_taggedpointer_obfuscator,如果再异或一次就能得到原来的数据
objective-c
extern uintptr_t objc_debug_taggedpointer_obfuscator;
uintptr_t
ssl_objc_decodeTaggedPointer(id ptr)
{
// 再次异或解密
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"L"];
NSLog(@"%p - %@ - %@ -0x%lx",str,str,str.class,ssl_objc_decodeTaggedPointer(str));
}
return 0;
}
0x800000000000260b就是我们想要的数据
改变环境变量
- 打开Xcode项目。
- 在顶部菜单栏中选择"Product",然后选择"Scheme",再点击"Edit Scheme..."。或者也可以在右侧的"Scheme"面板中点击齿轮图标选择"Edit Scheme..."。
- 在弹出的编辑Scheme窗口中,左侧选择你的目标App或Test Bundle。
- 在"Run"或"Profile"或"Analyze"或"Test"的标签页中找到"Environment Variables"。
- 在"Environment Variables"列表中,设置环境变量
OBJC_DISABLE_TAG_OBFUSCATION
为YES
,关闭数据混淆
x86-64下的TaggedPointer结构
TaggedPointer标志位
下面的源码是判断是否为tagged pointer
类型:
objective-c
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
#endif
这里用到了_OBJC_TAG_MASK
掩码,来看下它的定义:
objective-c
#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
类型。
类标志位
下面是类的标志位
objective-c
// 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,
第1-3
位是类标志位
数据类型和所占位数
第4-7位表示数据类型或字符串长度
数据存储和结构图
tagged pointer
的8-63
位用来存储数据
arm64下的TaggedPointer结构
objective-c
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;
}
TaggedPointer标志位
objective-c
#if OBJC_SPLIT_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
# define _OBJC_TAG_INDEX_SHIFT 0
# define _OBJC_TAG_SLOT_SHIFT 0
# define _OBJC_TAG_PAYLOAD_LSHIFT 1
# define _OBJC_TAG_PAYLOAD_RSHIFT 4
# define _OBJC_TAG_EXT_MASK (_OBJC_TAG_MASK | 0x7UL)
# define _OBJC_TAG_NO_OBFUSCATION_MASK ((1UL<<62) | _OBJC_TAG_EXT_MASK)
# define _OBJC_TAG_CONSTANT_POINTER_MASK \
~(_OBJC_TAG_EXT_MASK | ((uintptr_t)_OBJC_TAG_EXT_SLOT_MASK << _OBJC_TAG_EXT_SLOT_SHIFT))
# define _OBJC_TAG_EXT_INDEX_SHIFT 55
# define _OBJC_TAG_EXT_SLOT_SHIFT 55
# define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 9
# define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12
在arm64
环境下_OBJC_TAG_MASK
的值为 (1UL<<63)
,ptr & (1UL<<63)
还是等于(1UL<<63)
,也就是说指针地址第63
位是1
的时候,代表这个指针是tagged pointer
类型。
类标志位
类标志位
放在了地址的前三位,即0-2
位
数据类型&字符串长度
数据类型&字符串长度
放在地址的3-6
位
数据存储和结构图
7
到62
位用来存储数据
TaggedPointer结构总结
在x86结构中:
- 地址最低位也就是0位是TaggedPointer标志位
- 第1-3位是类标志位
- 第4-7位表示数据类型或字符串长度
8-63
位用来存储数据
在arm64架构中:
- 地址最高位也就是63位是TaggedPointer标志位
类标志位
放在了地址的前三位,即0-2
位数据类型&字符串长度
放在地址的3-6
位7
到62
位用来存储数据
Tagged Pointer可表示的数字范围是-2^55+1 ~ 2^55-1,超出这个范围的数字,NSNumber会转换为普通的Objective-C对象分配在堆上。
Tagged Pointer可表示的字符串范围是9个字符。字符串会转成__NSCFString
类型
TaggedPointer面试题
执行下面两段代码,有什么区别?
objective-c
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"];
});
}
objective-c
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
类型。
第一段代码字符串是__NSCFString
存储在堆上,它是个正常对象,需要维护引用计数的。异步并发执行setter
方法,可能就会有多条线程同时执行[_name release]
,连续release
两次就会造成对象的过度释放,导致Crash
。
第二段代码中的NSString
为NSTaggedPointerString
类型,在objc_release
函数中会判断指针是不是TaggedPointer
类型,是的话就不对对象进行release
操作,也就避免了因过度释放对象而导致的Crash