Runtime的本质4-super调用的本质

1. super的本质

1.1 问题

首先来看一道面试题:

// 下列代码中Person继承自NSObject,Student继承自Person,写出下列代码输出内容。

#import "Student.h"
@implementation Student
- (instancetype)init
{
    if (self = [super init]) {
        NSLog(@"[self class] = %@", [self class]);
        NSLog(@"[self superclass] = %@", [self superclass]);
        NSLog(@"----------------");
        NSLog(@"[super class] = %@", [super class]);
        NSLog(@"[super superclass] = %@", [super superclass]);

    }
    return self;
}
@end

输出:

2020-02-13 15:45:13.848176+0800 Runtime的本质4[30347:13168682] [self class] = Student
2020-02-13 15:45:13.848628+0800 Runtime的本质4[30347:13168682] [self superclass] = Person
2020-02-13 15:45:13.848718+0800 Runtime的本质4[30347:13168682] ----------------
2020-02-13 15:45:13.848786+0800 Runtime的本质4[30347:13168682] [super class] = Student
2020-02-13 15:45:13.848836+0800 Runtime的本质4[30347:13168682] [super superclass] = Person

上述代码中可以发现无论是self还是super调用classsuperclass的结果都是相同的。

为什么结果是相同的?super关键字在调用方法的时候底层调用流程是怎样的?

1.2 super调用的本质

我们通过一段代码来看一下super调用的底层实现,为Person类提供run方法,Student类中重写run方法,方法内部调用[super run];,将Student.m转化为c++代码查看其底层实现:

- (void) run
{
    [super run];
    NSLog(@"Student...");
}

上述代码转化为c++代码

static void _I_Student_run(Student * self, SEL _cmd) {
    
    ((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("run"));
    
    
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_jm_dztwxsdn7bvbz__xj2vlp8980000gn_T_Student_e677aa_mi_0);
}

通过上述源码可以发现,[super run]转化为底层源码内部其实调用的是objc_msgSendSuper函数。

objc_msgSendSuper函数内传递了两个参数:

  1. __rw_objc_super结构体
  2. sel_registerName("run")方法名。

__rw_objc_super结构体内传入的参数是selfclass_getSuperclass(objc_getClass("Student"))

class_getSuperclass也就是Student的父类Person

首先我们找到objc_msgSendSuper函数查看内部结构:

OBJC_EXPORT id _Nullable
objc_msgSendSuper(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
    OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);

可以发现objc_msgSendSuper中传入的结构体是objc_super,我们来到objc_super内部查看其内部结构。

我们通过源码查找objc_super结构体查看其内部结构:

// 精简后的objc_super结构体
struct objc_super {
    __unsafe_unretained _Nonnull id receiver; // 消息接受者
    __unsafe_unretained _Nonnull Class super_class; // 消息接受者的父类
    /* super_class is the first class to search */ 
    // 父类是第一个开始查找的类
};

objc_super结构体中可以发现receiver消息接受者仍然为selfsuper_class仅仅是用来告知消息查找从哪一个类开始,是直接从父类的类对象开始去查找。

我们通过一张图看一下其中的区别:

runtime_selfsuper

从上图中我们知道

super调用方法的消息接受者receiver仍然是self,只是从父类Person的类对象开始去查找方法。

class方法的实现

那么此时重新回到面试题,我们知道class的底层实现如下面代码所示:

+ (Class)class {
    return self;
}

- (Class)class {
    return object_getClass(self);
}

class方法内部实现是根据消息接受者返回其对应的类对象,最终会找到基类NSObject的方法列表中。

selfsuper的区别仅仅是self从本类类对象开始查找方法,super从父类类对象开始查找方法,它两的消息接受者都是一样的,在这儿消息接收者都是self,因此最终得到的结果都是相同的。

另外我们在回到run方法内部,很明显可以发现,如果super不是从父类开始查找方法,从本类查找方法的话,就调用方法本身造成循环调用方法而crash

