写点什么

iOS 内存管理之 Tagged Pointer

2020 年 11 月 02 日

iOS内存管理之Tagged Pointer

1 背景

iPhone5s 是首个采用 64 位架构的 A7 双核处理器的手机,为了节省内存和提高执行效率,苹果提出了 Tagged Pointer 的概念。对于 64 位程序,引入 Tagged Pointer 后,相关逻辑能减少一半以上的内存占用,以及 3 倍的访问速度提升,100 倍的创建、销毁速度提升。


本文将带我们来理解这个概念 是怎么节省内存和提高执行效率的。(注:本篇文章所用系统 皆为 64 位系统)


2 不使用 Tagged Pointer 的情况

以 NSNumber *a = @(1);为例,在不使用 Tagged Pointer 的情况下,我们看下在内存上和访问效率上都是什么情况。


在内存上:


如下图所示, 1 个小对象 需要至少使用 24 字节(指针 8 字节 + 对象 16 字节 )


栈:在栈上,占 1 个指针 8 字节,里面存储的是堆内存的地址 0x600001a92920。


堆:在堆上,占 16 个字节,isa 指针占 8 个字节,1 为 int 类型,占 4 个字节,但由于内存对齐机制(ios 内存对齐 为 16 字节),堆需要 16 个字节的内存。



在效率上:


NSNumber 对象需要动态分配内存、维护引用计数、管理它的生命周期等


方法调用 需要 objc_msgSend 的执行流程(消息发送、动态方法解析、消息转发)


3 使用 Tagged Pointer 的情况

3.1 苹果对 Tagged Pointer 的介绍

苹果对 Tagged Pointer 的介绍主要有三点:


  • Tagged Pointer 被设计的目的是用来存储较小的对象,例如 NSNumber、NSDate、NSString 等;

  • Tagged Pointer 的值不再表示地址,而是真正的值;

  • 在内存读取上有着 3 倍的效率,创建时比以前快 106 倍 ;


3.2 Tagged Pointer 实质


Tagged Pointer 实质是一个伪指针,对象的指针中存储的数据变成了 Tag+Data 形式:


  • Tag 为特殊标记,用于区分是否是 Tagged Pointer 指针 以及区分 NSNumber、NSDate、NSString 等对象类型;

  • Data 为对象对应存储的值。


在内存上:只占一个指针的大小 8 字节,节省了很多内存开销;


在效率上:objc_msgSend 先识别 是否为 Tagged Pointer,若是,直接返回 不进行其他流程;若不是,进行其他流程(消息发送、动态方法解析、消息转发) 。从而节省了调用开销。


3.3 现状

一般我们在存放 NsNumber 和 NSDate 这一类变量的时候本身占用的内存大小常常不需要 8 个字节。4 字节带符号的整数可以达到 2^31=2147483648,99.999%的情况都能满足了。所以大部分都可以用 Tagged Pointer 类型,不满足的则申请堆内存。


4 Tagged Pointer 原理分析

4.1 设置环境变量

设置环境变量 OBJC_DISABLE_TAG_OBFUSCATION 为 YES, 为关闭 Tagged Pointer 的数据混淆;


设置环境变量 OBJC_DISABLE_TAGGED_POINTERS 为 YES, 来禁用 Tagged Pointer(目前不生效)在以前的版本,设置 OBJC_DISABLE_TAGGED_POINTERS 为 YES 会导致程序崩溃,是 runtime 中进行了判断,调用_objc_fatal()导致的程序崩溃。


4.2 举例分析

// 关闭 Tagged Pointer 数据混淆前(混淆为对于数据的保护)NSNumber *number1 = @(1);   // number1: 0x9a90d53a8ebc20bbNSNumber *number2 = @(2);   // number2: 0x9a90d53a8ebc208dNSNumber *number3 = @(3);   // number3: 0x9a90d53a8ebc209cNSNumber *numberFFFF = @(0xFFFFFFFFFFFFFFFF);   // numberFFFF: 0x600000aa0b80
// 关闭 Tagged Pointer 数据混淆后NSNumber *number1 = @(1); // number1: 0xb000000000000012NSNumber *number2 = @(2); // number2: 0xb000000000000022NSNumber *number3 = @(3); // number3: 0xb000000000000032NSNumber *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”,代表什么?


