前言
在 2013 年 9 月,苹果推出了 iPhone5s,与此同时,iPhone5s 配备了首个采用 64 位架构的 A7 双核处理器,在这之前通常创建对象,对象存储在堆中,对象的指针存储在栈中。要找到这个对象,就需要先在栈中,找到指针,然后通过指针找到堆中的对象。一个普通的 iOS 程序,从 32 位机器迁移到 64 位机器中后,虽然逻辑没有任何变化,但这种 NSNumber
、NSDate
一类的对象所占用的内存会翻倍。如下图所示:
为了存储和访问一个 NSNumber
对象,需要在堆上为其分配内存,另外还要维护它的引用计数,管理它的生命期。这些都给程序增加了额外的逻辑,造成运行效率上的损失。
什么是 Tagged Pointer
为了改进上面提到的内存占用和效率问题,苹果提出了 Tagged Pointer 对象。由于 NSNumber
、NSDate
一类的变量本身的值需要占用的内存大小常常不需要 8 个字节,拿整数来说,4 个字节所能表示的有符号整数就可以达到 20 多亿(注:,另外 1 位作为符号位),对于绝大多数情况都是可以处理的。
所以 Tagged Pointer 就是将一个对象的指针拆成两部分,一部分直接保存数据,另一部分作为特殊标记,表示这是一个特别的指针,不指向任何一个地址。所以,引入了 Tagged Pointer 对象之后,64 位 CPU 下 NSNumber
的内存图变成了以下这样:
Tagged Pointer 代码
对此,可以通过实验代码验证:
NSNumber *number1 = @1;
NSNumber *number2 = @2;
NSNumber *number3 = @3;
NSNumber *numberFFFF = @(0xFFFF);
NSString *string1 = [NSString stringWithFormat:@"1"];
NSLog(@"number1 pointer is %p", number1);
NSLog(@"number2 pointer is %p", number2);
NSLog(@"number3 pointer is %p", number3);
NSLog(@"numberffff pointer is %p", numberFFFF);
NSLog(@"string1 pointer is %p", string1);
NSNumber *bigNumber = @(0xEFFFFFFFFFFFFFFF);
NSString *bigString = [NSString stringWithFormat:@"12345678901"] ;
NSLog(@"bigNumber pointer is %p", bigNumber);
NSLog(@"bigString pointer is %p", bigString);
输出结果如下:
number1 pointer is 0xf49bc6f4ee1ca35e
number2 pointer is 0xf49bc6f4ee1ca36e
number3 pointer is 0xf49bc6f4ee1ca37e
numberffff pointer is 0xf49bc6f4ee135cbe
string1 pointer is 0xe49bc6f4ee1ca05d
bigNumber pointer is 0x6000000bb140
bigString pointer is 0x6000000bb160
可以发现,Tagged Pointer 的指针地址与大容量数据的指针地址不同。
Tagged Pointer 混淆
在 objc 源码中可以找到下面这样一段代码在 iOS 12.0 以后,用于混淆 TaggedPointer:
static void
initializeTaggedPointerObfuscator(void)
{
if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
// Set the obfuscator to zero for apps linked against older SDKs,
// in case they're relying on the tagged pointer representation.
DisableTaggedPointerObfuscation) {
objc_debug_taggedpointer_obfuscator = 0;
} else {
// Pull random data into the variable, then shift away all non-payload bits.
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}
通过 objc_debug_taggedpointer_obfuscator
查看底层是如何混淆处理的
-
编码
static inline void *_Nonnull _objc_encodeTaggedPointer(uintptr_t ptr) { return (void*)(objc_debug_taggedpointer_obfuscator ^ ptr); }
-
解码
static inline uintptr_t _objc_decodeTaggedPointer(const void *_Nullable ptr) { return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator; }
Tagged Pointer 指针信息
在 TaggedPointer 指针中,**最高位为标志位,61 - 63 位为类型信息,中间存储着实际信息,最后一位为标志位。
在
NSTaggedPointerString
类型中最后 4 位存储字符串长度**,字符串存储的是ascii
值
通过 _objc_decodeTaggedPointer
函数对 TaggedPointer 进行解码,可以得到实际的指针地址进行查看:
extern uintptr_t objc_debug_taggedpointer_obfuscator;
uintptr_t
_objc_decodeTaggedPointer_(id ptr)
{
return (uintptr_t)ptr ^ objc_debug_taggedpointer_obfuscator;
}
int main(){
NSString *string1 = [NSString stringWithFormat:@"1"];
NSNumber *number1 = @1;
NSLog(@"really string1 pointer is 0x%lx", _objc_decodeTaggedPointer_(string1));
NSLog(@"really number1 pointer is 0x%lx", _objc_decodeTaggedPointer_(number1));
}
输出结构如下:
really string1 pointer is 0xa000000000000311
really number1 pointer is 0xb000000000000012
通过上面的输出结果,我们会发现,字符串与数字的指针二进制前四位不一致,16 进制分别为 0xa
与 0xb
,转换为二进制分别为 1010
与 1011
。
TaggedPointer 标志位
在 objc 源码中搜索 TaggedPointer,可以找到下面这样一段代码用于判断是否为 TaggedPointer:
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
判断一个对象类型是否为 TaggedPointer 类型实际上是将指针地址与 _OBJC_TAG_MASK
进行按位与操作,结果在跟 _OBJC_TAG_MASK
进行对比,在看下_OBJC_TAG_MASK
的定义:
# if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
# else
# define _OBJC_TAG_MASK 1UL
# endif
_OBJC_TAG_MASK
表明如果 64位数据中,最高位是 1 的话,则表明当前是一个 tagged pointer
类型。
注意:TaggedPointer类型在iOS和MacOS中标志位是不同的iOS为最高位而MacOS为最低位
Tagged Pointer 类型
可以在 objc 源码中找到类型枚举 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_First60BitPayload = 0,
OBJC_TAG_Last60BitPayload = 6,
OBJC_TAG_First52BitPayload = 8,
OBJC_TAG_Last52BitPayload = 263,
OBJC_TAG_RESERVED_264 = 264
};
Tagged Pointer 特点
苹果对于 TaggedPointer 特点的介绍:
-
TaggedPointer 专门用来存储小的对象,例如
NSNumber
和NSDate
- TaggedPointer 指针的值不再是地址了,而是真正的值。所以,实际上它不再是一个对象了,它只是一个披着对象皮的普通变量而已。所以,它的内存并不存储在堆中,也不需要 malloc 和 free,也就不需要引用计数。
- 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍。
由此可见,苹果引入 TaggedPointer,不但减少了 64 位机器下程序的内存占用,还提高了运行效率。完美地解决了小内存对象在存储和访问效率上的问题。
补充: TaggedPointer 不是真正的对象,因此没有
isa
网友评论