目录
- 前言
- 什么是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_OBFUSCATION
为YES
-
进入
Edit Scheme
页面:
-
添加环境变量
OBJC_DISABLE_TAG_OBFUSCATION
并将其设置为YES
: -
添加
OBJC_DISABLE_TAGGED_POINTERS
并设置为YES
,会禁用Tagged Pointer
MacOS分析
NSNumber
MacOS
下NSNumber
的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
,最后一位1
为Tagged 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
MacOS
下NSString
的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类型:
类型 | 描述 |
---|---|
__NSCFConstantString | 1. 常量字符串,存储在字符串常量区,继承于 __NSCFString。相同内容的 __NSCFConstantString 对象的地址相同,也就是说常量字符串对象是一种单例,可以通过 == 判断字符串内容是否相同。2. 这种对象一般通过字面值@"..." 创建。如果使用 __NSCFConstantString 来初始化一个字符串,那么这个字符串也是相同的 __NSCFConstantString。 |
__NSCFString | 1. 存储在堆区,需要维护其引用计数,继承于 NSMutableString。2. 通过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的有d
、f
、string1
、string2
指针,它们的指针值分别为
0x6115
、0x6615
、0x6766656463626175
、0x880e28045a54195
:
- 其中
0x61
、0x66
、0x67666564636261
分别对应字符串的ASCII码 - 最后一位
5
的二进制为0101
,最后一位1
是代表这个指针是Tagged Pointer,010
对应十进制为2
,表示NSString类 - 倒数第二位
1
、1
、7
、9
代表字符串长度
对于string2
的指针值0x880e28045a54195
,虽然从指针中看不出来字符串的值,但其也是一个Tagged Pointer
iOS分析
iOS下则采用 MSB(Most Significant Bit,即最高有效位)为Tagged Pointer标识位
iOS
下NSNumber
的Tagged Pointer位视图:
iOS
下NSString
的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<<63
,x86 64
架构且MacOS系统下的掩码为1UL
,其他情况下的掩码为1UL<<63
简言之,iOS
和arm64
下采用最高有效位为Tagged Pointer的标识位,MacOS
的x86 64
下采用最低有效位为Tagged Pointer的标识位
而存储在堆空间的对象由于内存对齐(16的倍数),它的内存地址的最低有效位为0
。由此可以辨别Tagged Pointer和一般对象指针:
Tagged Pointer应用
objc_msgSend
能识别Tagged Pointer,比如NSNumber
的intValue
方法,直接从指针提取数据,不会进行objc_msgSend
的三大流程,节省了调用开销(消息转发、寻找isa
指向等)
内存管理相关的,如retain
方法中调用的rootRetain
:
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()
函数实现:
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.
**********************************************************************/
翻译:
-
Tagged pointer 指针对象将class和对象数据存储在对象指针中,指针实际上不指向任何东西。
-
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 负载数据容量,(存储对象数据)
-
在这些表示中,某些体系结构反转了MSB和LSB
从注释中我们得知:
- Tagged pointer存储对象数据目前分为
60bits
负载容量和52bits
负载容量。 - 类标识允许使用扩展形式
那么如何判断负载容量?类标识的扩展类型有那些?我们来看下全面的objc_tag_index_t
源码:
{
// 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
};
小结:
-
区分什么位置为负载内容位
MacOS下采用 LSB 即OBJC_TAG_First60BitPayload、OBJC_TAG_First52BitPayload。
iOS下则采用 MSB 即OBJC_TAG_Last60BitPayload、OBJC_TAG_Last52BitPayload。 -
区分负载数据容量
当类标识为0-6时,负载数据容量为60bits。
当类标识为7时(对应二进制为 0b111),负载数据容量为52bits。 -
类标识的扩展类型有哪些?
如果tag index 是 0b111(7), tagged pointer对象使用 “扩展” 表示形式
类标识的扩展类型为上面OBJC_TAG_Photos_1 ~OBJC_TAG_NSIndexSet。 -
类标识与负载数据容量对应关系
当类标识为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_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()
函数:
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()
:
// 返回指向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
数组里的结果返回
这里有两个重要的全局数组:
#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()
函数:
// 返回指向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指针
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); 为例:
- tag 为OBJC_TAG_NSNumber(3) 二进制:0b011,16进制为 0x0000000000000003,负载数据容量 为 OBJC_TAG_Last60BitPayload
- value 为 数据数值(1) + 数据类型(int 为 2) 16进制为 0x0000000000000012
- 在 iOS 下 源码中的宏定义:
- _OBJC_TAG_MASK :#define _OBJC_TAG_MASK (1UL<<63)
- _OBJC_TAG_INDEX_SHIFT:#define _OBJC_TAG_INDEX_SHIFT 60
- _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));
- (uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT):tag 为 0x0000000000000003 左移 _OBJC_TAG_INDEX_SHIFT(60) 得到十六进制: 0x3000000000000000
- ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >> _OBJC_TAG_PAYLOAD_LSHIFT):value 为0x0000000000000012,位运算后为 0x0000000000000012
- result 为 _OBJC_TAG_MASK(1UL<<63) 和 0x3000000000000000 和 0x0000000000000012 进行 “或” 操作
- result 为 0xb000000000000012
- 以NSNumber *a = @(1); 为例:
- 进行编码(数据混淆,数据保护):对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_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 pointers
与objc_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"];
});
}
这两段代码的区别就是字符串长度少了一位,恰这个临界长度导致了字符串类型的不同:
第一段代码中
name
是NSCFString
类型
看第一段代码崩溃的地方,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
解决方案:
-
使用
atomic
原子性关键字@property (atomic, copy)NSString* name;
-
加锁
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();
}