4.3 解析

我们先看结果,再分析。


4.3.1 解析结果

以上面例子中的 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 数据。下面会分别对标识位、类标识、数据类型做代码验证。


4.3.2 Tagged Pointer 标识位

如何判断为 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 的判断是如此的简单,只是二进制的与运算。


4.3.3 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: 0xa000000000000611NSString *string = [NSString stringWithFormat:@"a"];
复制代码


根据输出我们可以看到:


NSNumber 指针 0xb000000000000012,b 的二进制为 1011,后面的 011 是类标识位,对应十进制为 3,表示 NSNumber 类;


NSString 指针 0xa000000000000611, a 的二进制为 1010,后面的 010 是类标识位,对应十进制为 2,表示 NSString 类。


如图,类标识位置如下:



4.3.4 Tagged Pointer 数据类型

我们知道了以 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); // 0xb000000000000010NSNumber *number2 = @(b); // 0xb000000000000011NSNumber *number3 = @(c); // 0xb000000000000012NSNumber *number4 = @(d); // 0xb000000000000013NSNumber *number5 = @(e); // 0xb000000000000014NSNumber *number6 = @(f); // 0xb000000000000015
复制代码


可以看到,我们都用 NSNumber 类,用不同数据类型做测试,内存地址 16 进制只有最后一位发生了变化。其对应的数据类型分别为:


数据类型内存地址 二进制 最后四位
char0
short1
int2
long3
float4
double5


NSString、NSDate 的二进制最后四位 都是数据类型么?你可以自己去验证一下~


如图,数据类型位置如下:



至此我们就把 Tagged Pointer 实质 Tag+Data 完整地解析了一遍。


4.4 Tagged pointer 注释

在源码 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.**********************************************************************/
复制代码


对应注释翻译:


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

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

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

  4. 1 bit 标记是 Tagged Pointer

  5. 3 bits 标记类型

  6. 60 bits 负载数据容量,(存储对象数据)

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

  8. tag index 表示对象所属的 class

  9. 负载格式由对象的 class 定义

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

  11. 允许更多类,但 有效载荷 更小

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

  13. 1 bit 标记是 Tagged Pointer

  14. 3 bits 是 0b111

  15. 8 bits 扩展标记格式

  16. 52 bits 负载数据容量,(存储对象数据)

  17. 在这些表示中,某些体系结构反转了 MSB 和 LSB。


从注释中我们得知:


  • Tagged pointer 存储对象数据目前 分为 60bits 负载容量和 52bits 负载容量。

  • 类标识允许使用扩展形式。


那么如何判断负载容量?类标识的扩展类型有那些?我们来看下全面的 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}
复制代码


小结:


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


MacOS 下采用 LSB 即 OBJC_TAG_First60BitPayload、OBJC_TAG_First52BitPayload。


iOS 下则采用 MSB 即 OBJC_TAG_Last60BitPayload、OBJC_TAG_Last52BitPayload。


  1. 区分负载数据容量


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


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


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


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


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


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


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


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


你品,你细品这里。只要一个 tag,既可以区分负载数据容量,也可以区分类标识,就是这么滴强大~


5 创建 Tagged Pointer

我们知道了 Tagged Pointer 的实质 Tag+Data,知道了 Tag 对应什么,Data 对应什么。那么为什么 NSNumber、NSDate、NSString 会转成为伪指针呢?其他的为什么不会呢?NSNumber、NSDate、NSString 是如何生成 Tagged Pointer 的?下面让我们继续探索 Tagged Pointer。


5.1 Tagged Pointer 初始化

5.1.1 初始变量设置

