OC对象的本质之:isa和superclass

213 阅读7分钟

有点儿Objective-C基础的应该都知道,当实例对象调用一个对象方法时,底层实际上都是转成了调用objc_msgSend函数。 我们在之前的OC对象的本质之:对象的类型中已经提过,实例对象(instance对象)中存储的仅仅是isa和成员变量,并没有对象方法,对象方法是存储在类对象(class对象)中的;而类对象(class对象)中也没有存储类方法,类方法是存储在元类对象(meta-class对象)中的。

那么我们来想一个问题:对象方法并没有存储在实例对象里,那实例对象是如何调用存储在类对象中的对象方法呢?类方法并没有存储在类对象里,类对象又是如何调用存储在元类对象中的类方法呢?

没错,就是通过isa指针。

isa

  • 实例对象中的isa指针指向的是类对象的内存地址,通过isa找到类对象然后调用对象方法的实现;类对象中的isa指针指向的是元类对象的内存地址,通过isa找到元类对象然后调用类方法的实现。

对象的种类_isa.png

我们来求证一下:

@interface Person : NSObject
@end

@implementation Person
@end

Person *per = [[Person alloc] init];
Class perClass = [Person class];

按照我们上面的说法,perisa存的地址值就是perClass的地址。 image.png

我们借助lldb指令来打印一下:

(lldb) p/x (long)per->isa
输出:(long) $1 = 0x011d800100008261

(lldb) p/x perClass
输出:(Class) $2 = 0x0000000100008260 Person

嗯?怎么不一样?怎么回事儿? 事实上,实例对象中的isa指针指向的是类对象的内存地址是很久以前的沿袭下来的"照惯例"的说法,在64bit之前确实就是如此。从64bit开始,isa需要进行一次位运算,才能计算出真实的地址。

isa_&mask.png 我们在objc源码中找到ISA_MASK的值:

arm64:0x007ffffffffffff8ULL
x86_64:0x00007ffffffffff8ULL

我当前的项目是MacOS - Command Line Tool,所以我们用0x00007ffffffffff8ULL来验证一下:

(lldb) p/x (long)per->isa & 0x00007ffffffffff8ULL
输出:(unsigned long long) $3 = 0x0000000100008260

是了!!!!! 但是一般我们还是会“简而言之”的说实例对象中的isa指针指向的是类对象的内存地址,图个方便。

元类的isa指针又指向哪里呢?指向的是它的基类的元类对象。 基类的元类对象中也有isa指针,它又指向哪里呢?指向的是它自己。

isa.png

class对象的superclass

可以看到,class对象(类对象)、meta-class对象(元类对象)中都有一个superclass指针,那么它的作用是什么呢?

我定义了PersonStudent两个类如下:

@interface Person : NSObject
- (void)eat;
@end

@interface Student : Person
- (void)sing;
@end
Student *stu = [[Student alloc] init];
[stu sing];
[stu eat];
[stu init];
  • 当Student的实例对象stu调用sing方法的时候,是通过isa指针找到类对象,然后调用存储在类对象中的对象方法sing的实现。

那么当Student的实例对象stu调用父类Person中的eat方法的时候,它又是怎么找到的呢?

没错,就是通过superclass指针。

superclass_class.png

  • 当Student的实例对象stu调用父类Person中的eat方法的时候,会先通过isa找到类对象Student class,然后通过superclass找到父类的类对象Person classPerson class中就存储着对象方法eat方法的实现。

  • 当Student的实例对象stu调用NSObject中的init方法的时候,会先通过isa找到类对象Student class,然后通过superclass找到父类的类对象Person classPerson class再通过superclass指针找到它的父类NSObject的类对象NSObject classNSObject class中就存储着对象方法init的实现。

meta-class对象的superclass

@interface Person : NSObject
- (void)eat;
+ (void)sleep;
@end

@interface Student : Person
- (void)sing;
+ (void)dance;
@end
[Student dance];
[Student sleep];
[Student alloc];

superclass_meta-class.png

  • Student要调用类方法dance的时候,是通过isa指针找到元类对象,然后调用存储在元类对象中的类方法dance的实现。

  • Student要调用父类Person中的类方法sleep的时候,会先通过isa找到它的元类对象,它的元类对象Student meta-class通过superclass指针找到父类Person的元类对象Person meta-classPerson meta-class中就存储着类方法sleep方法的实现。

  • Student要调用NSObject中的alloc方法的时候,会先通过isa找到它的元类对象,它的元类对象Student meta-class通过superclass指针找到父类Person的元类对象Person meta-classPerson meta-class再通过superclass指针找到它的父类NSObject的元类对象NSObject meta-classNSObject meta-class中就存储着类方法alloc的实现。

