阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |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:055370

评论 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
回复
没有更多了
发现更多内容

模块化设计思想产品设计应用

燕陈华

产品设计 模块化流程 流程图

小棉袄,最终却没有变成你的防弹衣

小天同学

个人成长 成长 感悟 母亲节 感恩

软件产品开发流程

Interstate5

软件开发 软件开发流程

给在线教学泼点冷水

启润

在线教育 基础教育

权威与边界

伯薇

权威 边界 BART 工作方式

HTTP 升级 HTTPS 全过程记录

猴哥一一 cium

https 证书

用 Electron 打包语雀

封不羁

Java Electron

Spring整合MyBatis详细分析

Java收录阁

mybatis

webpack入门(一)

子铭

Java内存模型和volatile、final等关键字

麻瓜镇

Java 多线程

《零基础学 Java》 FAQ 之 3-为什么计算机里的浮点数不精确

臧萌

Java 浮点数

一口气带你踩完五个 List 的大坑,真的是处处坑啊!

楼下小黑哥

Java 踩坑 后端 集合

LeetCode 1232. Check If It Is a Straight Line

liu_liu

LeetCode

Android Studio NDK 编译 Bsdiff 库

码农亮哥

android-studio ndk bsdiff

高仿瑞幸小程序 05 更正轮播组件的高度计算

曾伟@喵先森

小程序 微信小程序 大前端

软件产品信息安全 - 数据分类

Interstate5

软件开发 信息安全 数据分类

《零基础学Java》 FAQ 之 零-这门课适合我吗?

臧萌

Java 编程语言

人人都应该懂的加密算法 - 公钥加密

麦叔

对称加密 加密解密 信息安全 公钥加密

Redis学习笔记(集合类型)

编程随想曲

redis

《零基础学 Java》 FAQ 之 4-关于补码,多说两句

臧萌

Java 补码

程序员必需清楚的进程和线程

小趴菜~

线程 操作系统 进程

大话区块链和比特币的技术原理

麦叔

比特币 区块链 数字货币 加密货币 加密解密

程序员的晚餐 | 5月9日 炖蹄髈

清远

程序员

产品周刊 | 第 14 期(20200510)

八味阁

产品 产品经理 产品设计

软件产品的信息安全问题

Interstate5

软件开发 信息安全

Java小想法: JDK许可证

X.F

Java 编程语言

白话计算机网络通信过程

WB

程序员 计算机网络

从nacos客户端的TIME_WAIT说起

捉虫大师

Java TCP nacos

面向对象是什么

落英亭郎

面向对象

《如何阅读一本书》读书心得

兆熊

读书笔记

《你好架构师之 压榨硬件价值的利器容器(Docker)》

再见小飞侠

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