在_read_images()方法中,有两处关键代码如下:


void _read_images(header_info **hList, uint32_t hCount, int totalClasses, int unoptimizedTotalClasses){    if (DisableTaggedPointers) {        disableTaggedPointers();    }
initializeTaggedPointerObfuscator();}
复制代码


上面方法主要分两部分:


  • disableTaggedPointers():禁用 Tagged Pointer,与环境变量 OBJC_DISABLE_TAGGED_POINTERS 相关。这里就不详述了~

  • initializeTaggedPointerObfuscator():初始化 TaggedPointer 混淆器:用于保护 Tagged Pointer 上的数据。我们看下这个方法的源码:


static voidinitializeTaggedPointerObfuscator(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;    }}
复制代码


上面方法主要分三部分:


  1. objc_debug_taggedpointer_obfuscator 是一个 unsigned long 类型的全局变量。objc_debug_taggedpointer_obfuscator 的用处,下面有用到~

  2. 对于一些旧版本 和 环境变量(OBJC_DISABLE_TAG_OBFUSCATION),禁用 tagged pointers 混淆。设置 objc_debug_taggedpointer_obfuscator 为 0,不混淆。

  3. 获得 objc_debug_taggedpointer_obfuscator 的值:

  4. 将随机数据放入变量中,然后移走所有非有效位。

  5. 和 ~_OBJC_TAG_MASK 作一次与操作。


5.1.2 Tagged Pointer 注册校验

为什么 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; } }}
复制代码


方法主要分为以下三部分:


  1. 判断是否禁用 Tagged Pointer,若禁用,则终止程序。

  2. 根据指定 tag 获取类指针。若 tag 被用于两个不同的类,则终止程序。

  3. 判断负载数据容量如果是 52bits 进行特殊处理,在 OBJC_TAG_RESERVED_7 处存储占位类 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;}
复制代码


以上方法主要分为三部分:


  1. 根据负载数据容量是 60bits 还是 52bits,区分为类标识是基础类标识还是扩展类标识。也可以说根据 tag 类标识区间判断。

  2. tag 是基础类标识,返回 classSlotForBasicTagIndex(tag)的结果;

  3. tag 是扩展类标识,对 tag 进行位操作,然后取出存在 objc_tag_ext_classes 数组里的结果返回。


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


#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}
复制代码


以上方法主要分为以下两个部分:


  1. 对 tag 类标识,进行了一系列的位运算。(运算里面的宏定义在 5.2 中有讲~有兴趣的可以自己算算哦~)

  2. 根据判断是 macOS or iOS,来获取 objc_tag_classes 数组里面的类指针。


5.2 生成 Tagged Pointer 指针

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);    }}
复制代码


方法主要分为以下三部分:


  1. 根据负载内容位进行区分:


传入的 tag 为类标识,同时也可以用于区分负载数据容量,苹果根据不同的负载数据容量对 Tagged Pointer 进行了不同的处理。


  1. 对传入 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;


  1. 进行编码(数据混淆,数据保护):对 result (0xb000000000000012)进行编码,我们看下:


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 方法等,有兴趣的可以去看看,在这里不一一叙述。


6 Tagged Pointer 使用注意

我们使用 Tagged Pointer 的时候需要注意什么呢?


所有的 oc 对象都有 isa 指针,而 Tagged Pointer 并不是真正的对象,是伪指针,它没有 isa 指针。所以通过 LLDB 打印 Tagged Pointer 的 isa,会提示下图所示的错误。打印 OC 对象的 isa 没有问题,对于 Tagged Pointer,应该换成相应的方法调用,如 isKindOfClass 和 object_getClass。



至此,关于 Tagged Pointer,已经讲完~♥️


本文转载自公众号贝壳产品技术(ID:beikeTC)。


原文链接


iOS内存管理之Tagged Pointer


2020 年 11 月 02 日 10:051395

评论 3 条评论