总结一下:类对象的superclass指针指向的是它父类的类对象,元类对象的superclass指针指向的是它父类的元类对象。

总结

网上找了一张我觉得总结得非常到位的一张图:

isa、superclass总结图.webp

  • 实例对象的isa指向类对象;类对象的isa指向元类对象;元类对象的isa指向基类的元类对象;基类的元类对象的isa指向它自己

  • 类对象的superclass指向父类的类对象,如果没有父类,superclass指针为nil

  • 元类对象的superclass指向父类的元类对象,基类的元类对象指向基类的类对象

    唯有这里的基类的元类对象指向基类的类对象可能比较令人费解,我们知道:元类对象里存储的是类方法,类对象里存储的是对象方法,那为什么会有这个指向呢?我们通过代码来看一下: 先来看一下应该没有任何疑义的:

Person类:
@interface Person : NSObject
+ (void)rap;
+ (void)playBasketball;
@end

@implementation Person
+ (void)rap {
    NSLog(@"Person: %@(%p) rap", NSStringFromClass(self), self);
}
@end


NSObject分类:
@interface NSObject (Rap)
+ (void)rap;
+ (void)playBasketball;
@end

@implementation NSObject (Rap)
+ (void)rap {
    NSLog(@"NSObject: %@(%p) rap", NSStringFromClass(self), self);
}
+ (void)playBasketball {
    NSLog(@"NSObject: %@(%p) playBasketball", NSStringFromClass(self), self);
}
@end

使用:
NSLog(@"Person class:%p\nNSObject class:%p",[Person class], [NSObject class]);
[Person rap];
[NSObject rap];
[Person playBasketball];

输出:
Person class:0x100008260
NSObject class:0x7ff84ced9ab0
Person: Person(0x100008260) rap
NSObject: NSObject(0x7ff84ced9ab0) rap
NSObject: Person(0x100008260) playBasketball


解读:
- Person类对象调用类方法rap,会通过isa指针找到它的元类对象Person meta-classPerson meta-class中存储了rap的实现;
- NSObject类对象调用类方法rap,会通过isa指针找到它的元类对象NSObject meta-classNSObject meta-class中存储了rap的实现;
- Person类对象调用类方法playBasketball,会通过isa指针找到它的元类对象Person meta-classPerson meta-class中并没有playBasketball的实现,所以它的superclass指针再找到基类的元类对象NSObject meta-classNSObject meta-class中存储了playBasketball的实现。

上面的代码应该是没有任何疑义的。我们再来修改一下NSObject(Rap)中代码,把+ (void)playBasketball;的实现去掉,改成对象方法的实现:

@interface NSObject (Rap)
+ (void)rap;
+ (void)playBasketball;
@end

@implementation NSObject (Rap)
+ (void)rap {
    NSLog(@"NSObject: %@(%p) rap", NSStringFromClass(self), self);
}
- (void)playBasketball {
    NSLog(@"NSObject对象方法: %@(%p) playBasketball", NSStringFromClass(self), self);
}
@end

那么此时两个类的+ (void)playBasketball;都是没有实现的,我们这时候调用[Person playBasketball];,会发生什么呢?

NSLog(@"Person class:%p\nNSObject class:%p",[Person class], [NSObject class]);
[Person playBasketball];

输出:
Person class:0x100008268
NSObject class:0x7ff84ced9ab0
NSObject对象方法: Person(0x100008268) playBasketball

居然没有抛出错误,而是调用了NSObject(Rap)中的对象方法- (void)playBasketball,有没有觉得很费解?这就是我们上面说的基类的元类对象指向基类的类对象

我们都知道,当我们调用方法的时候,底层都是转成了objc_msgSend函数,那么当Person调用类方法playBasketball的时候,实则是转成了类似这样结构东西: objc_msgSend([Person class], @selector(playBasketball)),它只知道是给Person class发送了这样一条消息,但并不会知道playBasketball是类方法还是对象方法。

所以,当我们向Person class发送playBasketball消息的时候,它先通过isa找到元类对象Person meta-classPerson meta-class中找不到,再通过superclass指针找到它的基类的元类对象NSObject meta-classNSObject meta-class中也没有,再通过superclass指针找到基类的类对象NSObject class,而NSObject class中则有名为playBasketball的实现,所以调用成功。