3、isa指针与元类:isa指针结构、类对象与元类、继承链与isa走位

好,咱们今天聊点硬核的。isa指针和元类,这俩概念我当年刚接触时也是一头雾水。说实话,很多iOS开发者工作两三年了,对这块还是模模糊糊。但你要想真正理解Runtime,这关必须过。

3.1 isa指针的结构

先说说isa指针。每个Objective-C对象,都有一个isa指针。它指向什么?指向这个对象的类。但问题来了——在64位架构下,isa指针不再是一个简单的地址了。

我记得有一次,我在调试一个内存泄漏问题时,发现isa指针的值很奇怪。后来一查,才知道苹果对isa做了优化。它现在是一个联合体(union),里面塞了很多位域信息。

isa指针的位域结构(arm64)

union isa_t {
    Class cls;
    uintptr_t bits;
    struct {
        uintptr_t nonpointer        : 1;  // 是否开启指针优化
        uintptr_t has_assoc         : 1;  // 是否有关联对象
        uintptr_t has_cxx_dtor      : 1;  // 是否有C++析构函数
        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; // 额外的引用计数
    };
};

看到没?一个isa指针,硬生生被拆成了这么多字段。说白了,苹果为了性能,把引用计数、类指针、弱引用标记等信息全塞进了一个指针里。这就是所谓的non-pointer isa

我个人习惯在分析Crash日志时,会特别关注isa的bits值。有一次,我发现一个对象的isa的deallocating位被置1了,但引用计数还没归零。嗯,这就是典型的过度释放问题。

小技巧: 你可以用object_getClass()来获取isa指向的类,但要注意,如果isa开启了nonpointer优化,直接读取isa指针拿到的不是完整地址,需要做位掩码操作。

3.2 类对象与元类

好,接下来是重头戏。类对象是什么?元类又是什么?

你想想看,我们平时写的[NSObject alloc],这个alloc方法是谁实现的?是NSObject这个类对象。但类对象本身也是一个对象,它也有isa指针。它的isa指向谁?指向元类(Meta Class)

说白了,元类就是类对象的类。它负责存储类方法。每个类都有且只有一个元类。

类对象 vs 元类

概念 存储内容 isa指向
实例对象 成员变量值 类对象
类对象 实例方法列表、属性、协议 元类
元类 类方法列表 根元类

我在项目中遇到过一个问题:动态添加类方法时,死活调不到。后来发现,我往类对象上添加了方法,但类方法应该往元类上添加。这个坑,我踩过一次就记住了。

3.3 继承链与isa走位

现在我们把isa和继承链结合起来看。这就是Runtime最精妙的地方——isa走位

画个图你就明白了:

实例对象 (obj) isa 类对象 (Person) isa + 实例方法 元类 (Person Meta) 类方法 根元类 (NSObject Meta) 根类对象 (NSObject) isa isa isa superclass superclass superclass isa 实线箭头 = isa 指向 虚线箭头 = superclass 继承 注意:根元类的isa指向自己 根元类的superclass指向根类对象

这张图我建议你多看几遍。核心规律就两条:

  • isa走位: 实例 → 类 → 元类 → 根元类(根元类的isa指向自己)
  • 继承走位: 子类 → 父类 → 根类(根类的superclass为nil)

为什么会这样设计?我个人的理解是:为了统一消息发送机制。不管是实例方法还是类方法,底层走的都是objc_msgSend。通过isa找到类或元类,再通过superclass沿着继承链往上找,直到找到方法的实现。

注意: 根元类的isa指向自己,这是一个闭环。根元类的superclass指向根类对象(NSObject)。这意味着,类方法找不到时,会去根类对象里找实例方法。这就是为什么[NSObject class]能调用的原因。

3.4 实际应用场景

说了这么多理论,咱们看看实际中怎么用。

场景一:动态判断类型

// 判断对象是否为某个类的实例
if ([obj isKindOfClass:[Person class]]) {
    // 这里会沿着继承链比较
}

// 判断对象是否精确为某个类的实例
if ([obj isMemberOfClass:[Person class]]) {
    // 这里只比较isa指向的类
}

场景二:KVO的实现原理

我记得有一次排查KVO崩溃,发现isa指针被动态替换了。KVO会在运行时动态创建一个子类,然后把对象的isa指向这个子类。这就是isa swizzling。

// KVO后,isa指向了 NSKVONotifying_Person
// 而不是原来的 Person
Class cls = object_getClass(obj);
NSLog(@"%@", NSStringFromClass(cls));
// 输出: NSKVONotifying_Person

场景三:消息转发

当方法找不到时,Runtime会沿着isa和superclass走位,触发消息转发流程。我曾经用这个机制实现过一个动态代理,拦截所有未实现的方法调用。

避坑指南: 我曾经在重写methodSignatureForSelector:时,忘记返回正确的方法签名,导致程序直接crash。记住,消息转发的三个步骤缺一不可:动态方法解析 → 备用接收者 → 完整消息转发。

3.5 小结

isa指针和元类,说白了就是Runtime的基石。没有它们,消息发送、动态方法解析、KVO这些机制都无从谈起。

我个人建议,你可以在Xcode里打断点,用lldb命令p (Class)object_getClass(obj)来查看isa指向。亲手验证一下,比看十篇文章都管用。

嗯,这一章就到这里。记住那张isa走位图,面试时能帮你解决80%的问题。


公众号:蓝海资料掘金营,微信deep3321