superclass方法的实现

同理superclass底层实现同class类似,其底层实现代码如下入所示

+ (Class)superclass {
    return self->superclass;
}

- (Class)superclass {
    return [self class]->superclass;
}

因此得到的结果也是相同的。

2. isKindOfClassisMemberOfClass

isKindOfClass isKindOfClass实例方法底层实现:

- (BOOL)isMemberOfClass:(Class)cls {
   // 直接获取调用者实例类对象并判断是否等于传入的类对象
    return [self class] == cls;
}

- (BOOL)isKindOfClass:(Class)cls {
   // 向上查询调用者父类对象等于传入的类对象则返回YES
   // 直到基类还不相等则返回NO
    for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

isKindOfClass isKindOfClass类方法底层实现:

// 判断调用者元类对象是否等于传入的元类元类对象
// 此时self是类对象 object_getClass((id)self)获取到的就是元类
+ (BOOL)isMemberOfClass:(Class)cls {
    return object_getClass((id)self) == cls;
}

// 向上查找调用者元类对象是否等于传入的元类对象
// 如果找到基类还不相等则返回NO
// 注意:这里会找到基类
+ (BOOL)isKindOfClass:(Class)cls {
    for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
        if (tcls == cls) return YES;
    }
    return NO;
}

通过上述源码分析我们可以知道:

  1. isMemberOfClass 判断左边类对象或者元类对象是否刚好等于右边类型。
  2. isKindOfClass 判断左边或者左边类型的父类是否刚好等于右边类型(右边的类型是否是左边类型的子类)。

注意:类方法内部是获取其元类对象进行比较

我们查看以下代码

NSLog(@"%d",[Person isKindOfClass: [Person class]]);
NSLog(@"%d",[Person isKindOfClass: object_getClass([Person class])]);
NSLog(@"%d",[Person isKindOfClass: [NSObject class]]);

//输出:
2020-02-13 17:30:38.439190+0800 Runtime的本质4[38056:13260008] 0
2020-02-13 17:30:38.439242+0800 Runtime的本质4[38056:13260008] 1
2020-02-13 17:30:38.439313+0800 Runtime的本质4[38056:13260008] 1

分析上述输出内容:

  1. 第一个 0:上面提到过类方法是获取self的元类对象与传入的参数进行比较,但是[Person class]获取到的是类对象,因此返回NO
  2. 第二个 1:同上,此时我们传入Person元类对象,此时返回YES。验证上述说法
  3. 第三个 1:我们发现此时传入[NSObject class]的是NSObject类对象并不是元类对象,但是返回的值却是YES
    原因是基元类的superclass指针是指向基类对象NSObject的。如下图13号线
isa_index_pointer

那么Person元类通过superclass指针一直找到基元类,还是不相等,此时再次通过superclass指针来到基类,那么此时发现相等就会返回YES了。

通过以上验证我看可以总结出如下的用法:

  1. 通过实例方法来调用,传入的参数应该是类对象
  2. 通过类方法来调用,传入的参数应该是元类对象

3. 函数栈的内存分布

通过一道面试题对之前学习的知识进行复习。

//以下代码是否可以执行成功,如果可以,打印结果是什么。

// Person.h
#import 
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
- (void)test;
@end

// Person.m
#import "Person.h"
@implementation Person
- (void)test
{
    NSLog(@"test print name is : %@", self.name);
}
@end

// ViewController.m
@implementation ViewController
- (void)viewDidLoad {
    [super viewDidLoad];
    
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj test];
    
    Person *person = [[Person alloc] init];
    [person test];
}

这道面试题确实很无厘头的一道题,日常工作中没有人这样写代码,但是需要解答这道题需要很完备的底层知识,我们通过这道题来复习一下,首先看一下打印结果。

2020-02-13 17:43:09.307047+0800 Runtime的本质4[38983:13272769] test print name is : 
2020-02-13 17:43:09.307180+0800 Runtime的本质4[38983:13272769] test print name is : (null)

