iPhone5s是首个采用 64 位架构的A7双核处理器的手机,为了节省内存和提高执行效率,苹果提出了Tagged Pointer的概念。对于 64 位程序,引入 Tagged Pointer 后,相关逻辑能减少一半以上的内存占用,以及3倍的访问速度提升,100 倍的创建、销毁速度提升。
本文将带我们来理解这个概念 是怎么节省内存和提高执行效率的。(注:本篇文章所用系统 皆为64位系统)
以NSNumber *a = @(1);为例,在不使用Tagged Pointer的情况下,我们看下在内存上和访问效率上都是什么情况。
在内存上:
如下图所示, 1个小对象 需要至少使用24字节(指针8字节 + 对象16字节 )
栈:在栈上,占1个指针 8字节,里面存储的是堆内存的地址0x600001a92920。
堆:在堆上,占16个字节,isa指针占8个字节,1为int类型,占4个字节,但由于内存对齐机制(ios 内存对齐 为16字节),堆需要16个字节的内存。
在效率上:
NSNumber对象需要动态分配内存、维护引用计数、管理它的生命周期等
方法调用 需要objc_msgSend的执行流程(消息发送、动态方法解析、消息转发)
苹果对 Tagged Pointer 的介绍主要有三点:
Tagged Pointer 实质是一个伪指针,对象的指针中存储的数据变成了 Tag+Data 形式:
在内存上:只占一个指针的大小 8 字节,节省了很多内存开销;
在效率上:objc_msgSend 先识别 是否为 Tagged Pointer,若是,直接返回 不进行其他流程;若不是,进行其他流程(消息发送、动态方法解析、消息转发) 。从而节省了调用开销。
一般我们在存放 NsNumber 和 NSDate 这一类变量的时候本身占用的内存大小常常不需要 8 个字节。4 字节带符号的整数可以达到 2^31=2147483648,99.999% 的情况都能满足了。所以大部分都可以用 Tagged Pointer 类型,不满足的则申请堆内存。
设置环境变量 OBJC_DISABLE_TAG_OBFUSCATION 为 YES, 为关闭 Tagged Pointer 的数据混淆;
设置环境变量 OBJC_DISABLE_TAGGED_POINTERS 为 YES, 来禁用 Tagged Pointer(目前不生效)在以前的版本,设置 OBJC_DISABLE_TAGGED_POINTERS 为 YES 会导致程序崩溃,是 runtime 中进行了判断,调用 _objc_fatal() 导致的程序崩溃。
// 关闭 Tagged Pointer 数据混淆前(混淆为对于数据的保护)
NSNumber *number1 = @(1); // number1: 0x9a90d53a8ebc20bb
NSNumber *number2 = @(2); // number2: 0x9a90d53a8ebc208d
NSNumber *number3 = @(3); // number3: 0x9a90d53a8ebc209c
NSNumber *numberFFFF = @(0xFFFFFFFFFFFFFFFF); // numberFFFF: 0x600000aa0b80
// 关闭 Tagged Pointer 数据混淆后
NSNumber *number1 = @(1); // number1: 0xb000000000000012
NSNumber *number2 = @(2); // number2: 0xb000000000000022
NSNumber *number3 = @(3); // number3: 0xb000000000000032
NSNumber *numberFFFF = @(0xFFFFFFFFFFFFFFFF); // numberFFFF: 0x6000032d9560
我们设置环境变量 OBJC_DISABLE_TAG_OBFUSCATION 为 YES,关闭了数据混淆可以看出:number1 的内存为 0xb000000000000012、number2 的内存为 0xb000000000000022、number3 的内存为 0xb000000000000032。并且 number1 的值为 1、number2 的值为 2、number3 的值为 3。
通过观察发现,对象的值 1、2、3 都存储在了对应的指针中,对应 0xb000000000000012 中的 1、0xb000000000000022 中的 2、0xb000000000000032 中的 3。(混淆为苹果对于数据的保护)而 numberFFFF 的值 0xFFFFFFFFFFFFFFFF,由于数据过大,导致无法 1 个指针 8 个字节的内存根本存不下,而申请了堆内存。
我们都知道所有的 oc 对象都有 isa 指针,那么判断一个指针是否是伪指针最重要的证据是其 isa 指针了,我们看下他们对应的 isa 指针,如下图:
由上图我们可以看出,number1、number2、number3 指针为 Tagged Pointer 类型,为伪指针,isa 指针为 nil。numberFFFF 的 isa 指针真实存在,在堆内存中分配了空间,不是 Tagged Pointer 类型。
以上例子从内存值 和 isa 两方面来验证了 Tagged Pointer 的定义,结合例子我们做下总结:
Tagged Pointer 为 Tag+Data 形式,其中 Data 为内存地址中的 1、2、3 (红色),为存储对应着对象的值。(例:0xb000000000000012 中的 1)
但是内存地址: 0xb000000000000012 中对应的“b” 和 “2”,代表什么?
我们先看结果,再分析。
以上面例子中的 0xb000000000000012 为例,指针中的 b 代表什么?
b 的二进制为 1011,其中第一位 1 是 Tagged Pointer 标识位,代表这个指针是 Tagged Pointer;后面的 011 是类标识位,对应十进制为 3,表示 NSNumber 类。
指针中的 2 代表什么?
2 代表数据类型(NSNumber 为 short、 int、 long 、 float 、 double 等。NSString 为 string 长度)。
以 iOS 中 NSNumber 为例,我们看下图按照位域操作,Tag 和 Data 分别显示在什么位置、代表什么。
Tagged Pointer 的 Tag 标记,为最高 4 位。其余为 NSNumber 数据。下面会分别对标识位、类标识、数据类型做代码验证。
如何判断为 Tagged Pointer?
在源码 objc_internal.h 中可以找到判断 Tagged Pointer 标识位的方法,如下代码:
static inline bool
_objc_isTaggedPointer(const void * _Nullable ptr)
{
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
将一个指针与 _OBJC_TAG_MASK 掩码 进行按位与操作。这个掩码 _OBJC_TAG_MASK 的源码同样在 objc_internal.h 中可以找到:
#if (TARGET_OS_OSX || TARGET_OS_IOSMAC) && __x86_64__
// 64-bit Mac - tag bit is LSB
# define OBJC_MSB_TAGGED_POINTERS 0
#else
// Everything else - tag bit is MSB
# define OBJC_MSB_TAGGED_POINTERS 1
#endif
#if OBJC_MSB_TAGGED_POINTERS
# define _OBJC_TAG_MASK (1UL<<63)
#else
# define _OBJC_TAG_MASK 1UL
#endif
根据源码得知:
MacOS下采用 LSB(Least Significant Bit,即最低有效位)为 Tagged Pointer 标识位;(define _OBJC_TAG_MASK 1UL)
iOS下则采用 MSB(Most Significant Bit,即最高有效位)为 Tagged Pointer 标识位。(define _OBJC_TAG_MASK (1UL<<63))< span="">
如下图,以 NSNumber 为例:
在 iOS 中,1 个指针 8 个字节,64 位,最高位为 1,则为 Tagged Pointer。
同理在上面 4.3.1 Tag 解析结果一节中,以 0xb000000000000012 为例:
0xb000000000000012 为 16 进制指针中的最高位 b 的二进制为 1011,最高位为 1,则代表这个指针是 Tagged Pointer。
且 _objc_isTaggedPointer 判断 Tagged Pointer 标识位是处处优先判断的。如下面源码(下面源码只展示相关部分)所示:
ALWAYS_INLINE id
objc_object::rootRetain(bool tryRetain, bool handleOverflow)
{
if (isTaggedPointer()) return (id)this;
}
ALWAYS_INLINE bool
objc_object::rootRelease(bool performDealloc, bool handleUnderflow)
{
if (isTaggedPointer()) return false;
}
inline bool
objc_object::isTaggedPointer()
{
return _objc_isTaggedPointer(this);
}
在源码 objc_object.h 中可以找到的 objc_object::rootRetain 方法,该方法为引用计数 +1 的方法,在这个方法中,优先判断是否是 Tagged Pointer,Tagged Pointer 为伪指针,不需要记录引用计数。
在源码 objc_object.h 中可以找到的 objc_object::rootRelease 方法,该方法为引用计数 -1 的方法,在这个方法中,优先判断是否是 Tagged Pointer,Tagged Pointer 为伪指针,不需要记录引用计数。
objc_msgSend 为汇编代码,但其实里面也优先做了 Tagged Pointer 标识位判断。如果不是 Tagged Pointer 则进行消息转发等流程。
Tagged Pointer 的判断是如此的简单,只是二进制的与运算。
从苹果官方介绍来看, Tagged Pointer 被设计的目的是用来存储较小的对象,例如 NSNumber、NSDate、NSString 等;那么 Tagged Pointer 只是一个伪指针,一个 64 位的二进制,如何来区分是 NSNumber 呢?还是 NSString 等呢?
在源码 objc_internal.h 中可以查看到 NSNumber、NSDate、NSString 等类的标识位,这里只展示我们关心的类型,全面的在 4.4 里有介绍。
{
// 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,
// 保留位
OBJC_TAG_RESERVED_7 = 7,
。。。
}
下面让我们举例验证,不同的类型,输出一下看看地址:
// number1: 0xb000000000000012
NSNumber *number1 = @(1);
// string: 0xa000000000000611
NSString *string = [NSString stringWithFormat:@"a"];
根据输出我们可以看到:
NSNumber 指针 0xb000000000000012,b 的二进制为 1011,后面的 011 是类标识位,对应十进制为 3,表示 NSNumber 类;
NSString 指针 0xa000000000000611, a 的二进制为 1010,后面的 010 是类标识位,对应十进制为 2,表示 NSString 类。
如图,类标识位置如下:
我们知道了以 NSNumber 为例的地址 0xb000000000000012 的数据数值、Tagged Pointer 标识位、Tagged Pointer 类标识。那么最后一位 2 代表的是什么呢?
16 进制的最后一位(即 2 进制的最后四位)表示数据类型。同样我们举例验证:
char a = 1;
short b = 1;
int c = 1;
long d = 1;
float e = 1.0;
double f = 1.00;
NSNumber *number1 = @(a); // 0xb000000000000010
NSNumber *number2 = @(b); // 0xb000000000000011
NSNumber *number3 = @(c); // 0xb000000000000012
NSNumber *number4 = @(d); // 0xb000000000000013
NSNumber *number5 = @(e); // 0xb000000000000014
NSNumber *number6 = @(f); // 0xb000000000000015
可以看到,我们都用 NSNumber 类,用不同数据类型做测试,内存地址 16 进制只有最后一位发生了变化。其对应的数据类型分别为:
数据类型 | 内存地址 二进制 最后四位 |
---|---|
char | 0 |
short | 1 |
int | 2 |
long | 3 |
float | 4 |
double | 5 |
NSString、NSDate 的二进制最后四位 都是数据类型么?你可以自己去验证一下~
如图,数据类型位置如下:
至此我们就把 Tagged Pointer 实质 Tag+Data 完整地解析了一遍。
在源码 objc-runtime-new.mm 中有一段注释对 Tagged pointer objects 进行了解释,原文如下:
/***********************************************************************
* 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.
**********************************************************************/
对应注释翻译:
从注释中我们得知:
那么如何判断负载容量?类标识的扩展类型有那些?我们来看下全面的 objc_tag_index_t 源码:
// 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,
// 保留位
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,
// 前 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 的实质 Tag+Data,知道了 Tag 对应什么,Data 对应什么。那么为什么 NSNumber、NSDate、NSString 会转成为伪指针呢?其他的为什么不会呢?NSNumber、NSDate、NSString 是如何生成 Tagged Pointer 的?下面让我们继续探索 Tagged Pointer。
在 _read_images() 方法中,有两处关键代码如下:
void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses)
{
if (DisableTaggedPointers) {
disableTaggedPointers();
}
initializeTaggedPointerObfuscator();
}
上面方法主要分两部分:
static void
initializeTaggedPointerObfuscator(void)
{
if (sdkIsOlderThan(10_14, 12_0, 12_0, 5_0, 3_0) ||
DisableTaggedPointerObfuscation) {
objc_debug_taggedpointer_obfuscator = 0;
} else {
arc4random_buf(&objc_debug_taggedpointer_obfuscator,
sizeof(objc_debug_taggedpointer_obfuscator));
objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;
}
}
上面方法主要分三部分:
为什么 NSNumber、NSDate、NSString 会转成为伪指针呢?其他的为什么不会呢?
加载程序时,从 dyld 库的 _dyld_start() 函数开始,经历了多般步骤,开始调用 _objc_registerTaggedPointerClass() 函数。下面我们来看下在源码 objc-runtime-new.mm 中该方法的实现:
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());
}
*slot = cls;
if (tag < OBJC_TAG_First60BitPayload || tag > OBJC_TAG_Last60BitPayload) {
Class *extSlot = classSlotForBasicTagIndex(OBJC_TAG_RESERVED_7);
if (*extSlot == nil) {
extern objc_class OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
*extSlot = (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
}
}
}
方法主要分为以下三部分:
其实这个方法 起的名字是注册,在我看来,应该叫校验。校验在全局数组(以 tag 进行位操作 为索引,类为 value,的全局数组)中,用 tag 取出来的类指针 与 注册的类是否相符。
这里我们主要关注下 _objc_registerTaggedPointerClass() 方法的精髓第二点、根据指定 tag 获取类指针。我们看下 classSlotForTagIndex 的源码实现:
static 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;
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;
}
以上方法主要分为三部分:
这里有两个重要的全局数组:
#if SUPPORT_TAGGED_POINTERS
extern "C" {
extern Class objc_debug_taggedpointer_classes[_OBJC_TAG_SLOT_COUNT];
extern 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 的源码:
static Class *
classSlotForBasicTagIndex(objc_tag_index_t tag)
{
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
}
以上方法主要分为以下两个部分:
static inline void * _Nonnull
_objc_makeTaggedPointer(objc_tag_index_t tag, uintptr_t value)
{
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(result);
} 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(result);
}
}
方法主要分为以下三部分:
传入的 tag 为类标识,同时也可以用于区分负载数据容量,苹果根据不同的负载数据容量对 Tagged Pointer 进行了不同的处理。
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 pointers 与 objc_debug_taggedpointer_obfuscator 来进行 “异或” 操作。
源码里面还有很多别的方法,例如取 Tagged Pointer 指针里面的 tag 方法,获取 Tagged Pointer 指针 里面的 value 方法等,有兴趣的可以去看看,在这里不一一叙述。
我们使用 Tagged Pointer 的时候需要注意什么呢?
所有的 oc 对象都有 isa 指针,而 Tagged Pointer 并不是真正的对象,是伪指针,它没有 isa 指针。所以通过 LLDB 打印 Tagged Pointer 的 isa,会提示下图所示的错误。打印 OC 对象的 isa 没有问题,对于 Tagged Pointer,应该换成相应的方法调用,如 isKindOfClass 和 object_getClass。
至此,关于 Tagged Pointer,已经讲完~♥️
本文转载自公众号贝壳产品技术(ID:beikeTC)。
原文链接:
领取专属 10元无门槛券
私享最新 技术干货