【iOS】Tagged Pointer

目录

    • 前言
    • 什么是Tagged Pointer?
      • 引入Tagged Pointer技术之前
      • 引入Tagged Pointer之后
      • 总结
    • Tagged Pointer原理(Tag+Data分析)
      • 关闭数据混淆
      • MacOS分析
        • NSNumber
        • NSString
      • iOS分析
    • 判断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应该是无指向的:

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

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

    // 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变量地址:

// 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类型:

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

打印结果分析:

NSString对象类型分析
a__NSCFConstantString通过字面量@"..."创建
b__NSCFStringa 的深拷贝,指向不同的内存地址,被拷贝到堆区
c__NSCFConstantStringa 的浅拷贝,指向同一块内存地址
dNSTaggedPointerString单独对 a 进行 copy(如 c),浅拷贝是指向同一块内存地址,所以不会产生Tagged Pointer;单独对 a 进行 mutableCopy(如 b),复制出来是可变对象,内容大小可以扩展;而Tagged Pointer存储的内容大小有限,因此无法满足可变对象的存储要求。
e__NSCFConstantString使用 __NSCFConstantString 来初始化的字符串
fNSTaggedPointerString通过stringWithFormat:方法创建,指针足够存储字符串的值。
string1NSTaggedPointerString通过stringWithFormat:方法创建,指针足够存储字符串的值。
string2NSTaggedPointerString通过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的标识位

查以下函数源码:

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

