【iOS】——TaggedPointer

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专门用来存储小的对象,例如NSNumberNSDate

  • Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free。

  • 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。

在OC中每个对象都有isa指针,但Tagged Pointer没有,因为Tagged Pointer并不是真正的对象,它没有在堆上分配空间而是直接将值存在指针区域的栈区。如果直接访问Tagged Pointer`

类型的变量的isa指针的话就会在编译时发出警告:

只要避免在代码中直接访问对象的 isa 变量,即可避免这个问题。可以使用isKindOfClassobject_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就是我们想要的数据

改变环境变量

  1. 打开Xcode项目。
  2. 在顶部菜单栏中选择"Product",然后选择"Scheme",再点击"Edit Scheme..."。或者也可以在右侧的"Scheme"面板中点击齿轮图标选择"Edit Scheme..."。
  3. 在弹出的编辑Scheme窗口中,左侧选择你的目标App或Test Bundle。
  4. 在"Run"或"Profile"或"Analyze"或"Test"的标签页中找到"Environment Variables"。
  5. 在"Environment Variables"列表中,设置环境变量OBJC_DISABLE_TAG_OBFUSCATIONYES,关闭数据混淆



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的值为1ptr & 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 pointer8-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

数据存储和结构图

762位用来存储数据

TaggedPointer结构总结

在x86结构中:

  • 地址最低位也就是0位是TaggedPointer标志位
  • 第1-3位是类标志位
  • 第4-7位表示数据类型或字符串长度
  • 8-63位用来存储数据

在arm64架构中:

  • 地址最高位也就是63位是TaggedPointer标志位
  • 类标志位放在了地址的前三位,即0-2
  • 数据类型&字符串长度放在地址的3-6
  • 762位用来存储数据

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

第二段代码中的NSStringNSTaggedPointerString类型,在objc_release函数中会判断指针是不是TaggedPointer类型,是的话就不对对象进行release操作,也就避免了因过度释放对象而导致的Crash

相关推荐
LOLA44441 小时前
如何解压rar格式文件?8种方法(Win/Mac/手机/网页端)
macos·软件需求
陈皮话梅糖@10 小时前
iOS 集成ffmpeg
ios·ffmpeg
allanGold11 小时前
【xcode 16.2】升级xcode后mac端flutter版的sentry报错
macos·xcode16.2
幽夜落雨11 小时前
ios老版本应用安装方法
ios
gxhlh17 小时前
局域网中 Windows 与 Mac 互相远程连接的最佳方案
windows·macos
宏基骑士17 小时前
mac 电脑上安装adb命令
macos·adb
胖虎119 小时前
实现 iOS 自定义高斯模糊文字效果的 UILabel(文末有Demo)
ios·高斯模糊文字·模糊文字
水银嘻嘻1 天前
【Mac】Python相关知识经验
开发语言·python·macos
梦魇梦狸º2 天前
mac 配置 python 环境变量
chrome·python·macos
丁总学Java2 天前
macOS如何进入 Application Support 目录(cd: string not in pwd: Application)
macos