通过上述打印结果我们可以看出,是可以正常运行并打印的,说明obj可以正常调用test方法,但是我们发现打印self.namee的内容却是。下面person实例调用test不做过多解释了,主要用来和上面方法调用做对比。

为什么会是这样的结果呢?首先通过一张图看一下两种调用方法的内存信息。

runtime_memory_review

通过上图我们可以发现两种方法调用方式很相近。那么obj为什么可以正常调用方法?

obj为什么可以正常调用方法?

首先通过之前的学习我们知道,person调用方法时首先通过isa指针找到类对象进而查找方法并进行调用。

person实例对象内实际上是取最前面8个字节空间(指针类型在64位占8字节)也就是isa,并通过计算得出类对象地址。

而通过上图我们可以发现,obj在调用test方法时,也会通过其内存地址找到cls,而cls中取出最前面8个字节空间其内部存储的刚好是Person类对象地址。因此obj是可以正常调用方法的。

super调用的时候会创建局部变量

问题出在[super viewDidLoad]这段代码中,通过上述对super本质的分析我们知道,super内部调用objc_msgSendSuper2函数。

我们知道objc_msgSendSuper2函数内部会传入两个参数,objc_super2结构体和SEL,并且objc_super2结构体内有两个成员变量消息接受者和其父类。

struct objc_super2 {
    id receiver; // 消息接受者
    Class current_class; // 当前类
};

通过以上分析我们可以得知[super viewDidLoad]内部objc_super2结构体内存储如下所示

struct objc_super2 = {
    self,
    [ViewController Class]
};

那么objc_msgSendSuper2函数调用之前,会先创建局部变量结构体objc_super2结构体,用于为objc_msgSendSuper2函数传递的参数。

为什么会调用objc_msgSendSuper2函数,后面会证明。

3.1 函数局部变量由高地址向低地址分配在栈空间

局部变量是存储在栈空间内的,并且是由高地址向低地址有序存储。

我们通过一段代码验证一下。

long long a = 1;
long long b = 2;
long long c = 3;
NSLog(@"%p %p %p", &a,&b,&c);

// 输出:
0x7ffee36dfc20 0x7ffee36dfc18 0x7ffee36dfc10

通过上述代码打印内容,我们可以验证局部变量在栈空间内是由高地址向低地址连续存储的。

那么我们回到题中,通过上述分析我们知道,此时代码中包含局部变量依次为:objc_super2 结构体、clsobj。通过一张图展示一下这些局部变量存储结构。

runtime_super_review

上面我们知道当person实例对象调用方法的时候,会取实例变量前8个字节空间也就是isa来找到类对象地址。那么当访问实例变量的时候,就跳过isa的8个字节空间往高地址去找实例变量。

那么当obj在调用test方法的时候同样找到cls中取出前8个字节,也就是Person类对象的内存地址,那么当访问成员变量_name的时候,会继续向高地址内存空间查找,此时就会找到objc_super结构体,从中取出8个字节空间也就是self,因此此时访问到的self.name就是ViewController对象。

当访问成员变量_name的时候,test函数中的self也就是方法调用者其实是obj,那么self.name就是通过obj去找_name,跳过cls的8个指针,再取8个指针,此时自然获取到ViewController对象。

因此上述代码中cls就相当于isaisa下面的8个字节空间就相当于_name成员变量。因此成员变量_name的访问到的值就是cls地址后向高地址位取8个字节地址空间存储的值。

为了验证上述说法,我们做一个实验,在cls后高地址中添加一个string,那么此时cls下面的高地址位就是string。以下示例代码

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSString *string = @"string";
    
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj test];
    
    Person *person = [[Person alloc] init];
    [person test];
}

此时的局部变量内存结构如下图所示

runtime_super_review1

