Objective-C Runtime 中的 isa 指针详解

1 阅读8分钟

一、为什么要理解 isa

Objective-C 的对象具有动态性。执行下面的代码时:

[person sayHello];

Runtime 需要知道 person 属于哪个类,然后才能查找并执行 sayHello。

对象内部的 isa 就是连接对象和类系统的入口。Runtime 会根据 isa 找到 Class,再根据 selector 查找方法实现。

可以简单理解为:

对象
  ↓ isa
Class
  ↓ 方法缓存和方法列表
IMP

IMP 是实际的方法实现函数地址。

二、实例对象、Class 和 Meta-Class

假设有一个类:

@interface Person : NSObject
​
- (void)sayHello;
+ (void)test;
​
@end

创建实例:

Person *person = [[Person alloc] init];

三者关系大致如下:

person 实例对象
    └── isa ─────> Person 类对象
                         └── isa ─────> Person 元类对象

实例对象

实例对象通常包含:

  • isa
  • 实例变量
  • 其他对象数据

实例对象的 isa 指向所属的 Class。

Class 类对象

Class 对象保存类的结构信息,例如:

  • isa
  • superclass
  • 实例方法列表
  • 方法缓存
  • 属性列表
  • 成员变量列表
  • 协议列表

Meta-Class 元类

类方法并不存放在普通实例对象中,而是存放在元类的方法列表中:

+ (void)test;

调用类方法时,Runtime 会通过类对象的 isa 找到元类,然后在元类的方法列表中查找。

因此可以记忆为:

实例方法 → Class 的方法列表
类方法   → Meta-Class 的方法列表

三、isa 和 superclass 的区别

这两个概念容易混淆。

isa:表示“我是什么类型”

实例对象 isa → Class
Class isa     → Meta-Class

superclass:表示“我的父类是谁”

Person superclass → NSObject

消息查找时,如果当前类找不到方法,Runtime 会沿着 superclass 链继续向上查找。

所以:

isa         用于定位当前对象对应的类
superclass  用于定位父类

四、传统 isa 和 Non-pointer ISA

早期架构中,isa 可以理解为一个普通的 Class 指针:

struct objc_object {
    Class isa;
};

这种情况下,isa 直接保存 Class 的内存地址。

从 arm64 开始,Apple 对 isa 做了优化。由于 64 位地址空间通常没有使用全部地址位,而且对象地址还具有对齐特征,Runtime 可以把一部分 bit 用来保存额外信息。

因此 isa 不再只是一个纯指针,而可以理解为:

union isa_t {
    Class cls;
    uintptr_t bits;
};

同一块内存既可以按 Class 指针解释,也可以按 64 位整数解释。

这种优化后的形式通常称为 Non-pointer ISA。

五、Non-pointer ISA 的位域

典型的 arm64 Runtime 位域大致如下:

union isa_t {
    Class cls;
    uintptr_t bits;
​
    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33;
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t deallocating      : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };
};

这些字段加起来正好是 64 bit:

1 + 1 + 1 + 33 + 6 + 1 + 1 + 1 + 19 = 64

nonpointer

表示是否使用 Non-pointer ISA:

0:更接近传统的普通指针形式
1:使用位域保存类信息和对象状态

has_assoc

表示对象是否设置过关联对象:

objc_setAssociatedObject(object, key, value, policy);

如果没有关联对象,销毁对象时就不需要检查关联对象表,可以加快释放过程。

has_cxx_dtor

表示对象是否包含需要执行 C++ 析构函数的成员。

Objective-C++ 中,如果对象包含 C++ 对象,销毁时还需要执行 C++ 析构逻辑。这个字段可以帮助 Runtime 判断是否需要执行额外的析构操作。

shiftcls

保存当前 isa 目标的压缩类指针信息。

它不是完整的 Class 指针,而是经过移位或压缩后的类地址信息。对于实例对象,它对应 Class;对于类对象,它对应 Meta-Class。

magic

用于校验 Non-pointer ISA 的格式和状态是否合法。

它可以帮助 Runtime 或调试工具判断当前 isa 是否符合预期,但不是普通业务代码可以使用的初始化状态字段。

weakly_referenced

表示对象是否曾经被 weak 指针引用过。

如果对象存在弱引用,Runtime 需要在对象销毁时找到这些 weak 指针,并将它们自动设置为 nil。

如果对象没有弱引用,销毁时可以跳过相关清理流程。

deallocating

表示对象是否正在销毁。

当对象的引用计数降为零,Runtime 进入销毁流程时,会使用相关状态区分对象的正常存活状态和释放状态。

extra_rc

用于在 isa 中保存部分额外引用计数。

常见的简化理解是:

实际引用计数 ≈ extra_rc + 1

这样,很多 retain 和 release 操作可以直接修改 isa 中的 bit,而不需要访问额外的数据结构。

has_sidetable_rc

表示对象是否需要使用 Side Table 保存额外引用计数。

当 extra_rc 的空间不够时,Runtime 会把更多引用计数信息放入 Side Table,并通过该字段标记。

