2、Runtime的核心概念:isa指针、元类(MetaClass)、类对象与实例对象的关系
好,咱们今天来聊聊Runtime里最核心的几个概念。说实话,很多人学iOS开发好几年,天天用[obj doSomething],但底层到底怎么找到这个方法的?对象和类到底是什么关系?这些问题一问就懵了。
我个人觉得,搞懂isa指针、元类、类对象和实例对象的关系,是理解Runtime的“第一道坎”。跨过去,后面就豁然开朗了。
2.1 先搞清楚:实例对象、类对象、元类对象
咱们先看个最基础的代码:
// 定义一个类
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
- (void)sayHello;
@end
@implementation Person
- (void)sayHello {
NSLog(@"Hello, I'm %@", self.name);
}
@end
// 使用
Person *p = [[Person alloc] init];
p.name = @"Tom";
[p sayHello];
这段代码里,p是什么?Person是什么?很多人觉得p是对象,Person是类。没错,但不够准确。
在Runtime的世界里,一切皆对象。你想想看:
- 实例对象:就是
p,通过alloc创建出来的。它存储了成员变量的值(比如name = @"Tom")。 - 类对象:就是
[Person class]返回的那个东西。它存储了实例方法列表、属性列表、协议列表等。每个类在内存中只有一个类对象。 - 元类对象:这个稍微抽象一点。它存储了类方法列表。每个类也只有一个元类对象。
我刚开始学的时候,觉得元类这东西太绕了。后来在项目中遇到一个坑——动态添加类方法时死活调不到,一查才发现是元类对象没搞对。嗯,这里要注意。
2.2 isa指针:对象的“身份证”
每个Objective-C对象都有一个isa指针。它指向什么?
- 实例对象的isa:指向类对象。
- 类对象的isa:指向元类对象。
- 元类对象的isa:指向根元类对象(通常是
NSObject的元类)。 - 根元类对象的isa:指向它自己。
说白了,isa就是用来回答“你是谁?你从哪里来?”这个问题的。Runtime通过isa指针找到对象的类,然后去类的方法列表里查找要调用的方法。
核心记忆点:
- 实例对象 → isa → 类对象(找实例方法)
- 类对象 → isa → 元类对象(找类方法)
- 元类对象 → isa → 根元类对象(形成闭环)
2.3 元类(MetaClass):类方法的“家”
为什么需要元类?我举个例子你就明白了。
// 类方法
+ (void)classMethod {
NSLog(@"This is a class method");
}
// 调用
[Person classMethod];
当Runtime执行[Person classMethod]时,它怎么找到这个方法?
流程是这样的:
- 拿到
Person类对象(注意,这里Person本身也是一个对象)。 - 通过
Person类对象的isa指针,找到它的元类对象。 - 在元类对象的方法列表中查找
classMethod。 - 找到了,就执行。
你看,类方法其实是被存储在元类对象里的。元类就是专门用来存放类方法的“容器”。
避坑指南:我曾经在动态添加方法时,用class_addMethod给类对象添加了一个类方法,结果死活调不到。后来才发现,类方法要添加到元类对象上,而不是类对象本身。正确的做法是:
// 获取元类对象
Class metaClass = objc_getMetaClass("Person");
// 添加类方法到元类
class_addMethod(metaClass, @selector(classMethod), (IMP)myClassMethodImp, "v@:");
2.4 继承链:isa和superclass的“双链”结构
Objective-C的继承体系,其实是两条链交织在一起的:
- isa链:用于方法查找(实例方法走isa→类对象,类方法走isa→元类对象)。
- superclass链:用于继承(子类找不到方法,就去父类找)。
我画了一张图,帮你理清这个关系:
这张图里,红色箭头是isa链,蓝色箭头是superclass链。你仔细看看:
- 实例对象
p的isa指向Person类对象。 Person类对象的isa指向它的元类对象。Person类对象的superclass指向NSObject类对象。Person元类对象的superclass指向NSObject元类对象(根元类)。- 根元类对象的isa指向自己,形成闭环。
注意:根元类对象的isa指向自己,这是Runtime设计上的一个“巧妙”之处。如果不这样设计,isa链就会无限递归下去。我记得第一次看到这个设计时,觉得苹果这帮人真聪明。
2.5 方法查找的完整流程
现在咱们把isa和superclass结合起来,看看一个方法调用到底是怎么找到的。
场景一:调用实例方法
[p sayHello];
查找流程:
- 通过
p的isa找到Person类对象。 - 在
Person类对象的方法列表中查找sayHello。 - 如果没找到,通过
Person类对象的superclass找到NSObject类对象。 - 在
NSObject类对象的方法列表中查找sayHello。 - 如果还没找到...嗯,那就进入消息转发流程了(后面章节会讲)。
场景二:调用类方法
[Person classMethod];
查找流程:
- 通过
Person类对象的isa找到它的元类对象。 - 在元类对象的方法列表中查找
classMethod。 - 如果没找到,通过元类对象的superclass找到父类的元类对象。
- 继续查找,直到根元类对象。
个人经验:我在做AOP(面向切面编程)时,经常需要hook类方法。这时候一定要搞清楚,你要hook的是元类对象上的方法,而不是类对象上的。我曾经因为搞混了这个,导致hook了实例方法却以为hook了类方法,排查了半天。
2.6 一张表总结
| 对象类型 | 存储内容 | isa指向 | superclass指向 |
|---|---|---|---|
| 实例对象 | 成员变量值 | 类对象 | 无(实例对象没有superclass) |
| 类对象 | 实例方法列表、属性、协议 | 元类对象 | 父类类对象 |
| 元类对象 | 类方法列表 | 根元类对象 | 父类元类对象 |
| 根元类对象 | 根类方法 | 自己 | 根类类对象 |
说白了,整个Runtime的消息传递机制,就是建立在isa和superclass这两条链上的。你只要记住:
- 找实例方法:顺着isa到类对象,再顺着superclass往上找。
- 找类方法:顺着isa到元类对象,再顺着superclass往上找。
嗯,核心概念就这些。搞懂了isa、元类、类对象和实例对象的关系,后面再讲消息发送、消息转发、Method Swizzling这些,你就不会觉得晕了。
公众号:蓝海资料掘金营,微信deep3321