速来报名!AICon北京站鸿蒙专场~ 了解详情
写点什么

iOS 内存管理之 Tagged Pointer

  • 2020-11-02
  • 本文字数:10695 字

    阅读完需:约 35 分钟

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:056365

评论 4 条评论

发布
用户头像
`_objc_registerTaggedPointerClass` 确实是注册。
通过 classSlotForTagIndex 获取的是 `objc_tag(_ext)_classes` 数组中 tag 对应的内存地址,然后通过 `*slot = cls` 进行赋值,完成注册绑定。
判断 `cls && oldCls && cls != oldCls` 是防止同一个 tag 绑定到了不同的类上。
2022-01-07 21:53
回复
用户头像
初始化是在汇编中
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
回复
没有更多了
发现更多内容

淘宝APP商品详情深度解析:item_get_app精准提取标题、价格、销量、描述及SKU信息,助力竞品分析与推广策略优化

代码忍者

API 接口 pinduoduo API

《实力见证,卓越前行 ——伊克罗德信息荣获 Amazon Redshift Delivery 认证》

伊克罗德信息科技

大数据 Amazon

【JIT/极态云】技术文档--模型事件

武汉万云网络科技有限公司

MES系统助力中小企业数字化转型

万界星空科技

数字化转型 生产管理系统 mes 万界星空科技 中小企业数字化转型

创新业务与有效流量是全媒体数字化转型成功与否的最佳证明。

Geek_2305a8

淘宝商品关键词API接口:智能分析关键词数据,提升电商运营效率

代码忍者

API 接口 pinduoduo API

GitLab 如何降级安装?

极狐GitLab

【问卷调研】HarmonyOS SDK开发者社区用户需求有奖调研

HarmonyOS SDK

IPQ5322 vs IPQ9574: A Comparison of Temperature Range and Use Cases for Wi-Fi 7 in Enterprise and Industrial Applications

wallyslilly

ipq9574 IPQ5332

iLogtail 开源贡献人物专访:技术之路无坦途,与社区共同成长

阿里巴巴云原生

阿里云 云原生 iLogtail

操作系统大会2024即将在京召开,见证openEuler发展新里程

科技热闻

鸿蒙网络编程系列47-仓颉版UDP客户端

长弓三石

DevEco Studio 开发实例 HarmonyOS NEXT 网络与连接

如何使用PHP开发API接口?

科普小能手

php API php扩展 API接口 API 测试

区块链NFT项目开发流程

区块链软件开发推广运营

交易所开发 dapp开发 链游开发 公链开发 代币开发

揭秘!Vue3.5响应式重构如何让内存占用减少56%

EquatorCoco

Vue

Microsoft Phone System Direct Routing需要准备的

cts喜友科技

通信 通信云 通讯

Viscosity for Mac(open客户端)中文版

Mac相关知识分享

第74期 | GPTSecurity周报

云起无垠

5分钟搞懂 Golang 数据库连接管理

俞凡

golang

Serverless 微服务治理神器: 阿里云 SAE 全链路灰度揭秘

阿里巴巴云原生

阿里云 Serverless 云原生

百度世界大会:AI时代的公司官网来了 会后首日新增582家企业合作

Geek_2d6073

AI时代的研发生产关系,是否有新的可能?

思码逸研发效能

哇喔!20种单例模式的实现与变异总结

威哥爱编程

Java 设计模式 单例模式 JavaEE

尝鲜智能体,今年双十一“快递100”用上混元大模型

Geek_2d6073

SSH 与 SSL:主要区别及用途说明

高端章鱼哥

大选获胜,盘点特朗普相关加密货币资产

TechubNews

南京大学 鲲鹏昇腾科教创新孵化中心揭牌,引领高校科研生态新模式

Geek_2d6073

公链数字钱包开发与加密钱包App原生开发

区块链软件开发推广运营

交易所开发 dapp开发 链游开发 公链开发 代币开发

快递鸟单号识别API接口代码操作流程

快递鸟

快递

深入探索淘宝API:电商开发的高效策略与实践

代码忍者

API 接口 pinduoduo API

2024年AI办公工具API:优化工作流程的智能解决方案

幂简集成

API

iOS内存管理之Tagged Pointer_架构_孙齐_InfoQ精选文章