【iOS】Tagged Pointer

目录

    • 前言
    • [什么是Tagged Pointer?](#什么是Tagged Pointer?)
      • [引入Tagged Pointer技术之前](#引入Tagged Pointer技术之前)
      • [引入Tagged Pointer之后](#引入Tagged Pointer之后)
      • 总结
    • [Tagged Pointer原理(Tag+Data分析)](#Tagged Pointer原理(Tag+Data分析))
    • [判断Tagged Pointer](#判断Tagged Pointer)
      • [Tagged Pointer应用](#Tagged Pointer应用)
      • [Tagged Pointer 注意点](#Tagged Pointer 注意点)
    • [Tagged Pointer注释](#Tagged Pointer注释)
    • [创建Tagged Pointer](#创建Tagged Pointer)
      • [Tagged Pointer初始化](#Tagged Pointer初始化)
      • [生成Tagged Pointer指针](#生成Tagged Pointer指针)
    • 相关题目

前言

学习Tagged Pointer原理,参考文章:

老生常谈内存管理(五):Tagged Pointer

什么是Tagged Pointer?

由于NSNumber、NSDate、NSString一类的变量本身的值需要占用的内存大小常常不需要8个字节,拿整数来说,4 个字节所能表示的有符号整数就可以达到 20 多亿(注:2^31=2147483648,另外 1 位作为符号位),对于绝大多数情况都是可以处理的

为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念。对于64位程序,引入Tagged Pointer后,相关逻辑能减少一半的内存占用,以及3倍的访问速度提升,100倍的创建、销毁速度提升

引入Tagged Pointer技术之前

对象存储在堆上,对象的指针存储的是堆中对象的地址值(即指向堆中的对象)

从内存上看

这样的存储方式会在CPU位数升级的情况下导致占用存储翻倍(以NSNumber对象为例):

32位系统下的指针占4字节,64位系统下的指针占8字节

从效率上看

为了存储和访问一个 NSNumber 对象,我们需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失

引入Tagged Pointer之后

会将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了Tagged Pointer对象之后,64位CPU下NSNumber的内存图变成了以下这样:

如图,Tagged Pointer是将值的信息直接存储到了指针本身里面

要注意的是,当8字节可以承载用于表示的数值时,系统就会以Tagged Pointer的方式生成指针,如果8字节承载不了时,则又用以前的方式来生成普通的指针,才会将对象存储在堆上

由于Tagged Pointer不是对象,所以它的isa应该是无指向的:

objectivec 复制代码
NSNumber *number1 = @7;

总结

  • Tagged Pointer专门用来存储小的对象,例如NSNumber和NSDate
  • Tagged Pointer指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要malloc和free
  • 在内存读取上有着3倍的效率,创建时比以前快106倍

使用了Tagged Pointer,NSNumber对象的值直接存储在了指针上,不会在堆上申请内存。则使用一个NSNumber对象只需要指针的8个字节内存就够了,大大的节省了内存占用

Tagged Pointer原理(Tag+Data分析)

关闭数据混淆

在现在的版本中,为了保证数据安全,苹果对Tagged Pointer做了数据混淆,开发者通过打印指针无法判断它是不是一个Tagged Pointer,更无法读取Tagged Pointer的存储数据

所以在分析Tagged Pointer之前,我们需要先关闭Tagged Pointer的数据混淆,以方便我们调试程序。通过设置环境变量 OBJC_DISABLE_TAG_OBFUSCATIONYES

  • 进入Edit Scheme页面:

  • 添加环境变量OBJC_DISABLE_TAG_OBFUSCATION并将其设置为YES

  • 添加OBJC_DISABLE_TAGGED_POINTERS并设置为YES,会禁用Tagged Pointer

MacOS分析

NSNumber

MacOSNSNumber的Tagged Pointer位视图:

MacOS下采用 LSB(Least Significant Bit,即最低有效位)为Tagged Pointer标识位

比如,打印存储整型1的NSNumber变量地址,为0x127十六进制:

  • 1表示整型1

  • 2表示数据类型为int

    倒数第二位 对应数据类型
    0 char
    1 short
    2 int
    3 long
    4 float
    5 double
  • 7的二进制为0111,最后一位1Tagged Pointer标识位 ,表示这个指针是Tagged Pointer;前面三位011类标识位 ,对应十进制为3,表示NSNumber类

    objc4源码可查出各个类的标识位:

    objectivec 复制代码
    // objc-internal.h
    {
        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,
        // ......
    }
NSString

MacOSNSString的Tagged Pointer位视图:

MacOS下采用 LSB(Least Significant Bit,即最低有效位)为Tagged Pointer标识位

64 bit的MacOS下,如果一个NSString对象指针为Tagged Pointer,那么它的后4位(0-3)作为标识位 ,第4-7位表示字符串长度 ,剩余的56位就可以用来存储字符串

比如,打印下列字符串的NSString变量地址:

objectivec 复制代码
// MRC 环境
#define HTLog(_var) \
{ \
    NSString *name = @#_var; \
    NSLog(@"%@: %p, %@, %lu", name, _var, [_var class], [_var retainCount]); \
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSString *a = @"a";
        NSMutableString *b = [a mutableCopy];
        NSString *c = [a copy];
        NSString *d = [[a mutableCopy] copy];
        NSString *e = [NSString stringWithString:a];
        NSString *f = [NSString stringWithFormat:@"f"];
        NSString *string1 = [NSString stringWithFormat:@"abcdefg"];
        NSString *string2 = [NSString stringWithFormat:@"abcdefghi"];
        NSString *string3 = [NSString stringWithFormat:@"abcdefghij"];
        HTLog(a);
        HTLog(b);
        HTLog(c);
        HTLog(d);
        HTLog(e);
        HTLog(f);
        HTLog(string1);
        HTLog(string2);
        HTLog(string3);
    }
    return 0;
}
/*
a: 0x100002038, __NSCFConstantString
b: 0x10071f3c0, __NSCFString
c: 0x100002038, __NSCFConstantString
d: 0x6115, NSTaggedPointerString
e: 0x100002038, __NSCFConstantString
f: 0x6615, NSTaggedPointerString
string1: 0x6766656463626175, NSTaggedPointerString
string2: 0x880e28045a54195, NSTaggedPointerString
string3: 0x10071f6d0, __NSCFString
*/

从打印结果来看,有三种NSString类型:

类型 描述
__NSCFConstantString 1. 常量字符串,存储在字符串常量区,继承于 __NSCFString相同内容的 __NSCFConstantString 对象的地址相同,也就是说常量字符串对象是一种单例,可以通过 == 判断字符串内容是否相同2. 这种对象一般通过字面值@"..."创建。如果使用 __NSCFConstantString 来初始化一个字符串,那么这个字符串也是相同的 __NSCFConstantString。
__NSCFString 1. 存储在堆区,需要维护其引用计数,继承于 NSMutableString2. 通过stringWithFormat:等方法创建的NSString对象(且字符串值过大无法使用Tagged Pointer存储)一般都是这种类型。
NSTaggedPointerString Tagged Pointer,字符串的值直接存储在了指针上

打印结果分析:

NSString对象 类型 分析
a __NSCFConstantString 通过字面量@"..."创建
b __NSCFString a 的深拷贝,指向不同的内存地址,被拷贝到堆区
c __NSCFConstantString a 的浅拷贝,指向同一块内存地址
d NSTaggedPointerString 单独对 a 进行 copy(如 c),浅拷贝是指向同一块内存地址,所以不会产生Tagged Pointer;单独对 a 进行 mutableCopy(如 b),复制出来是可变对象,内容大小可以扩展;而Tagged Pointer存储的内容大小有限,因此无法满足可变对象的存储要求。
e __NSCFConstantString 使用 __NSCFConstantString 来初始化的字符串
f NSTaggedPointerString 通过stringWithFormat:方法创建,指针足够存储字符串的值。
string1 NSTaggedPointerString 通过stringWithFormat:方法创建,指针足够存储字符串的值。
string2 NSTaggedPointerString 通过stringWithFormat:方法创建,指针足够存储字符串的值。
string3 __NSCFString 通过stringWithFormat:方法创建,指针不足够存储字符串的值。

可以看到,为Tagged Pointer的有dfstring1string2指针,它们的指针值分别为
0x61150x66150x67666564636261750x880e28045a54195

  • 其中0x610x660x67666564636261分别对应字符串的ASCII
  • 最后一位5的二进制为0101,最后一位1是代表这个指针是Tagged Pointer,010对应十进制为2,表示NSString
  • 倒数第二位1179代表字符串长度

对于string2的指针值0x880e28045a54195,虽然从指针中看不出来字符串的值,但其也是一个Tagged Pointer

iOS分析

iOS下则采用 MSB(Most Significant Bit,即最高有效位)为Tagged Pointer标识位

iOSNSNumber的Tagged Pointer位视图:

iOSNSString的Tagged Pointer位视图:

与MacOS主要区别就是标识位的位置不同

判断Tagged Pointer

依据就是Tagged Pointer的标识位

查以下函数源码:

objectivec 复制代码
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

可以看到,_objc_isTaggedPointer函数的实现是将指针值与一个_OBJC_TAG_MASK掩码进行按位与运算,查看该掩码:

再查宏判断条件的值:

可以看到,arm64架构下的掩码为1UL<<63x86 64架构且MacOS系统下的掩码为1UL,其他情况下的掩码为1UL<<63

简言之,iOSarm64下采用最高有效位 为Tagged Pointer的标识位,MacOSx86 64下采用最低有效位为Tagged Pointer的标识位

而存储在堆空间的对象由于内存对齐 (16的倍数),它的内存地址的最低有效位为0。由此可以辨别Tagged Pointer一般对象指针

Tagged Pointer应用

objc_msgSend能识别Tagged Pointer,比如NSNumberintValue方法,直接从指针提取数据,不会进行objc_msgSend的三大流程,节省了调用开销(消息转发、寻找isa指向等)

内存管理相关的,如retain方法中调用的rootRetain

objectivec 复制代码
ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
    // 如果是 tagged pointer,直接返回 this
    if (isTaggedPointer()) return (id)this; 

    bool sideTableLocked = false;
    bool transcribeToSideTable = false; 

    isa_t oldisa;
    isa_t newisa;
    ......

isTaggedPointer()函数实现:

objectivec 复制代码
inline bool 
objc_object::isTaggedPointer() 
{
    return _objc_isTaggedPointer(this);
}

Tagged Pointer 注意点

我们知道,所有OC对象都有isa指针,而Tagged Pointer并不是真正的对象,它没有isa指针,所以如果你直接访问Tagged Pointer的isa成员的话,将会有如下警告:

Tagged Pointer注释

源码中对Tagged Pointer有这样一段注释:

cpp 复制代码
/***********************************************************************
* Tagged pointer objects.
*
* Tagged pointer objects store the class and the object value in the
* object pointer; the "pointer" does not actually point to anything.
*
* Tagged pointer objects currently use this representation:
* (LSB)
*  1 bit   set if tagged, clear if ordinary object pointer
*  3 bits  tag index
* 60 bits  payload
* (MSB)
* The tag index defines the object's class.
* The payload format is defined by the object's class.
*
* If the tag index is 0b111, the tagged pointer object uses an
* "extended" representation, allowing more classes but with smaller payloads:
* (LSB)
*  1 bit   set if tagged, clear if ordinary object pointer
*  3 bits  0b111
*  8 bits  extended tag index
* 52 bits  payload
* (MSB)
*
* Some architectures reverse the MSB and LSB in these representations.
*
* This representation is subject to change. Representation-agnostic SPI is:
* objc-internal.h for class implementers.
* objc-gdb.h for debuggers.
**********************************************************************/

翻译:

  1. Tagged pointer 指针对象将class和对象数据存储在对象指针中,指针实际上不指向任何东西。

  2. Tagged pointer 当前使用此表示形式:

    (LSB)(macOS)64位分布如下:

    • 1 bit 标记是 Tagged Pointer
    • 3 bits 标记类型
    • 60 bits 负载数据容量,(存储对象数据)

    (MSB)(iOS)64位分布如下:

    • tag index 表示对象所属的 class
    • 负载格式由对象的 class 定义
    • 如果 tag index 是 0b111(7), tagged pointer 对象使用 "扩展" 表示形式
    • 允许更多类,但 有效载荷 更小

    (LSB)(macOS)(带有扩展内容)64位分布如下:

    • 1 bit 标记是 Tagged Pointer
    • 3 bits 是 0b111
    • 8 bits 扩展标记格式
    • 52 bits 负载数据容量,(存储对象数据)
  3. 在这些表示中,某些体系结构反转了MSB和LSB

从注释中我们得知:

  • Tagged pointer存储对象数据目前分为60bits负载容量和52bits负载容量。
  • 类标识允许使用扩展形式

那么如何判断负载容量?类标识的扩展类型有那些?我们来看下全面的objc_tag_index_t源码:

cpp 复制代码
{
    // 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,

    // 60-bit reserved
    // 保留位
    OBJC_TAG_RESERVED_7        = 7, 

    // 52-bit payloads
    OBJC_TAG_Photos_1          = 8,
    OBJC_TAG_Photos_2          = 9,
    OBJC_TAG_Photos_3          = 10,
    OBJC_TAG_Photos_4          = 11,
    OBJC_TAG_XPC_1             = 12,
    OBJC_TAG_XPC_2             = 13,
    OBJC_TAG_XPC_3             = 14,
    OBJC_TAG_XPC_4             = 15,
    OBJC_TAG_NSColor           = 16,
    OBJC_TAG_UIColor           = 17,
    OBJC_TAG_CGColor           = 18,
    OBJC_TAG_NSIndexSet        = 19,
    OBJC_TAG_NSMethodSignature = 20,
    OBJC_TAG_UTTypeRecord      = 21,
    OBJC_TAG_Foundation_1      = 22,
    OBJC_TAG_Foundation_2      = 23,
    OBJC_TAG_Foundation_3      = 24,
    OBJC_TAG_Foundation_4      = 25,


    OBJC_TAG_FirstUnobfuscatedSplitTag = 136, // 128 + 8, first ext tag with high bit set

    OBJC_TAG_Constant_CFString = 136,
	
	// 前60位负载内容
    OBJC_TAG_First60BitPayload = 0, 
    // 后60位负载内容
    OBJC_TAG_Last60BitPayload  = 6, 
    // 前52位负载内容
    OBJC_TAG_First52BitPayload = 8, 
    // 后52位负载内容
    OBJC_TAG_Last52BitPayload  = 263,
	
	// 保留位
    OBJC_TAG_RESERVED_264      = 264
};

小结

  1. 区分什么位置为负载内容位

    MacOS下采用 LSB 即OBJC_TAG_First60BitPayload、OBJC_TAG_First52BitPayload。

    iOS下则采用 MSB 即OBJC_TAG_Last60BitPayload、OBJC_TAG_Last52BitPayload。

  2. 区分负载数据容量

    当类标识为0-6时,负载数据容量为60bits。

    当类标识为7时(对应二进制为 0b111),负载数据容量为52bits。

  3. 类标识的扩展类型有哪些?

    如果tag index 是 0b111(7), tagged pointer对象使用 "扩展" 表示形式

    类标识的扩展类型为上面OBJC_TAG_Photos_1 ~OBJC_TAG_NSIndexSet。

  4. 类标识与负载数据容量对应关系

    当类标识为0-6时,负载数据容量为 60bits。即OBJC_TAG_First60BitPayload 和 OBJC_TAG_Last60BitPayload,负载数据容量 的取值区间也为 0 - 6。

    当类标识为7时,负载数据容量为52bits。即OBJC_TAG_First52BitPayload 和 OBJC_TAG_Last52BitPayload,负载数据容量的取值区间为 8 - 263。

只要一个tag,既可以区分负载数据容量,也可以区分类标识

创建Tagged Pointer

我们知道了Tagged Pointer展现给我们的是Tag + Data,那么它底层是怎么生成这种类型的伪指针的呢?为什么NSNumber、NSDate、NSString会生成,而其他的不会呢?

下面来来研究一下,这几个对象是如何生成Tagged Pointer

Tagged Pointer初始化

初始变量设置

_read_images()函数:

objectivec 复制代码
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses) {
	// ...
    if (DisableTaggedPointers) {
    	// 禁用Tagged Pointer,与环境变量OBJC_DISABLE_TAGGED_POINTERS相关
        disableTaggedPointers();
    }
	// 初始化 TaggedPointer 混淆器:用于保护 Tagged Pointer 上的数据
    initializeTaggedPointerObfuscator();
    // ...
}

initializeTaggedPointerObfuscator()函数:

objectivec 复制代码
OBJC_EXPORT uintptr_t objc_debug_taggedpointer_obfuscator
    OBJC_AVAILABLE(10.14, 12.0, 12.0, 5.0, 3.0);

static void
initializeTaggedPointerObfuscator(void)
{
    if (!DisableTaggedPointerObfuscation){
//    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 {
        // 对于与旧版 SDK 链接的应用程序,将混淆器变量设置为0,
        // 防止他们正依赖TaggedPointer
        objc_debug_taggedpointer_obfuscator = 0;
    }
}
  • objc_debug_taggedpointer_obfuscator是一个unsigned long的全局变量
  • 对于一些旧版环境和环境变量OBJC_DISABLE_TAG_OBFUSCATION,禁用数据混淆,设置objc_debug_taggedpointer_obfuscator为0,不混淆
  • 获得objc_debug_taggedpointer_obfuscator的值:
    • 将随机数据放入变量中,然后移走所有非有效位
    • ~_OBJC_TAG_MASK作一次按位与&操作
Tagged Pointer注册校验

为什么NSNumber、NSDate、NSString会转成为伪指针呢?其他的为什么不会呢?

加载程序时,从dyld库的_dyld_start()函数开始,经历了多般步骤,开始调用_objc_registerTaggedPointerClass()函数:

objectivec 复制代码
void
_objc_registerTaggedPointerClass(objc_tag_index_t tag, Class cls)
{
    if (objc_debug_taggedpointer_mask == 0) {
        _objc_fatal("tagged pointers are disabled");
    }

    Class *slot = classSlotForTagIndex(tag);
    if (!slot) {
        _objc_fatal("tag index %u is invalid", (unsigned int)tag);
    }

    Class oldCls = *slot;

    if (cls  &&  oldCls  &&  cls != oldCls) {
        _objc_fatal("tag index %u used for two different classes "
                    "(was %p %s, now %p %s)", tag, 
                    oldCls, oldCls->nameForLogging(), 
                    cls, cls->nameForLogging());
    }

	// 如果尚未设置,则将占位类存储在为扩展tag空间保留的基本标记槽中。
	// 在注册第一个扩展标记时延迟执行此操作,以便
	// 旧调试器能够更频繁地正确表征伪指针。
    *slot = cls;
    if (tag < OBJC_TAG_First60BitPayload || tag > OBJC_TAG_Last60BitPayload) {
    	// OBJC_TAG_RESERVED_7 == 7
        Class *extSlot = classSlotForBasicTagIndex(OBJC_TAG_RESERVED_7);
        if (*extSlot == nil) {
            extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
            *extSlot = (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
        }
    }
}
  • 判断是否禁用了Tagged Pointer,若禁用,则终止程序
  • 根据指定tag获取类指针。若tag被用于两个不同的类,则终止程序
  • 判断负载数据容量如果是52bits进行特殊处理,在OBJC_TAG_RESERVED_7处存储占位类OBJC_CLASS_$___NSUnrecognizedTaggedPointer

其实这个方法起的名字是注册,其实应该叫校验。校验在全局数组(以tag进行位操作为索引,类为value的全局数组)中,用 tag取出来的类指针注册的类 是否相符

根据指定tag获取类指针函数classSlotForTagIndex()

objectivec 复制代码
// 返回指向tag类数组中类存储的指针,如果标记超出范围,则返回 nil。
static ptrauth_taggedpointer_table_entry Class *
classSlotForTagIndex(objc_tag_index_t tag)
{
    if (tag >= OBJC_TAG_First60BitPayload && tag <= OBJC_TAG_Last60BitPayload) {
        return classSlotForBasicTagIndex(tag);
    }

    if (tag >= OBJC_TAG_First52BitPayload && tag <= OBJC_TAG_Last52BitPayload) {
        int index = tag - OBJC_TAG_First52BitPayload;
#if OBJC_SPLIT_TAGGED_POINTERS
        if (tag >= OBJC_TAG_FirstUnobfuscatedSplitTag)
            return &objc_tag_ext_classes[index];
#endif
        uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator
                                    >> _OBJC_TAG_EXT_INDEX_SHIFT)
                                   & _OBJC_TAG_EXT_INDEX_MASK);
        return &objc_tag_ext_classes[index ^ tagObfuscator];
    }

    return nil;
}
  • 根据负载数据容量是60bits还是52bits,区分为类标识是基础类标识还是扩展类标识。也可以说根据 tag类标识区间 判断
  • tag是基础类标识,返回classSlotForBasicTagIndex(tag)的结果
  • tag是扩展类标识,对tag进行操作,然后取出存在objc_tag_ext_classes数组里的结果返回

这里有两个重要的全局数组:

objectivec 复制代码
#if SUPPORT_TAGGED_POINTERS

extern "C" { 
    extern ptrauth_taggedpointer_table_entry Class objc_debug_taggedpointer_classes[_OBJC_TAG_SLOT_COUNT];
    extern ptrauth_taggedpointer_table_entry Class objc_debug_taggedpointer_ext_classes[_OBJC_TAG_EXT_SLOT_COUNT];
}
#define objc_tag_classes objc_debug_taggedpointer_classes
#define objc_tag_ext_classes objc_debug_taggedpointer_ext_classes

#endif

数组objc_tag_classes:存储苹果定义的几个基础类

数组objc_tag_ext_classes:存储苹果预留的扩展类

classSlotForBasicTagIndex()函数:

objectivec 复制代码
// 返回指向tag类数组中类存储的指针
// 假定该tag是一个有效的基本tag
static ptrauth_taggedpointer_table_entry Class *
classSlotForBasicTagIndex(objc_tag_index_t tag)
{
#if OBJC_SPLIT_TAGGED_POINTERS
    uintptr_t obfuscatedTag = _objc_basicTagToObfuscatedTag(tag);
    return &objc_tag_classes[obfuscatedTag];
#else
    uintptr_t tagObfuscator = ((objc_debug_taggedpointer_obfuscator
                                >> _OBJC_TAG_INDEX_SHIFT)
                               & _OBJC_TAG_INDEX_MASK);
    uintptr_t obfuscatedTag = tag ^ tagObfuscator;

    // Array index in objc_tag_classes includes the tagged bit itself
#   if SUPPORT_MSB_TAGGED_POINTERS
    return &objc_tag_classes[0x8 | obfuscatedTag];
#   else
    return &objc_tag_classes[(obfuscatedTag << 1) | 1];
#   endif
#endif
}
  • tag类标识,进行了一系列的位运算
  • 根据判断是macOS or iOS,来获取objc_tag_classes数组里面的类指针

生成Tagged Pointer指针

objectivec 复制代码
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
    return _objc_makeTaggedPointer_withObfuscator(tag, value, objc_debug_taggedpointer_obfuscator);
}

__attribute__((no_sanitize("unsigned-shift-base")))
static inline void * _Nonnull
_objc_makeTaggedPointer_withObfuscator(objc_tag_index_t tag, uintptr_t value,
                                       uintptr_t obfuscator)
{
    if (tag <= OBJC_TAG_Last60BitPayload) {
        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_withObfuscator(result, obfuscator);
    } else {
        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_withObfuscator(result, obfuscator);
    }
}
  • 根据负载内容位进行区分:传入的tag为类标识,同时也可以用于区分负载数据容量,苹果根据不同的负载数据容量对Tagged Pointer进行了不同的处理
  • 对传入objc_tag_index_t tag和value进行位运算:
    • 以NSNumber *a = @(1); 为例:
      1. tag 为OBJC_TAG_NSNumber(3) 二进制:0b011,16进制为 0x0000000000000003,负载数据容量 为 OBJC_TAG_Last60BitPayload
      2. value 为 数据数值(1) + 数据类型(int 为 2) 16进制为 0x0000000000000012
    • 在 iOS 下 源码中的宏定义:
      1. _OBJC_TAG_MASK :#define _OBJC_TAG_MASK (1UL<<63)
      2. _OBJC_TAG_INDEX_SHIFT:#define _OBJC_TAG_INDEX_SHIFT 60
      3. _OBJC_TAG_PAYLOAD_RSHIFT:#define _OBJC_TAG_PAYLOAD_RSHIFT 4
    • 对tag 和 value进行运算得到指针result:uintptr_t result = (_OBJC_TAG_MASK | ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) | ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT));
      1. (uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT):tag 为 0x0000000000000003 左移 _OBJC_TAG_INDEX_SHIFT(60) 得到十六进制: 0x3000000000000000
      2. ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT):value 为0x0000000000000012,位运算后为 0x0000000000000012
      3. result 为 _OBJC_TAG_MASK(1UL<<63) 和 0x3000000000000000 和 0x0000000000000012 进行 "或" 操作
      4. result 为 0xb000000000000012
  • 进行编码(数据混淆,数据保护):对result (0xb000000000000012)进行编码,我们看下:
cpp 复制代码
static inline void * _Nonnull
_objc_encodeTaggedPointer(uintptr_t ptr)
{
    return _objc_encodeTaggedPointer_withObfuscator(ptr, objc_debug_taggedpointer_obfuscator);
}

static inline void * _Nonnull
_objc_encodeTaggedPointer_withObfuscator(uintptr_t ptr, uintptr_t obfuscator)
{
    uintptr_t value = (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;
}

static inline uintptr_t
_objc_decodeTaggedPointer(const void * _Nullable ptr)
{
    return _objc_decodeTaggedPointer_withObfuscator(ptr, objc_debug_taggedpointer_obfuscator);
}

static inline uintptr_t
_objc_decodeTaggedPointer_withObfuscator(const void * _Nullable ptr,
                                         uintptr_t obfuscator)
{
    uintptr_t value
      = _objc_decodeTaggedPointer_noPermute_withObfuscator(ptr, obfuscator);
#if OBJC_SPLIT_TAGGED_POINTERS
    uintptr_t basicTag = (value >> _OBJC_TAG_INDEX_SHIFT) & _OBJC_TAG_INDEX_MASK;

    value &= ~(_OBJC_TAG_INDEX_MASK << _OBJC_TAG_INDEX_SHIFT);
    value |= _objc_obfuscatedTagToBasicTag(basicTag) << _OBJC_TAG_INDEX_SHIFT;
#endif
    return value;
}

无论是编码还是解码,都是对tagged pointersobjc_debug_taggedpointer_obfuscator来进行 "异或" 操作

相关题目

执行下面两段代码:

cpp 复制代码
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; ++i) {
    dispatch_async(queue, ^{
        person.name = [NSString stringWithFormat: @"abcdefghij"];
    });
}
cpp 复制代码
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
for (int i = 0; i < 1000; ++i) {
    dispatch_async(queue, ^{
        person.name = [NSString stringWithFormat: @"abcdefghi"];
    });
}

这两段代码的区别就是字符串长度少了一位,恰这个临界长度导致了字符串类型的不同:

第一段代码中
nameNSCFString类型

看第一段代码崩溃的地方,objc_release函数执行报错:

__NSCFString存储在堆上,它是个正常对象,需要维护引用计数的。name通过setter方法为其赋值。而setter方法的实现如下:

cpp 复制代码
- (void)setName:(NSString *)name {
    if(_name != name) {
        [_name release];
        _name = [name copy]; // or [name retain]
    }
}

异步并发执行setter方法,可能就会有多条线程同时执行[_name release]连续release两次就会造成对象的过度释放 ,导致Crash

解决方案:

  1. 使用atomic原子性关键字

    cpp 复制代码
    @property (atomic, copy)NSString* name;
  2. 加锁

    cpp 复制代码
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    for (int i = 0; i < 1000; ++i) {
        dispatch_async(queue, ^{
       	   // 加锁
           person.name = [NSString stringWithFormat: @"abcdefghij"];
           // 解锁
        });
    }

第二段代码中
name中是NSTaggedPointerString类型

objc_release函数中会判断指针是不是TaggedPointer类型,是的话就不对对象进行release操作,也就避免了因过度释放对象而导致的Crash,因为根本就没执行释放操作

cpp 复制代码
__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{
    if (!obj) return;
    if (obj->isTaggedPointer()) return;  // 直接返回,不会释放
    return obj->release();
}
相关推荐
Java小白笔记7 小时前
Mac中安装homebrew
macos
HerayChen10 小时前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
hairenjing112310 小时前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机
半桶水专家10 小时前
go语言中package详解
开发语言·golang·xcode
DisonTangor12 小时前
苹果发布iOS 18.2首个公测版:Siri接入ChatGPT、iPhone 16拍照按钮有用了
ios·chatgpt·iphone
- 羊羊不超越 -12 小时前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
小李飞刀李寻欢12 小时前
Mac电脑如何解压rar压缩包
macos·rar·解压
Java小白笔记12 小时前
Mac中禁用系统更新
macos
AndyFrank12 小时前
mac crontab 不能使用问题简记
linux·运维·macos
Mac新人12 小时前
一招解决Mac没有剪切板历史记录的问题
macos·mac