发布
用户头像
初始化是在汇编中
2021 年 06 月 02 日 10:42
回复
objc-msg中可以找到相关汇编
2021 年 06 月 02 日 10:43
回复
用户头像
关于 tagged pointer 的标志位,在 macOS 且 x86_64 平台上是低位,在其他平台上是高位。可以参考 objc4-818.2 objc-internal.h 的 OBJC_SPLIT_TAGGED_POINTERS 和 OBJC_MSB_TAGGED_POINTERS 声明
2021 年 05 月 22 日 21:25
回复
没有更多了
发现更多内容

IP地址定位在网站上的几个代表性应用

郑州埃文科技

JavaScript and Ruby in ABAP

Jerry Wang

JavaScript SAP abap WebClient UI

使用ABAP(ADBC)和Java(JDBC)连接SAP HANA数据库

Jerry Wang

JavaScript SAP abap WebClient UI SAP UI5

IP数据库的定位能力在商业端的具体应用有哪些?(一)

郑州埃文科技

Pulumi Stack 命令不能找到默认的 Stack

HoneyMoose

C/C++学习:C++并发与多线程

奔着腾讯去

c++ 并发 多线程并发 POSIX线程 C++11线程

Internationalization(i18n) support in SAP CRM,UI5 and Hybris

Jerry Wang

JavaScript CRM SAP abap SAP UI5

BZZ节点算力挖矿系统开发流程丨BZZ节点算力挖矿现成源码

系统开发咨询1357O98O718

Pulumi AWS 在进行预览更新的时候持续提示 Key 错误

HoneyMoose

使用ABAP批量下载Markdown源文件里的图片到本地

Jerry Wang

markdown SAP abap download

膜拜!首次公布Java10W字面经,Github访问量破百万

云流

Java 程序员 架构 面试

宏碁亮相2021西洽会,以绿色智能“洽谈未来”

科技热闻

SAP BSP和JSP里的UI元素ID生成逻辑

Jerry Wang

Java jsp SAP abap SAP UI5

help.hybris.com和help.sap.com网站的搜索实现

Jerry Wang

Java SAP abap Hybris

Jerry和您聊聊Chrome开发者工具

Jerry Wang

chrome 前端 SAP Chrome开发者工具

Redis - 复制

旺仔大菜包

redis

Golang Testing 概览 - 深入篇

hedzr

go testing

直呼内行!阿里大佬离职带出内网专属“高并发系统设计”学习笔记

云流

Java 程序员 架构 面试

Golang Profiling: 关于 pprof

hedzr

go profiling

戏说代理模式

编程三昧

随笔 设计模式 开发 代理模式

从天而降的AI“青云梯”,开发者们准备好了吗?

脑极体

SAP ABAP和Hybris的源代码生成工具

Jerry Wang

Java SAP abap Hybris commerce

Hybris UI的Route(路由)实现

Jerry Wang

Java Java 8 SAP WebClient UI Hybris

还在用SELECT COUNT统计数据库表的行数?Out了

Jerry Wang

SAP abap hana

ABAP下载的病毒扫描Virus Scan

Jerry Wang

下载 SAP abap 病毒扫描

通过ABAP代码判断当前系统类型,BYD还是S4 OP还是S4 Cloud

Jerry Wang

SAP abap S/4HANA SAP Business ByDesign

SAP Cloud for Customer(C4C)和微软Outlook的集成

Jerry Wang

微软 SAP abap SAP UI5 outlook

如何查看SAP CRM WebUI,C4C和Hybris里的页面技术信息

Jerry Wang

CRM abap WebClient UI SAP UI5

乾坤大挪移:SAP CRM WebClient UI 和 SAP Fiori UI 混搭并存

Jerry Wang

CRM SAP abap WebClient UI bsp

Hybris ECP里Customer对应的数据库表

Jerry Wang

JavaScript CRM SAP WebClient UI SAP UI5

Golang Testing 概览 - 补充篇

hedzr

go testing assertion

iOS内存管理之Tagged Pointer-InfoQ