今天研究一下super
关键字,在讲之前我们先看一下下面四条语句输出打印什么:备注说明:Student 继承 Person,两个类中都有 - (void)test 方法
@implementation Student
- (void)test{
[super test];
[self class];
// class 方法的本质
NSLog(@"[self class] : %@",[self class]); //取出 self 的类对象,所以应该输出 Student
NSLog(@"[self superclass] : %@",[self superclass]);//取出 self 的 superclass ,应该输出 Peson
NSLog(@"[super class] : %@",[super class]); //取出 self 的父类的类对象,应该输出 Person
NSLog(@"[super superclass] : %@",[super superclass]);//取出 self 的父类的 superclass ,应该输出 NSObject
}
运行一下代码,看看我们分析的对不对:
19-12-03 11:36:13.991206+0800 superTest[1494:254018] [self class] : Student
2019-12-03 11:36:13.991651+0800 superTest[1494:254018] [self superclass] : Person
2019-12-03 11:36:13.991691+0800 superTest[1494:254018] [super class] : Student
2019-12-03 11:36:13.991734+0800 superTest[1494:254018] [super superclass] : Person
[self class]
,[self superclass]
我们都分析对了,为什么[super class]
,[super superclass]
的结果和我们分析的不一样呢?
要搞清楚这个问题我们就需要搞懂super
关键字,class()
,superClass()
的底层,我们把Student.m
转为c++
代码看看底层是怎样的:
发现
super
底层被转换为objc_msgSendSuper(arg1,arg2)
函数,里面传入两个参数__rw_objc_super 结构体和 SEL
,所以上面的代码也可以把__rw_objc_super
抽离出去,换一种写法:
那么
__rw_objc_super
是什么呢,我们在runtime
源码中搜搜objc_super
:
objc_super
的底层就是消息的接受者和他的父类,结合这些底层知识我们把[super test]
底层的c++
代码修改一下:
super 方法的接受者仍然是子类,传入的父类是干嘛用的呢?
我们在
runtime
源码中搜索objc_msgSendSuper
:
原来 superclass 是为了告诉 runtime ,查找方法的时候直接从 superclass 的类中查找.❎不要在像以前那样通过 实例对象 isa 找到类对象,从类对象的方法列表中查找,如果找不到再通过类对象的 superclass 找到父类从父类的方法列表中查找.❎
-
class
方法的底层:
//class 底层实现
- (Class)class{
return object_getClass(self);//获取方法接受者的类对象或者元类对象
// object_getClass底层调用的 getIsa(),如果是实例对象获取的就是类对象,如果是类对象获取的就是元类对象.
}
-
superClass
方法的底层:
// superclass 底层实现
- (Class)superclass{
//先获取方法接受者的类对象
//在获取它的父类对象
return class_getSuperclass(object_getClass(self));
}
搞明白这几个关键字后,我们再回头看看[super class]
,[super superclass]
:
-
[super class]
:方法的接受者仍然是self
,class()
方法内部获取到self
的类对象,所以还是Student
. -
[super superclass]
:方法的接受者仍然是self
,superclass()
方法内部会现获取self
的类对象Student
,在获取Student
的父类Person
.
其实我们再调用class()
的时候,最终调用的都是NSObject
类中的class()
方法.
isMemberOfClass 和 isKindOfClass 区别:
我们看看下面四句输出语句,仔细想想会打印输出什么:
- (void)test2{
NSLog(@"%d", [NSObject isKindOfClass:[NSObject class]]);
NSLog(@"%d", [NSObject isMemberOfClass:[NSObject class]]);
NSLog(@"%d", [Student isKindOfClass:[Student class]]);
NSLog(@"%d", [Student isMemberOfClass:[Student class]]);
}
分析一下,感觉好像都是YES
呀,实际运行一下看看结果:
2019-12-03 15:31:24.838726+0800 superTest[1794:338777] 1
2019-12-03 15:31:24.839133+0800 superTest[1794:338777] 0
2019-12-03 15:31:24.839170+0800 superTest[1794:338777] 0
2019-12-03 15:31:24.839224+0800 superTest[1794:338777] 0
怎么样,跟你们分析的答案一样吗?我们还是看看这两个方法的本质:
+ (BOOL)isMemberOfClass:(Class)cls {
// 获取接收者的元类对象 , 直接和 传进来的 cls 比较,也就是说传进来的 cls 也应该是 元类对象,否则就为 false
return object_getClass((id)self) == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
// 获取消息接收者的类对象 和 传入的 cls 判断是否相等
return [self class] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
// 第一步: 获取传入的消息接收者的元类对象
// 第二步: 判断和传入的 cls 是否相等,也就是说传入的也必须是 元类对象, 否则为 false
// 第三步: 如果不相等,继续找这个 元类对象的 superclass 继续比较,直到 superclass 为 nil.
for (Class tcls = object_getClass((id)self); tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
// 第一步: 取出消息接收者的类对象,和传入的 cls 判断是否相等
// 第二步: 如果不相等,继续找这个类对象的 superclass 继续比较,直到 superclass 为nil 为止.
for (Class tcls = [self class]; tcls; tcls = tcls->superclass) {
if (tcls == cls) return YES;
}
return NO;
}
根据源码分析,+
开头的方法,传入的必须是元类对象,所以我们后面三条输出语句都是false
.如果想要输出true
,这样改动即可:
- (void)test2{
NSLog(@"%d", [NSObject isKindOfClass:[NSObject class]]); // 1
NSLog(@"%d", [NSObject isMemberOfClass:object_getClass([NSObject class])]); //1
NSLog(@"%d", [Student isKindOfClass:object_getClass([Student class])]); //1
NSLog(@"%d", [Student isMemberOfClass:object_getClass([Student class])]); //1
}
// 输出结果
2019-12-03 16:05:07.352199+0800 superTest[1820:351570] 1
2019-12-03 16:05:07.352531+0800 superTest[1820:351570] 1
2019-12-03 16:05:07.352564+0800 superTest[1820:351570] 1
2019-12-03 16:05:07.352590+0800 superTest[1820:351570] 1
那为什么第一条语句[NSObject isKindOfClass:[NSObject class]]
同样也是输出true
呢?右边应该是一个元类对象呀.我们在OC对象的底层结构及isa、superClass详解已经详细讲过isa
和superclass
.当时还重点把基类元类对象的 superclass 指向类对象这条线用⭕️标记了出来,因为它太特殊.
所以
[NSObject isKindOfClass:[NSObject class]]
这条语句会从元类对象一直找到NSObject
类对象.事实上,所有继承自NSObject
类的子类调用[** isKindOfClass:[NSObject class]]
都是成立的.
进阶训练:
创建一个Person
类,类中有一个有一个test
方法:
@interface Person : NSObject
@property(nonatomic,copy)NSString *name;
- (void)test;
@end
@implementation Person
- (void)test{
NSLog(@"my name is %@",self.name);
}
@end
现在我们像下面这样调用:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = @"123";
id personClass = [Person class];
void * pointer = &personClass;
[(__bridge Person *)pointer test];
}
@end
大家分析一下pointer test
能不能调用成功,为什么?如果调用成功会打印什么?为什么?
我们运行一下代码,看看能不能成功:
2019-12-04 09:56:44.802983+0800 指针调用方法[942:50150] my name is 123
调用成功了,但是为什么打印的是123?这个123是不是局部变量 str = @"123"
?我们修改str = @"666
再运行一下:
2019-12-04 09:59:01.033985+0800 指针调用方法[953:51910] my name is 666
发现打印的就是局部变量str
的值.很奇怪,怎么会这样呢?下面我们就好好分析一下:
分析:
如果我们要调用test
方法,正常的做法应该是这样:
//调用test方法正常步骤
Person *person = [[Person alloc]init];
[person test];
我们在OC对象的底层结构及isa、superClass详解这一篇中知道了方法调用的本质就是通过isa
指针找到对应的类,然后查找方法.所以上面的代码本质上就是这样:
我们再画图分析一下
[(__bridge Person *)pointer test]
:
他们的内存结构何其相似.
personClass
在这里不就等价于isa
么?他们都指向Person
类对象.[person test]
是通过实例对象 person
的isa
指针找到Person 类对象
,然后从类对象的方法列表中查找方法.所以,方法的调用本质就是只要能找到类对象.而[(__bridge Person *)pointer test]
,指针变量pointer
中存储的personClass
恰巧就指向类对象,所以最后能调用成功.
那为什么打印
my name is 666
是局部变量的值呢?这是因为栈内存分配空间的机制导致的,我们写一个方法:
- (void)stackMemoryTest{
int a = 1;
int b = 2;
int c = 3;
int d = 4;
NSLog(@"a: %p",&a);
NSLog(@"b: %p",&b);
NSLog(@"c: %p",&c);
NSLog(@"d: %p",&d);
}
// 打印结果
2019-12-04 10:34:37.514299+0800 指针调用方法[1125:79517] a: 0x7ffeefbfea3c
2019-12-04 10:34:37.514362+0800 指针调用方法[1125:79517] b: 0x7ffeefbfea38
2019-12-04 10:34:37.514390+0800 指针调用方法[1125:79517] c: 0x7ffeefbfea34
2019-12-04 10:34:37.514415+0800 指针调用方法[1125:79517] d: 0x7ffeefbfea30
从打印结果中可以看到,栈内存分配空间是从高地址往低地址分配的,先创建的局部变量分配在高地址,后创建的分配在低地址.所以一下这段代码在内存中的布局如图:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = @"123";
id personClass = [Person class];
void * pointer = &personClass;
[(__bridge Person *)pointer test];
}
@end
那最后如何会输出
my name is 666
呢?我们参考一下[ person test ]
:
我们在获取
self.name的时候,如果转换成汇编代码会发现本质上就是找到
isa指针,然后越过8个字节,从而找到
_name.这就是内存访问的本质:找到某块内存的地址,读取地址中的值.
回到
[(__bridge Person *)pointer test]
,pointer
也会同样的越过personClass
这8个字节找到str
.所以最后就打印的是my name is 666
;
如果把
NSString *str = @"666";
这段代码注释掉会打印什么?
2019-12-04 11:38:31.350106+0800 指针调用方法[1201:119557] my name is
会发现打印的是ViewController
,这又是为什么呢?
这就是super
关键字引起的.因为[super viewDidLoad];
这句代码:
super
底层被转换为objc_msgSendSuper(arg1,arg2)
这个函数,而这个函数需要两个参数.第一个参数就是__rw_objc_super
结构体,这个结构体中有两个成员:self (ViewController)
和它的父类 UIViewController
';第二个参数就是方法名了.
所以,这里就相当于存在一个结构体类型的局部变量,我们画图说明:
图中已经用红线标记出来了,在取
self->name
的时候,越过personClass
的8个指针,整好找到了self
也就是ViewController
.从图中可以看到,只要比personClass
先声明的局部变量,并且是先后声明的关系就会打印出来.
比如,我们在添加一个
str2
:
关于 super 关键字的一点补充
在前面讲super
关键字的时候,我们看到super
转换为c++
代码的时候,被转换成了objc_msgSendSuper(arg1,arg2)
函数.其实实际上底层执行并不是objc_msgSendSuper(arg1,arg2)
函数,而是objc_msgSendSuper2(arg1,arg2)
函数.我们在[super viewDidLoad];
处打个断点,然后显示汇编语言看一下:
并且上面讲的
objc_msgSendSuper(arg1,arg2)
中的第一个参数arg1
是__rw_objc_super
结构体,这个结构体如下:
struct __rw_objc_super {
struct objc_object *object;
struct objc_object *superClass;
}
而objc_super2
的结构体如下:
struct objc_super2 {
id receiver;
Class current_class;
};
可以看到objc_super2
这个结构体中传入的是Class current_class;
也就是当前类.而在_objc_msgSendSuper2
内部获取当前类的superClass
:
我们可以通过访问内存验证一下:
如上图所示,我们取出结构体中的第二个成员看看是什么就清楚了.
所以,
super
底层其实是调用objc_msgSendSuper2()
函数,然后传入的是当前类对象,只不过在内部又会取出当前类对象的superclass
.
这只是一个小细节,和我们最开头说的也不矛盾,知悉就好.