一、isa 不只是一个指针
在 64 位设备上,指针只需要 36~40 位就能表示所有内存地址。苹果觉得剩下的位浪费了,于是把 isa 设计成了一个 union(联合体) ,把类指针和一堆标志位都塞进了这 64 位里。
这叫 Tagged Pointer / Non-pointer ISA 技术。
二、isa_t 的完整源码
// 文件:objc-private.h
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits; // 原始的 64 位值
private:
Class cls; // 类指针(只在 non-pointer isa 关闭时使用)
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // 展开后是一堆位域定义
};
...
};
ISA_BITFIELD 展开(ARM64,iOS 真机)
// 这是 ARM64 的位域定义
uintptr_t nonpointer : 1; // bit 0
uintptr_t has_assoc : 1; // bit 1
uintptr_t has_cxx_dtor : 1; // bit 2
uintptr_t shiftcls : 33; // bit 3~35 ← 类指针在这里!
uintptr_t magic : 6; // bit 36~41
uintptr_t weakly_referenced : 1; // bit 42
uintptr_t unused : 1; // bit 43
uintptr_t has_sidetable_rc : 1; // bit 44
uintptr_t extra_rc : 19; // bit 45~63
三、每一位的含义逐个解释
bit 0:nonpointer
uintptr_t nonpointer : 1;
含义: 这个 isa 是不是 "non-pointer isa"(优化过的 isa)。
0:纯指针,整个 64 位就是类地址(老设备/某些特殊情况)1:non-pointer isa,64 位里藏了很多信息
现代 iOS 设备全是 1。
bit 1:has_assoc
uintptr_t has_assoc : 1;
含义: 这个对象是否有关联对象(Associated Object)。
关联对象就是你用 objc_setAssociatedObject 给对象动态绑定的数据。
为什么需要这一位?
- 对象 dealloc 时,runtime 需要清理关联对象
- 用这一位做快速判断:
has_assoc == 0→ 跳过关联对象清理,直接释放,更快
bit 2:has_cxx_dtor
uintptr_t has_cxx_dtor : 1;
含义: 这个类(或它的父类)是否有 C++ 析构函数,或者 OC 的 .cxx_destruct 方法。
.cxx_destruct 是编译器自动生成的方法,用来清理带有 __strong 修饰的成员变量(ARC 下自动 release)。
为什么需要这一位?
- 对象 dealloc 时,如果没有需要清理的 C++ 对象,就跳过
.cxx_destruct调用 - 优化释放速度
bit 3~35:shiftcls(33位)
uintptr_t shiftcls : 33;
含义: 这 33 位才是真正的类指针(右移 3 位存储,取的时候左移 3 位还原)。
为什么只用 33 位?因为 ARM64 的内存对齐保证类地址的低 3 位永远是 0,可以省掉。
如何取出类指针?
// runtime 内部的取法
Class getClass() const {
return (Class)(shiftcls << 3); // 左移3位还原真实地址
}
bit 36~41:magic(6位)
uintptr_t magic : 6;
含义: 固定的魔数,值是 0b011010(十进制 26)。
用途: 调试用。当你看到一个 isa,如果 magic 值不对,说明这个对象已经被释放或内存被踩了(野指针)。Xcode 和 runtime 的断言会检查这个值。
bit 42:weakly_referenced
uintptr_t weakly_referenced : 1;
含义: 这个对象是否被弱引用(__weak 指针)指向过。
为什么需要这一位?
- 对象 dealloc 时,如果有弱引用指向它,需要去
SideTable(全局散列表)里把那些弱引用都清零(避免 dangling pointer) - 用这一位快速判断:
weakly_referenced == 0→ 跳过 SideTable 查找,直接释放
bit 43:unused
uintptr_t unused : 1;
含义: 目前未使用,预留位。
bit 44:has_sidetable_rc
uintptr_t has_sidetable_rc : 1;
含义: 引用计数是否溢出到了 SideTable。
正常情况下,引用计数存在 isa 的 extra_rc 里(19位,最大能存 2^19 - 1 = 524287)。如果引用计数超过了这个值,has_sidetable_rc = 1,多出来的部分存在全局的 SideTable 里。
bit 45~63:extra_rc(19位)
uintptr_t extra_rc : 19;
含义: 存储对象的引用计数 - 1。
为什么是减 1?因为对象存活时引用计数至少为 1,存 0 代表计数是 1,节省一点空间。
实际的引用计数 = extra_rc + 1(如果 has_sidetable_rc == 0)
四、如何取出 isa 里的类指针(实际代码)
// objc-object.h
inline Class objc_object::getIsa() {
if (fastpath(!isTaggedPointer())) {
return ISA();
}
// ... TaggedPointer 的特殊处理
}
inline Class objc_object::ISA(bool authenticated) {
ASSERT(!isTaggedPointer());
#if SUPPORT_INDEXED_ISA
// 某些架构用索引
...
#else
// ARM64 主路径:取 shiftcls 位,左移3位还原地址
return (Class)(isa.bits & ISA_MASK);
#endif
}
其中 ISA_MASK 在 ARM64 是 0x0000000ffffffff8ULL,作用就是取 bit 3~35。
五、元类(Metaclass)是什么?
这是 OC 最难理解的概念之一,但其实逻辑非常自洽。
问题的由来
在 OC 里,"一切皆对象"——包括类本身也是对象。
[NSString class] // 这返回的是一个对象
[NSString stringWithString:@"hello"] // 这是给"类对象"发消息
既然类也是对象,那类对象的 isa 指向哪里?
答案就是:元类(metaclass) 。
元类的定义
元类是"类的类"。它存储的是类方法(+ 方法),就像普通类存储实例方法(- 方法)一样。
对比:类 vs 元类
| 普通类(Class) | 元类(Metaclass) | |
|---|---|---|
| 本质 | objc_class 结构体 | 也是 objc_class 结构体 |
| 方法列表里存的 | 实例方法(-) | 类方法(+) |
| isa 指向 | 元类 | 根元类(NSObject 的元类) |
| superclass 指向 | 父类 | 父类的元类 |
六、完整的 isa + 继承链图
这是 OC 里最经典的一张图,一定要理解它:
isa isa isa
实例对象(inst) --------→ 类(MyClass) --------→ 元类(Meta-MyClass) ──→ 根元类
│
superclass superclass superclass │ isa(自指)
MyClass ───────→ NSObject Meta-MyClass ──────→ Meta-NSObject─┘
│ │
│ superclass = nil │ superclass
↓ ↓
(nil) NSObject(不是元类!)
用文字描述:
实例对象.isa→MyClass(类)MyClass.isa→Meta-MyClass(元类)Meta-MyClass.isa→Meta-NSObject(根元类)Meta-NSObject.isa→Meta-NSObject(自指!根元类的 isa 指向自己)
继承链:
MyClass.superclass→NSObjectNSObject.superclass→nilMeta-MyClass.superclass→Meta-NSObject(元类也有继承链)Meta-NSObject.superclass→NSObject(元类继承链的终点是 NSObject 类,不是 nil! )
七、为什么元类的继承链终点是 NSObject?
这个设计让你可以在任何类方法里调用 NSObject 的实例方法(比如 respondsToSelector:)。
// 这为什么能工作?
[MyClass respondsToSelector:@selector(doSomething)];
+respondsToSelector: 是 NSObject 的实例方法(- 方法),存在 NSObject 类里。
当你给 MyClass 发这个消息,runtime 查找路径:
Meta-MyClass(没有)
→ Meta-NSObject(没有)
→ NSObject(在这找到了!)
因为 Meta-NSObject.superclass = NSObject,所以元类链最终能访问到 NSObject 的实例方法。优雅!
八、TaggedPointer:特殊的对象
不是所有"对象"都是真正的对象(有 isa 的结构体)。
什么是 TaggedPointer?
对于一些小值对象(比如 NSNumber、NSDate、小字符串),苹果直接把值编码进指针本身,不分配堆内存。
NSNumber *num = @42;
// 在 64 位下,这个指针可能长这样:
// 0xb000000000000162 (不是真实的堆地址!)
// 最高位 1 = TaggedPointer 标志
// 低位存了 42 这个值
判断是否是 TaggedPointer
static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) {
return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
// ARM64: _OBJC_TAG_MASK = (1UL<<63),最高位为1就是 TaggedPointer
TaggedPointer 的好处
- 不需要堆分配:直接在指针里存值,
alloc时不走malloc - 不需要引用计数:也不需要 release,直接丢弃
- 更快:少了内存分配和释放的开销
九、SideTable:引用计数和弱引用的大本营
当 isa 的 extra_rc 不够用,或者有弱引用时,数据存在 SideTable 里。
struct SideTable {
spinlock_t slock; // 自旋锁,保证线程安全
RefcountMap refcnts; // 引用计数表(散列表)
weak_table_t weak_table; // 弱引用表
};
全局有 8 个(或 64 个)SideTable,通过对象地址取模来分配,减少锁竞争。
weak_table_t 弱引用表
struct weak_table_t {
weak_entry_t *weak_entries; // 弱引用条目数组
size_t num_entries;
...
};
struct weak_entry_t {
DisguisedPtr<objc_object> referent; // 被指向的对象
// 指向该对象的所有 __weak 指针地址的集合
union {
struct { weak_referrer_t *referrers; ... };
struct { weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; };
};
};
__weak 置零的过程
对象 dealloc
↓
检查 isa.weakly_referenced
↓(== 1)
去 SideTable 找 weak_entry_t
↓
遍历所有指向该对象的 __weak 指针
↓
全部置 nil
↓
从 weak_table 删除该条目
这就是为什么 __weak 指针在对象释放后自动变成 nil,而不会变成野指针——runtime 帮你清零了。
十、小结
| 概念 | 本质 | 存在哪里 |
|---|---|---|
| isa | 64位 union,含类指针+引用计数+标志位 | 每个对象的第一个字段 |
| 元类 | 存类方法的 objc_class | 全局静态区 |
| TaggedPointer | 值直接编码进指针,无堆对象 | 栈/寄存器 |
| extra_rc | 引用计数(-1)的快速存储 | isa 的高19位 |
| SideTable | 溢出引用计数 + 弱引用表 | 全局散列表 |
下一篇:延伸问题 Q&A——消息发送、方法查找、Swizzle、dealloc 全流程等