objc4 runtime internals

Tagged Pointer 对象

Tagged Pointer 是一种“指针即对象”的表示法:对象没有堆内存,类信息和 payload 直接编码在指针位中。objc4 对它提供了创建、取 tag、取 payload、取类、消息发送、禁用和混淆等完整路径。

目录 作用 实现原理 核心宏、结构、函数 关键流程 测试视角 源码依据

作用

减少堆分配

小整数、短字符串、日期、索引路径等小对象可以直接塞进指针。NSNumber 的小整数就是典型例子,test/taggedNSPointers.m 验证了 NSNumber numberWithInt:1234 是 tagged pointer。

保留对象语义

虽然指针不指向对象内存,它仍能接收消息、查询类、作为字典 key/value,并通过 Foundation/CF 桥接。运行时把 tag 映射到真实 Class 后,继续走常规方法缓存查找。

绕开生命周期表

test/taggedPointers.m 验证 retain、release、autorelease、weak store/load 对 tagged pointer 不产生真实引用计数或弱引用表负担。它们本质上是立即值,不需要释放。

**一句话:**Tagged Pointer 把“对象身份”改成“位编码值”。运行时需要做的,是在所有需要对象元数据的地方识别它,并把 tag 翻译成类。

实现原理

objc4 在 64 位平台启用 tagged pointer。runtime/objc-config.hSUPPORT_TAGGED_POINTERS__LP64__ 为 1;非 64 位平台关闭。tag 标记位在大多数平台使用 MSB,macOS x86_64 使用 LSB。arm64 还使用 split tagged pointer 布局。

基本 tag:60-bit payload

tag 0…6 可用,tag 7 保留给扩展表示。

概念

tagged 标记

3-bit tag

60-bit payload

在 arm64 split 布局中,标记位是 bit 63,普通 tag 位在低 3 位,因此 _OBJC_TAG_INDEX_SHIFT 为 0。

扩展 tag:52-bit payload

tag 8…263 可用,tag 264 保留;用 tag 7 进入扩展空间。

概念

tagged 标记

0b111

8-bit ext tag

52-bit payload

扩展 tag 增加了可表示的类数量,但 payload 从 60 位降到 52 位。

// runtime/objc-internal.h:tag 空间的语义边界
OBJC_TAG_First60BitPayload = 0;
OBJC_TAG_Last60BitPayload  = 6;
OBJC_TAG_RESERVED_7        = 7;
OBJC_TAG_First52BitPayload = 8;
OBJC_TAG_Last52BitPayload  = 263;
OBJC_TAG_RESERVED_264      = 264;

// arm64 split tagged pointers:标记位在 MSB,普通 tag 在低 3 位。
#define _OBJC_TAG_MASK             (1UL<<63)
#define _OBJC_TAG_INDEX_SHIFT      0
#define _OBJC_TAG_SLOT_SHIFT       0
#define _OBJC_TAG_PAYLOAD_LSHIFT   1
#define _OBJC_TAG_PAYLOAD_RSHIFT   4

// 扩展表示:低 3 位都是 1,扩展 tag 放在高位区间。
#define _OBJC_TAG_EXT_MASK         (_OBJC_TAG_MASK | 0x7UL)
#define _OBJC_TAG_EXT_INDEX_SHIFT  55
#define _OBJC_TAG_EXT_PAYLOAD_LSHIFT 9
#define _OBJC_TAG_EXT_PAYLOAD_RSHIFT 12

核心宏、结构、函数和关键字段

