首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

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 举例分析

代码语言:javascript
复制
// 关闭 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”,代表什么?

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 标识位的方法,如下代码:

代码语言:javascript
复制
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 中可以找到:

代码语言:javascript
复制
#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 标识位是处处优先判断的。如下面源码(下面源码只展示相关部分)所示:

代码语言:javascript
复制
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 里有介绍。

代码语言:javascript
复制
{
    // 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,
    。。。
}

下面让我们举例验证,不同的类型,输出一下看看地址:

代码语言:javascript
复制
// 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 类。

如图,类标识位置如下:

4.3.4 Tagged Pointer 数据类型

我们知道了以 NSNumber 为例的地址 0xb000000000000012 的数据数值、Tagged Pointer 标识位、Tagged Pointer 类标识。那么最后一位 2 代表的是什么呢?

16 进制的最后一位(即 2 进制的最后四位)表示数据类型。同样我们举例验证:

代码语言:javascript
复制
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 完整地解析了一遍。

4.4 Tagged pointer 注释

在源码 objc-runtime-new.mm 中有一段注释对 Tagged pointer objects 进行了解释,原文如下:

代码语言:javascript
复制
/***********************************************************************
* 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 当前使用此表示形式:
    • (LSB)(macOS)64 位分布如下:
      • 1 bit 标记是 Tagged Pointer
      • 3 bits 标记类型
      • 60 bits 负载数据容量,(存储对象数据)
    • (MSB)(iOS)64 位分布如下:
      • tag index 表示对象所属的 class
      • 负载格式由对象的 class 定义
      • 如果 tag index 是 0b111(7), tagged pointer 对象使用 “扩展” 表示形式
      • 允许更多类,但 有效载荷 更小
    • (LSB)(macOS)(带有扩展内容)64 位分布如下:
      • 1 bit 标记是 Tagged Pointer
      • 3 bits 是 0b111
      • 8 bits 扩展标记格式
      • 52 bits 负载数据容量,(存储对象数据)
  3. 在这些表示中,某些体系结构反转了 MSB 和 LSB。

从注释中我们得知:

  • Tagged pointer 存储对象数据目前 分为 60bits 负载容量和 52bits 负载容量。
  • 类标识允许使用扩展形式。

那么如何判断负载容量?类标识的扩展类型有那些?我们来看下全面的 objc_tag_index_t 源码:

代码语言:javascript
复制
// 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() 方法中,有两处关键代码如下:

代码语言:javascript
复制
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 上的数据。我们看下这个方法的源码:

代码语言:javascript
复制
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;
    }
}

上面方法主要分三部分:

  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 的值:
    • 将随机数据放入变量中,然后移走所有非有效位。
    • 和 ~_OBJC_TAG_MASK 作一次与操作。

5.1.2 Tagged Pointer 注册校验

为什么 NSNumber、NSDate、NSString 会转成为伪指针呢?其他的为什么不会呢?

加载程序时,从 dyld 库的 _dyld_start() 函数开始,经历了多般步骤,开始调用 _objc_registerTaggedPointerClass() 函数。下面我们来看下在源码 objc-runtime-new.mm 中该方法的实现:

代码语言:javascript
复制
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 的源码实现:

代码语言:javascript
复制
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 数组里的结果返回。

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

代码语言:javascript
复制
#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 的源码:

代码语言:javascript
复制
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 指针

代码语言:javascript
复制
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)进行编码,我们看下:

代码语言:javascript
复制
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

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/R5s0BudUKwYNDAfrIVh4
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券