此时在访问_name成员变量的时候,越过cls内存往高地址找就会来到stringstring也是指针类型,占用8字节),此时拿到的成员变量就是string了。

输出:

2020-02-13 18:50:45.454119+0800 Runtime的本质4[44051:13338753] test print name is : string
2020-02-13 18:50:45.454212+0800 Runtime的本质4[44051:13338753] test print name is : (null)
Tips:

通过之前的底层学习,我们知道了一个OC的函数在底层默认是有两个参数self_cmd的,但是在这儿确没有入栈空间,这是因为arm64架构下,函数的参数一般都是通过寄存器来存储的,通过寄存器来操作内存效率会更高。

3.2 其他情况

再通过一段代码使用int数据进行试验

- (void)viewDidLoad {
    [super viewDidLoad];

    int a = 3;
    
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj test];
    
    Person *person = [[Person alloc] init];
    [person test];
}
// 程序crash,坏地址访问

我们发现程序因为坏地址访问而crash,此时局部变量内存结构如下图所示

runtime_super_review2

当需要访问_name成员变量的时候,会在cls后的高地址查找8位的字节空间,而我们知道int占4位字节,那么此时8位的内存空间同时占据int数据及objc_super结构体内,因此就会造成坏地址访问而crash

我们添加新的成员变量进行访问

// Person.h
#import 
@interface Person : NSObject
@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *nickName;
- (void)test;
@end
------------
// Person.m
#import "Person.h"
@implementation Person
- (void)test
{
    NSLog(@"test print name is : %@", self.nickName);
}
@end
--------
//  ViewController.m
- (void)viewDidLoad {
    [super viewDidLoad];

    NSObject *obj1 = [[NSObject alloc] init];
    
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj test];
    
    Person *person = [[Person alloc] init];
    [person test];
}

输出:

2020-02-13 19:04:55.572495+0800 Runtime的本质4[45176:13357077] test print name is : 
2020-02-13 19:04:55.572609+0800 Runtime的本质4[45176:13357077] test print name is : (null)

可以发现此时打印的仍然是ViewController对象,我们先来看一下其局部变量内存结构

runtime_super_review3

首先通过obj找到clscls找到类对象进行方法调用,此时在访问nickName时,obj查找成员变量,首先跳过8字节的cls,之后跳过第一个成员变量_name所占的8字节空间,最终再取8字节空间取出其中的值作为成员变量的值,那么此时也就是self了。

为什么会跳过_name的8个字节呢?

因为Person类里面有2个属性namenickName,现在访问的是nickNamename在低地址、nickname在高地址

@property (nonatomic, strong) NSString *name;
@property (nonatomic, strong) NSString *nickName;

- (void)test {
    NSLog(@"test print name is : %@", self.nickName);
}

现在我们把为Person类里面有2个属性交换位置

@property (nonatomic, strong) NSString *nickName;
@property (nonatomic, strong) NSString *name;

- (void)test {
    NSLog(@"test print name is : %@", self.nickName);
}

输出:

2020-02-13 19:29:43.272317+0800 Runtime的本质4[47065:13384489] test print name is : 
2020-02-13 19:29:43.272413+0800 Runtime的本质4[47065:13384489] test print name is : (null)

我们发现直接输出了obj1对象,原因是当跳过了8个字节的cls之后,会直接取8个字节空间取出其中的值作为nickName的值,因为现在的nickName在低地址。

现在我们再修改test方法如下:

- (void)test {
    NSLog(@"test print name is : %@", self.name);
}

输出:

2020-02-13 19:34:38.304233+0800 Runtime的本质4[47437:13389601] test print name is : 
2020-02-13 19:34:38.304346+0800 Runtime的本质4[47437:13389601] test print name is : (null)

我们发现又打印出了ViewController,原因上面已经说明了。

3.3 总结

