一、为什么要理解 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 对象保存类的结构信息,例如:
isasuperclass- 实例方法列表
- 方法缓存
- 属性列表
- 成员变量列表
- 协议列表
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 信息