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
调用class
或superclass
的结果都是相同的。
为什么结果是相同的?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
函数内传递了两个参数:
-
__rw_objc_super
结构体 -
sel_registerName("run")
方法名。
__rw_objc_super
结构体内传入的参数是self
和class_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
消息接受者仍然为self
,super_class
仅仅是用来告知消息查找从哪一个类开始,是直接从父类的类对象开始去查找。
我们通过一张图看一下其中的区别:
从上图中我们知道
super
调用方法的消息接受者receiver
仍然是self
,只是从父类Person
的类对象开始去查找方法。
class
方法的实现
那么此时重新回到面试题,我们知道class的底层实现如下面代码所示:
+ (Class)class {
return self;
}
- (Class)class {
return object_getClass(self);
}
class
方法内部实现是根据消息接受者返回其对应的类对象,最终会找到基类NSObject
的方法列表中。
而self
和super
的区别仅仅是self
从本类类对象开始查找方法,super
从父类类对象开始查找方法,它两的消息接受者都是一样的,在这儿消息接收者都是self
,因此最终得到的结果都是相同的。
另外我们在回到run
方法内部,很明显可以发现,如果super
不是从父类开始查找方法,从本类查找方法的话,就调用方法本身造成循环调用方法而crash
。
superclass
方法的实现
同理superclass
底层实现同class
类似,其底层实现代码如下入所示
+ (Class)superclass {
return self->superclass;
}
- (Class)superclass {
return [self class]->superclass;
}
因此得到的结果也是相同的。
2. isKindOfClass
与 isMemberOfClass
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;
}
通过上述源码分析我们可以知道:
-
isMemberOfClass
判断左边类对象或者元类对象是否刚好等于右边类型。 -
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
分析上述输出内容:
- 第一个 0:上面提到过类方法是获取
self
的元类对象与传入的参数进行比较,但是[Person class]
获取到的是类对象,因此返回NO
。 - 第二个 1:同上,此时我们传入
Person
元类对象,此时返回YES
。验证上述说法 - 第三个 1:我们发现此时传入
[NSObject class]
的是NSObject
类对象并不是元类对象,但是返回的值却是YES
。
原因是基元类的superclass
指针是指向基类对象NSObject
的。如下图13
号线
那么Person
元类通过superclass
指针一直找到基元类,还是不相等,此时再次通过superclass
指针来到基类,那么此时发现相等就会返回YES
了。
通过以上验证我看可以总结出如下的用法:
- 通过实例方法来调用,传入的参数应该是类对象
- 通过类方法来调用,传入的参数应该是元类对象
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.name
e的内容却是
。下面person
实例调用test
不做过多解释了,主要用来和上面方法调用做对比。
为什么会是这样的结果呢?首先通过一张图看一下两种调用方法的内存信息。
通过上图我们可以发现两种方法调用方式很相近。那么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
结构体、cls
、obj
。通过一张图展示一下这些局部变量存储结构。
上面我们知道当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
就相当于isa
,isa
下面的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];
}
此时的局部变量内存结构如下图所示
此时在访问_name
成员变量的时候,越过cls
内存往高地址找就会来到string
(string
也是指针类型,占用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
,此时局部变量内存结构如下图所示
当需要访问_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
对象,我们先来看一下其局部变量内存结构
首先通过obj
找到cls
,cls
找到类对象进行方法调用,此时在访问nickName
时,obj
查找成员变量,首先跳过8字节的cls
,之后跳过第一个成员变量_name
所占的8字节空间,最终再取8字节空间取出其中的值作为成员变量的值,那么此时也就是self
了。
为什么会跳过_name
的8个字节呢?
因为Person
类里面有2个属性name
、nickName
,现在访问的是nickName
,name
在低地址、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 总结
总结:这道面试题虽然很无厘头,让人感觉无从下手但是考察的内容非常多。
super
的底层本质为调用objc_msgSendSuper2
函数,传入objc_super2
结构体,结构体内部存储消息接受者和当前类,用来告知系统方法查找从父类开始。局部变量分配在栈空间,并且从高地址向低地址连续分配。先创建的局部变量分配在高地址,后续创建的局部变量连续分配在较低地址。
方法调用的消息机制,通过
isa
指针找到类对象进行消息发送。指针存储的是实例变量的首字节地址,上述例子中
person
指针存储的其实就是实例变量内部的isa
指针的地址。访问成员变量的本质,找到成员变量的地址,按照成员变量所占的字节数,取出地址中存储的成员变量的值。
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];
}
通过断点查看其汇编调用栈
上图中可以发现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];
}
上述代码的局部变量内存结构我们之前已经分析过了,真正的内存结构应该如下图所示
通过上面对面试题的分析,我们现在想要验证objc_msgSendSuper2
函数内传入的结构体参数,只需要拿到cls
的地址,然后向后移8个地址就可以获取到objc_super
结构体内的self
,在向后移8个地址就是current_class
的内存地址。通过打印current_class
的内容,就可以知道传入objc_msgSendSuper2
函数内部的是当前类对象还是父类对象了。
通过上图可以发现,最终打印的内容确实为当前类对象。
因此objc_msgSendSuper2
函数内部其实传入的是当前类对象,并且在函数内部获取其父类,告知系统从父类方法开始查找的。