总结:这道面试题虽然很无厘头,让人感觉无从下手但是考察的内容非常多。

  1. super的底层本质为调用objc_msgSendSuper2函数,传入objc_super2结构体,结构体内部存储消息接受者和当前类,用来告知系统方法查找从父类开始。

  2. 局部变量分配在栈空间,并且从高地址向低地址连续分配。先创建的局部变量分配在高地址,后续创建的局部变量连续分配在较低地址。

  3. 方法调用的消息机制,通过isa指针找到类对象进行消息发送。

  4. 指针存储的是实例变量的首字节地址,上述例子中person指针存储的其实就是实例变量内部的isa指针的地址。

  5. 访问成员变量的本质,找到成员变量的地址,按照成员变量所占的字节数,取出地址中存储的成员变量的值。

4. super调用的更深入探究

4.1 objc_msgSendSuper2函数

我们在第2节的时候知道了通过super调用的时候,OC代码转化为c++代码底层会调用objc_msgSendSuper

// 精简后的objc_super结构体
struct objc_super {
    __unsafe_unretained _Nonnull id receiver; // 消息接受者
    __unsafe_unretained _Nonnull Class super_class; // 消息接受者的父类
    /* super_class is the first class to search */ 
    // 父类是第一个开始查找的类
};

但是这并不能说明super底层调用函数就一定objc_msgSendSuper

其实super底层真正调用的函数是objc_msgSendSuper2函数我们可以通过查看super调用方法转化为汇编代码来验证这一说法:

- (void)viewDidLoad {
    [super viewDidLoad];
}

通过断点查看其汇编调用栈

runtime_msgsendsuper2

上图中可以发现super底层其实调用的是objc_msgSendSuper2函数,我们来到源码中查找一下objc_msgSendSuper2函数的底层实现,我们可以在汇编文件中找到其相关底层实现。

objc源码路径:https://opensource.apple.com/source/objc4/objc4-756.2/runtime/Messengers.subproj/objc-msg-arm64.s.auto.html

ENTRY _objc_msgSendSuper2
UNWIND _objc_msgSendSuper2, NoFrame

ldp p0, p16, [x0]       // p0 = real receiver, p16 = class
ldr p16, [x16, #SUPERCLASS] // p16 = class->superclass
CacheLookup NORMAL

END_ENTRY _objc_msgSendSuper2

通过上面汇编代码我们可以发现,其实底层是在函数内部调用的class->superclass获取父类,并不是我们上面分析的直接传入的就是父类的类对象。

其实_objc_msgSendSuper2内传入的结构体为objc_super2
struct objc_super2 {
    id receiver;
    Class current_class;
};

我们可以发现objc_super2中除了消息接受者receiver,另一个成员变量current_class也就是当前类对象。

与我们上面分析的不同,_objc_msgSendSuper2函数内其实传入的是当前类对象,然后在函数内部获取当前类对象的父类,并且从父类开始查找方法。

4.2 证明结构体objc_super2内部传入的是当前类,而不是父类

我们也可以通过代码验证上述结构体内成员变量究竟是当前类对象还是父类对象。下文中我们会通过另外一道面试题验证。

我们使用以下代码来验证上文中遗留的问题

- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [Person class];
    void *obj = &cls;
    [(__bridge id)obj test];
}

上述代码的局部变量内存结构我们之前已经分析过了,真正的内存结构应该如下图所示

runtime_super_review4

通过上面对面试题的分析,我们现在想要验证objc_msgSendSuper2函数内传入的结构体参数,只需要拿到cls的地址,然后向后移8个地址就可以获取到objc_super结构体内的self,在向后移8个地址就是current_class的内存地址。通过打印current_class的内容,就可以知道传入objc_msgSendSuper2函数内部的是当前类对象还是父类对象了。

runtime_super_review5

通过上图可以发现,最终打印的内容确实为当前类对象。
因此objc_msgSendSuper2函数内部其实传入的是当前类对象,并且在函数内部获取其父类,告知系统从父类方法开始查找的。

你可能感兴趣的:(Runtime的本质4-super调用的本质)