符号位置作用
SUPPORT_TAGGED_POINTERSruntime/objc-config.h编译期总开关。64 位启用,非 64 位关闭。
OBJC_MSB_TAGGED_POINTERS / OBJC_SPLIT_TAGGED_POINTERSruntime/objc-internal.h决定标记位方向和 arm64 split 布局。
objc_tag_index_truntime/objc-internal.htag 枚举。0…6 是 60-bit payload,8…263 是 52-bit payload。
objc_debug_taggedpointer_classesruntime/Messengers.subproj/objc-msg-arm64.s普通 tag 到 Class 的表。arm64 汇编里定义,C++ 里通过 objc_tag_classes 使用。
objc_debug_taggedpointer_ext_classesruntime/Messengers.subproj/objc-msg-arm64.s扩展 tag 到 Class 的表。放在普通表前面,便于消息发送汇编用负索引查表。
objc_debug_taggedpointer_obfuscatorruntime/objc-runtime-new.mm运行时随机混淆值。用于构造和解码 tagged pointer,降低伪造特定对象的可预测性。
_objc_makeTaggedPointerruntime/objc-internal.h根据 tag 和 payload 组装指针,再做混淆/置换。
_objc_isTaggedPointerruntime/objc-internal.h只检查标记位,不验证 tag 是否已注册类。
_objc_getTaggedPointerTag / _objc_getTaggedPointerValueruntime/objc-internal.h先解混淆,再取 tag 或 payload;payload 分无符号和有符号两种接口。
_objc_registerTaggedPointerClassruntime/objc-runtime-new.mm把 tag 注册到 Class,非法 tag 或冲突注册会 fatal。
objc_object::getIsa()runtime/objc-object.h普通对象读 isa;tagged pointer 通过 class 表取 Class。
GetTaggedClassruntime/Messengers.subproj/objc-msg-arm64.sarm64 消息发送中为 tagged receiver 快速取 Class 的汇编宏。

关键流程

1. 判断 tagged

判断逻辑非常轻:看指针与 _OBJC_TAG_MASK 的交集是否等于 mask。这个检查不保证类有效,只说明“位形态像 tagged pointer”。