ALWAYS_INLINE id 
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{// 如果是 tagged pointer,直接返回 thisif (isTaggedPointer()) return (id)this; bool sideTableLocked = false;bool transcribeToSideTable = false; isa_t oldisa;isa_t newisa;......

isTaggedPointer()函数实现:

inline bool 
objc_object::isTaggedPointer() 
{return _objc_isTaggedPointer(this);
}

Tagged Pointer 注意点

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

在这里插入图片描述

Tagged Pointer注释

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

/***********************************************************************
* 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源码:

{// 60-bit payloadsOBJC_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 payloadsOBJC_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 setOBJC_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()函数:

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()函数:

OBJC_EXPORT uintptr_t objc_debug_taggedpointer_obfuscatorOBJC_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,// 防止他们正依赖TaggedPointerobjc_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()函数:

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 == 7Class *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()

// 返回指向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_POINTERSif (tag >= OBJC_TAG_FirstUnobfuscatedSplitTag)return &objc_tag_ext_classes[index];
#endifuintptr_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数组里的结果返回

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

#if SUPPORT_TAGGED_POINTERSextern "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()函数:

// 返回指向tag类数组中类存储的指针
// 假定该tag是一个有效的基本tag
static ptrauth_taggedpointer_table_entry Class *
classSlotForBasicTagIndex(objc_tag_index_t tag)
{
#if OBJC_SPLIT_TAGGED_POINTERSuintptr_t obfuscatedTag = _objc_basicTagToObfuscatedTag(tag);return &objc_tag_classes[obfuscatedTag];
#elseuintptr_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_POINTERSreturn &objc_tag_classes[0x8 | obfuscatedTag];
#   elsereturn &objc_tag_classes[(obfuscatedTag << 1) | 1];
#   endif
#endif
}
  • tag类标识,进行了一系列的位运算
  • 根据判断是macOS or iOS,来获取objc_tag_classes数组里面的类指针

生成Tagged Pointer指针

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)进行编码,我们看下:
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_POINTERSif ((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;
#endifreturn (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_POINTERSuintptr_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;
#endifreturn value;
}

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

相关题目

执行下面两段代码:

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"];});
}
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方法的实现如下:

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

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

解决方案:

  1. 使用atomic原子性关键字

    @property (atomic, copy)NSString* name;
    
  2. 加锁

    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,因为根本就没执行释放操作

__attribute__((aligned(16), flatten, noinline))
void 
objc_release(id obj)
{if (!obj) return;if (obj->isTaggedPointer()) return;  // 直接返回,不会释放return obj->release();
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/382669.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

科研绘图系列:R语言微生物堆积图(stacked barplot)

介绍 堆叠条形图是一种数据可视化图表,它通过将每个条形分割成多个部分来展示不同类别的数值。每个条形代表一个总体数据,而条形内的每个部分则代表该总体数据中不同子类别的数值。这种图表特别适合展示整体与部分的关系,以及各部分在整体中的比例。 特点: 多部分条形:每…

Python爬虫:代理ip电商数据实战

引言&#xff1a;数据访问管理引发的烦恼 作为一名Python博主&#xff0c;爬虫技能对于获取和分析数据至关重要&#xff0c;经常爬一下&#xff0c;有益身心健康嘛。爬虫技术对很多人来说&#xff0c;不仅仅是一种工具&#xff0c;更像是一种艺术&#xff0c;帮助我们从互联网…

工程视角:数据结构驱动的应用开发--字典(dictionary),列表(list)与实体

这里写目录标题 业务业务场景流程分析 实现数据访问层&#xff08;DAL&#xff09;业务逻辑层&#xff08;BLL&#xff09;用户界面层&#xff08;UI&#xff09;工具类 设计思路为什么抽出工具类关于U层使用字典的好处工程视角 业务 业务场景 在一个金融应用系统中&#xff0c…

【益智问题】十三球称重

十三球称重 问题描述解决方案 问题描述 有13个外表一模一样的小球产品&#xff0c;其中有一个是次品&#xff0c;其质量与其他12个略有差别&#xff0c;但不知道是重还是轻。这13个球上都有标号&#xff0c;现在给你-架天平&#xff0c;只能使用三次&#xff0c;把这个次品小球…

Elasticsearch:Golang ECS 日志记录 - zap

ECS 记录器是你最喜欢的日志库的格式化程序/编码器插件。它们可让你轻松地将日志格式化为与 ECS 兼容的 JSON。 编码器以 JSON 格式记录日志&#xff0c;并在可能的情况下依赖默认的 zapcore/json_encoder。它还处理 ECS 错误格式的错误字段记录。 默认情况下&#xff0c;会添…

58、主从复制数据库+读写分离

mysql的主从复制和读写分离&#xff08;面试问原理&#xff09; mysql的主从复制和读写分离&#xff1a; 主从复制 面试必问 主从复制的原理。 读写分离&#xff0c;MHA 一、主从复制 1.1、主从复制的模式&#xff1a; 1、mysql的默认模式&#xff1a; 异步模式&#xf…

Three.js 官方文档学习笔记

Address&#xff1a;Three.js中文网 (webgl3d.cn) Author&#xff1a;方越 50041588 Date&#xff1a;2024-07-19 第一个3D案例—创建3D场景 创建3D场景对象Scene&#xff1a; const scene new THREE.Scene(); 创建一个长方体几何对象Geometry&#xff1a; const geomet…

FTP、NFS、SAMBA系统服务

⼀、rsync托管xinetd 1 、为什么要进⾏服务托管 独⽴服务&#xff1a;独⽴启动脚本 ssh ftp nfs dns ... 依赖服务 : 没有独⽴的启动脚本 rsync telnet 依赖 xinetd 服务&#xff08;独⽴服务&#xff09; 2 、如何将 rsync 托管给 xinetd 服务去管理&#xff1f; 第⼀步&am…

VMware中Ubuntu磁盘空间的清理

最近发现Ubuntu占用空间过大&#xff0c;在网上找了一些方法&#xff0c;在这里总结一下。 1.删除快照 把不需要的快照删除&#xff0c;但要注意删除快照可能会影响到后续的快照链。每个快照依赖于前面的快照。如果删除一个中间快照&#xff0c;虚拟机可能无法找到完整的差异…

leetcode 513.找树左下角的值

1.题目要求: 代码块: 给定一个二叉树的 根节点 root&#xff0c;请找出该二叉树的 最底层 最左边 节点的值。假设二叉树中至少有一个节点。2.此题思路: 1.创建队列&#xff0c;出队函数和入队函数: //创建队列 typedef struct queuet{struct TreeNode* value;struct queue* n…

IB user verbs介绍

本文来自对内核源代码文档/Documentation/infiniband/user_verbs.rst的翻译和理解。 在Infiniband设备帮助下&#xff0c;跨计算机的两个进程可以相互访问对方的虚地址空间。在Linux操作系统上&#xff0c;支持进程能直接访问本地Infiniband设备的资源&#xff0c;从而实现跨机…

TeamViewer手机端APP提示:请先验证账户

当你在手机端下载安装了TeamViewerAPP后&#xff0c;需要你先登录个人账号&#xff0c;然后还会要求你验证账户&#xff0c;同时跳转到一个网址中&#xff0c;但是这个网址并没有自动跳转到验证账户的位置。 解决办法&#xff1a; 在手机浏览器中进入下面这个网址&#xff1a;…

鸿蒙语言基础类库:【@system.vibrator (振动)】

振动 说明&#xff1a; 本模块首批接口从API version 4开始支持。后续版本的新增接口&#xff0c;采用上角标单独标记接口的起始版本。从API Version 8开始&#xff0c;该接口不再维护&#xff0c;推荐使用新接口[ohos.vibrator]。该功能使用需要对应硬件支持&#xff0c;仅支持…

VMware虚拟机无法访问互联网

一、什么情况下需要访问互联网 在某些情况下&#xff0c;VMware虚拟机需要通过访问互联网去获取外部资源&#xff0c;例如进行yum安装或者wget拉取操作等等。 二、处理方法 2.1.检查虚拟机网络模式和服务状态 虚拟机的网络模式需要使用 NAT 模式。 VMware NAT Service服务…

Delphi 11.2 配置Android SDK 环境

打开 Delphi 11 点击 Tools–Options… 然后点击 Deployment–SDK Manager–Add… 这里如果配置64位就选 Android 64-bit&#xff0c;如果配置32位就选 Android 32-bit 点击 Select an SDK version–Add New… 有警告图标的就是有问题的项&#xff0c;需要手动更新一下&#xf…

微信小程序开发:项目程序代码构成

✨✨ 欢迎大家来访Srlua的博文&#xff08;づ&#xffe3;3&#xffe3;&#xff09;づ╭❤&#xff5e;✨✨ &#x1f31f;&#x1f31f; 欢迎各位亲爱的读者&#xff0c;感谢你们抽出宝贵的时间来阅读我的文章。 我是Srlua小谢&#xff0c;在这里我会分享我的知识和经验。&am…

PostgreSQL创建表和自增序列

一、创建表&#xff1a; 注意&#xff1a; 1、在mysql没有序列的概念&#xff0c;id自增通过auto_increment实现&#xff1b; 2、pgsql没有auto_increment的概念&#xff0c;如何实现id自增&#xff1f;有两种方式&#xff1a; 方式一&#xff1a;创建序列&#xff0c;绑定…

Android lmkd机制详解

目录 一、lmkd介绍 二、lmkd实现原理 2.1 工作原理图 2.2 初始化 2.3 oom_adj获取 2.4 监听psi事件及处理 2.5 进程选取与查杀 2.5.1 进程选取 2.5.2 进程查杀 三、关键系统属性 四、核心数据结构 五、代码时序 一、lmkd介绍 Android lmkd采用epoll方式监听linux内…

SpringBoot连接PostgreSQL+MybatisPlus入门案例

项目结构 一、Java代码 pom.xml <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://mave…

客户案例 | 识货基于向量检索服务 Milvus 版搭建电商领域的向量数据检索平台

阿里云的Milvus服务以其性能稳定和功能多样化的向量检索能力&#xff0c;为识货团队在电商领域的向量检索场景中搭建业务系统提供了强有力的支持。该服务的分布式扩展能力不仅可靠&#xff0c;而且能够适应日益增长的数据规模。 一、客户介绍 识货&#xff0c;成立于2012年6月…