六、Side Table 是什么

Side Table 是 Objective-C Runtime 内部维护的辅助数据结构,不是某个类的普通属性。

它可以保存:

  • 超出 isa 位域容量的引用计数
  • 弱引用相关信息
  • 其他对象生命周期管理信息

对象生命周期管理大致有两种情况:

isa 中的 extra_rc 足够
    ↓
直接使用 isa 中的引用计数
​
isa 中的 extra_rc 不够
    ↓
使用 Side Table 保存额外引用计数

这样可以让普通对象尽量走更快的内联路径,只有特殊情况才访问 Side Table。

七、为什么需要 ISA_MASK

Non-pointer ISA 的 isa 同时包含:

  • 类指针信息
  • 标记位
  • 部分引用计数
  • 对象状态

因此不能把整个 isa.bits 直接当成 Class 地址。Runtime 需要先清除非地址部分,再提取真实类地址。

示意代码如下:

uintptr_t isaValue = object->isa.bits;
Class cls = (Class)(isaValue & ISA_MASK);

ISA_MASK 的作用就是保留类地址对应的 bit,清除标记位和引用计数位。

早期 Runtime 中可能看到类似定义:

#if __arm64__
#define ISA_MASK 0x0000000ffffffff8ULL
#elif __x86_64__
#define ISA_MASK 0x00007ffffffffff8ULL
#endif

但具体掩码依赖:

  • CPU 架构
  • iOS 版本
  • Objective-C Runtime 版本
  • arm64 和 arm64e 的差异
  • 指针认证机制

因此 ISA_MASK 只能用于理解 Runtime 原理,不应该在业务代码中写死依赖。

八、为什么指针可以保存额外 bit

对象和类的地址通常需要满足内存对齐要求。例如 8 字节对齐的地址,其低几位通常为零:

地址:xxxx xxxx xxxx 1000
低 3 位:000

Runtime 可以利用这些没有有效地址信息的 bit 保存标记。

这就是 Non-pointer ISA 的基础之一:

地址具有对齐特征
    ↓
部分 bit 可以复用
    ↓
isa 同时保存类地址和状态信息
    ↓
通过掩码恢复类地址

九、isa 和消息发送流程

调用实例方法:

[person sayHello];

Runtime 的流程可以简化为:

发送消息
    ↓
读取 person 的 isa
    ↓
解码或通过 ISA_MASK 提取 Class
    ↓
先查方法缓存
    ↓
再查当前类的方法列表
    ↓
如果找不到,沿 superclass 向上查找
    ↓
找到 IMP 并执行

调用类方法:

[Person test];

流程大致是:

Person 类对象
    ↓ isa
Person Meta-Class
    ↓
在 Meta-Class 的方法缓存和方法列表中查找 test
    ↓
找到 IMP 并执行

如果始终找不到方法,Runtime 还会继续尝试:

动态方法解析
    ↓
快速消息转发
    ↓
完整消息转发
    ↓
仍然失败则 unrecognized selector 崩溃

十、面试中的完整回答

可以这样回答:

Objective-C 对象内部有一个 isa,用于定位对象所属的 Class。实例对象的 isa 指向类对象,类对象的 isa 指向元类。实例方法存储在 Class 的方法列表中,类方法存储在 Meta-Class 的方法列表中。

在早期架构中,isa 可以看作普通的 Class 指针。从 arm64 开始,Apple 对 isa 做了 Non-pointer ISA 优化,把类信息、对象状态和部分引用计数压缩到一个 64 位结构中。has_assoc 表示是否存在关联对象,weakly_referenced 表示是否存在弱引用,deallocating 表示对象是否正在销毁,extra_rc 保存部分引用计数。

因为 Non-pointer ISA 中同时包含地址信息和标记位,所以 Runtime 需要通过 ISA_MASK 或其他内部解码逻辑提取真实类地址。如果 isa 中的引用计数空间不足,就会使用 Side Table 保存额外信息。

Runtime 找到 Class 后,会先查询方法缓存,再查询方法列表,找不到时沿着 superclass 链继续查找。类方法则会通过 Meta-Class 的方法列表查找。

十一、使用时的边界

isa_t、位域布局、ISA_MASK 和 Side Table 都属于 Objective-C Runtime 的内部实现细节,不是稳定的业务 API。

不同系统版本和架构可能存在差异,尤其是 arm64e 还涉及指针认证。因此这些内容适合用于:

  • 学习 Runtime 原理
  • 阅读源码
  • 面试准备
  • 使用调试工具分析对象

不建议在业务代码中:

  • 手动修改 isa
  • 写死 ISA_MASK
  • 依赖具体位域位置
  • 假设所有系统版本布局完全相同

最后可以记住:

isa:我是什么类型
superclass:我的父类是谁
Class:保存实例方法和类结构
Meta-Class:保存类方法
ISA_MASK:从编码后的 isa 中提取类地址
extra_rc:保存部分额外引用计数
Side Table:保存 isa 放不下的额外 Runtime 信息