// runtime/objc-internal.h
static inline bool
_objc_isTaggedPointer(const void *ptr)
{
    // MSB tagged 平台:最高位为 1 即视为 tagged。
    // macOS x86_64 等 LSB tagged 平台:最低位为 1。
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

static inline bool
_objc_isTaggedPointerOrNil(const void *ptr)
{
    // 很多运行时路径同时需要快速处理 nil 和 tagged pointer。
    return !ptr || ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}

2. 构造、取 tag、取 payload

选择表示

tag 0…6 用普通表示;tag 8…263 用扩展表示。

写入位域

设置 tagged 标记、tag 位和 payload 位。

混淆

用 obfuscator XOR,split 普通 tag 还会置换。

解码

读取时先反向处理混淆和 tag 置换。

移位取值

根据普通/扩展表示抽出 tag 或 payload。

// runtime/objc-internal.h:构造 tagged pointer 的核心逻辑,省略断言。
static inline void *
_objc_makeTaggedPointer_withObfuscator(objc_tag_index_t tag,
                                       uintptr_t value,
                                       uintptr_t obfuscator)
{
    if (tag <= OBJC_TAG_Last60BitPayload) {
        uintptr_t result =
            _OBJC_TAG_MASK |                              // 1. 标出这是 tagged pointer
            ((uintptr_t)tag << _OBJC_TAG_INDEX_SHIFT) |   // 2. 写入 3-bit 基本 tag
            ((value << _OBJC_TAG_PAYLOAD_RSHIFT) >>
             _OBJC_TAG_PAYLOAD_LSHIFT);                  // 3. 截断并放入 60-bit payload

        return _objc_encodeTaggedPointer_withObfuscator(result, obfuscator);
    } else {
        uintptr_t result =
            _OBJC_TAG_EXT_MASK |                          // 1. 标出扩展 tagged:MSB + 低 3 位全 1
            ((uintptr_t)(tag - OBJC_TAG_First52BitPayload)
                << _OBJC_TAG_EXT_INDEX_SHIFT) |           // 2. 写入 8-bit 扩展 tag
            ((value << _OBJC_TAG_EXT_PAYLOAD_RSHIFT) >>
             _OBJC_TAG_EXT_PAYLOAD_LSHIFT);               // 3. 截断并放入 52-bit payload

        return _objc_encodeTaggedPointer_withObfuscator(result, obfuscator);
    }
}

3. 取类:从 tag 到 Class

objc_object::getIsa() 对普通对象直接读 isa;如果 receiver 是 tagged pointer,则用 pointer 位计算 slot,再查类表。扩展 tagged 的普通 slot 中会放 __NSUnrecognizedTaggedPointer 占位类,触发二次查扩展表。

// runtime/objc-object.h:对象层面的 Class 获取。
inline Class
objc_object::getIsa() const
{
    if (fastpath(!isTaggedPointer()))
        return ISA(/*authenticated*/true);                // 普通对象:从对象内存读取 isa。

    uintptr_t ptr = (uintptr_t)this;
    uintptr_t slot = (ptr >> _OBJC_TAG_SLOT_SHIFT) &
                     _OBJC_TAG_SLOT_MASK;                 // 基本 tag slot。
    Class cls = objc_tag_classes[slot];

    if (slowpath(cls == (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer)) {
        slot = (ptr >> _OBJC_TAG_EXT_SLOT_SHIFT) &
               _OBJC_TAG_EXT_SLOT_MASK;                   // 扩展 tag slot。
        cls = objc_tag_ext_classes[slot];
    }
    return cls;
}

4. 注册 tag class

注册是运行时把某个 tag 绑定到某个 Class 的入口。它会处理禁用、非法 tag、重复注册和扩展 tag 占位类。

// runtime/objc-runtime-new.mm:注册 Class 到 tagged pointer tag。
void
_objc_registerTaggedPointerClass(objc_tag_index_t tag, Class cls)
{
    if (objc_debug_taggedpointer_mask == 0)
        _objc_fatal("tagged pointers are disabled");      // 运行期禁用后禁止注册。

    auto *slot = classSlotForTagIndex(tag);                // 根据 tag 找普通表或扩展表 slot。
    if (!slot)
        _objc_fatal("tag index %u is invalid", tag);       // tag 7、264 或越界会失败。

    Class oldCls = *slot;
    if (cls && oldCls && cls != oldCls)
        _objc_fatal("tag index %u used for two different classes", tag);

    *slot = cls;                                           // 同类重复注册和注册 nil 允许。

    if (tag < OBJC_TAG_First60BitPayload ||
        tag > OBJC_TAG_Last60BitPayload) {
        auto *extSlot = classSlotForBasicTagIndex(OBJC_TAG_RESERVED_7);
        if (*extSlot == nil)
            *extSlot = (Class)&OBJC_CLASS_$___NSUnrecognizedTaggedPointer;
    }
}

5. 消息发送分流

arm64 的 objc_msgSend 在入口先比较 receiver 和 0。MSB tagged pointer 看起来是负数,因此 b.le 同时捕获 nil 和 tagged。nil 返回 0;tagged 用 GetTaggedClass 得到 Class,然后回到正常缓存查找。

// runtime/Messengers.subproj/objc-msg-arm64.s:简化后的关键路径。
_objc_msgSend:
    cmp     p0, #0                         // receiver 与 0 比较。
    b.le    LNilOrTagged                   // 等于 0 是 nil;小于 0 是 MSB tagged pointer。

    ldr     p14, [x0]                      // 普通对象:从对象内存加载 raw isa。
    GetClassFromIsa_p16 p14, 1, x0         // 得到 class。
LGetIsaDone:
    CacheLookup NORMAL, _objc_msgSend, __objc_msgSend_uncached

LNilOrTagged:
    b.eq    LReturnZero                    // nil 消息返回 0。
    GetTaggedClass                         // tagged:从 tag class 表取 class 到 x16。
    b       LGetIsaDone                    // 之后复用普通方法缓存查找。

// GetTaggedClass 的 arm64 split 思路:
and x10, x0, #0x7                          // 低 3 位是 small tag。
asr x11, x0, #55                           // 扩展 tag 形成负索引。
cmp x10, #7
csel x12, x11, x10, eq                     // tag==7 查扩展表,否则查普通表。
ldr x16, [_objc_debug_taggedpointer_classes + x12*8]

6. 禁用与混淆

禁用 tagged pointer

OBJC_DISABLE_TAGGED_POINTERS=YES 会让启动流程调用 disableTaggedPointers(),把 mask、slot shift、payload shift、ext mask 等调试字段清零。之后 _objc_taggedPointersEnabled() 返回 false,注册类会 fatal。

禁用 tag 混淆

OBJC_DISABLE_TAG_OBFUSCATION=YES 或旧 SDK 链接路径会让 objc_debug_taggedpointer_obfuscator = 0。测试据此可直接用固定指针形态取出 tag 0。

// runtime/objc-runtime-new.mm:启动期混淆初始化。
static void
initializeTaggedPointerObfuscator(void)
{
    if (!DisableTaggedPointerObfuscation &&
        dyld_program_sdk_at_least(dyld_fall_2018_os_versions)) {
        arc4random_buf(&objc_debug_taggedpointer_obfuscator,
                       sizeof(objc_debug_taggedpointer_obfuscator));
        objc_debug_taggedpointer_obfuscator &= ~_OBJC_TAG_MASK;

#if OBJC_SPLIT_TAGGED_POINTERS
        objc_debug_taggedpointer_obfuscator &=
            ~(_OBJC_TAG_EXT_MASK | _OBJC_TAG_NO_OBFUSCATION_MASK);
        // split 布局还会随机打乱前 7 个普通 tag 的映射。
#endif
    } else {
        objc_debug_taggedpointer_obfuscator = 0;
    }
}

测试视角

测试文件覆盖点
test/taggedPointers.m构造 tagged pointer、验证 tag/payload、类表 slot、消息发送、结构体返回、浮点返回、关联对象、弱引用、retain/release/autorelease 绕行,以及 split 下 constant CFString raw pointer round trip。
test/taggedPointersAllClasses.m遍历所有可注册 tag,为空闲 tag 动态创建 Class,发送消息验证每个 tag 的 payload 解码都正确。
test/taggedNSPointers.m验证 Foundation/CF 中的 NSNumber tagged pointer:CF 类型、Class 注册、取 tag、重建指针、集合行为和 selector 响应。
test/taggedPointersDisabled.m设置 OBJC_DISABLE_TAGGED_POINTERS=YES 后,_objc_taggedPointersEnabled() 为 false,注册 tag class 触发 “tagged pointers are disabled”。
test/taggedPointersTagObfuscationDisabled.m设置 OBJC_DISABLE_TAG_OBFUSCATION=YES 后,固定形态的指针能直接解出 tag 0,说明 obfuscator 不参与。
test/badTagClass.m同一 tag 重复注册同一个类和 nil 允许;换成另一个类会 fatal,防止 tag 到 Class 映射歧义。
test/badTagIndex.m注册 OBJC_TAG_Last52BitPayload + 1 即 264 会 fatal,验证 tag 边界。

源码依据

运行时实现

runtime/objc-config.h runtime/objc-internal.h runtime/objc-object.h runtime/objc-runtime-new.mm runtime/Messengers.subproj/objc-msg-arm64.s

行为测试

test/taggedPointers.m test/taggedPointersAllClasses.m test/taggedPointersDisabled.m test/taggedPointersTagObfuscationDisabled.m test/taggedNSPointers.m test/badTagClass.m test/badTagIndex.m

**注意:**objc4 注释明确说明 tagged pointer 布局会随 OS/架构变化。业务代码不应依赖具体位布局,应使用公开或 SPI 层提供的表示无关接口。

本文档为单页自包含 HTML,面向 objc4 源码阅读,重点解释 Tagged Pointer 从位布局到消息发送的运行时闭环。