面试题引发的思考:
Q: 以下代码的打印内容是什么?
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
@end
@implementation Person
@end
// TODO: ----------------- Student类 -----------------
@interface Student : Person
@end
@implementation Student
- (instancetype)init
{
self = [super init];
if (self) {
NSLog(@"[self class] = %@", [self class]);
NSLog(@"[self superclass] = %@", [self superclass]);
NSLog(@"---------------------------");
NSLog(@"[super class] = %@", [super class]);
NSLog(@"[super superclass] = %@", [super superclass]);
}
return self;
}
@end
// TODO: ----------------- main -----------------
int main(int argc, const char * argv[]) {
@autoreleasepool {
Student *stu = [[Student alloc] init];
}
return 0;
}
打印结果应该是:
Demo[1234:567890] [self class] = Student
Demo[1234:567890] [self superclass] = Person
Demo[1234:567890] ---------------------------
Demo[1234:567890] [super class] = Person
Demo[1234:567890] [super superclass] = NSObject
真实打印结果是:
Demo[1234:567890] [self class] = Student
Demo[1234:567890] [self superclass] = Person
Demo[1234:567890] ---------------------------
Demo[1234:567890] [super class] = Student
Demo[1234:567890] [super superclass] = Person
Q:为什么打印结果和想象中有出入?
super
调用方法:消息接收者仍然是 子类对象self
,从父类开始查找方法的实现。class
方法的内部实现是根据 消息接收者 返回其对应的 类对象。而class
方法是在基类NSObject
类中实现的。superclass
方法的内部实现是根据 消息接收者 返回其对应的 父类的类对象。
1. self / super
与class / superclass
原理
(1) self / super
原理
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
- (void)run;
@end
@implementation Person
- (void)run {
NSLog(@"%s", __func__);
}
@end
// TODO: ----------------- Student类 -----------------
@interface Student : Person
@end
@implementation Student
- (void)run {
[super run];
NSLog(@"Student - run");
}
@end
通过xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc Student.m
将Student.m
转化为C++代码查看其底层实现:
即[super run];
语句转化为C++代码:
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){
(id)self,
(id)class_getSuperclass(objc_getClass("Student"))}, sel_registerName("run"));
// 简化代码
objc_msgSendSuper((__rw_objc_super){
self,
class_getSuperclass(objc_getClass("Student"))
}, @selector(run));
// 继续简化代码
struct __rw_objc_super arg = {
self,
class_getSuperclass(objc_getClass("Student"))
};
objc_msgSendSuper(arg, @selector(run));
由以上代码可知:
super
底层实现是objc_msgSendSuper
函数;objc_msgSendSuper
函数传入两个参数:__rw_objc_super
结构体与@selector(run)
方法名;__rw_objc_super
结构体传入两个参数:self
与class_getSuperclass(objc_getClass("Student"))
即Student
的父类Person
。
OC源码中搜索objc_msgSendSuper
:
由OC源码可知:
objc_msgSendSuper
函数传入的第一个参数为objc_super
结构体;objc_super
结构体传入两个参数:
receiver
消息接收者为self
;
superclass
决定从哪一个类开始查找方法的实现。
通过以上分析,可以得出self
、super
调用方法流程:
由上图可知:
super
调用方法:消息接收者仍然是子类对象self
,从父类开始查找方法的实现。
(2) class / superclass
原理
class
与superclass
的底层实现如下图所示:
class
方法的内部实现是根据消息接收者返回其对应的类对象。而class
方法是在基类NSObject
类中实现的。
则[self class];
和[super class];
的消息接收者都是本类Student
。
区别为:[self class];
从本类类对象开始查找方法,[super class];
从父类类对象开始查找方法。
superclass
方法的内部实现是根据消息接收者返回其对应的父类的类对象。
则[self superclass];
和[super superclass];
的消息接收者都是父类Person
。
区别为:[self superclass];
从父类类对象开始查找方法,[super superclass];
从父类的父类类对象开始查找方法。
3) objc_msgSendSuper2
函数
以上代码转化为C++后并不能说明super
底层调用函数就一定objc_msgSendSuper
函数。
通过断点查看汇编:
由以上可知:
super
底层调用的是objc_msgSendSuper2
函数。
通过OC源码查找objc_msgSendSuper2
函数底层实现:
由OC源码可知:
super
底层调用class->superclass
获取父类,并非通过C++代码分析的直接传入父类对象。
其实_objc_msgSendSuper2
内传入的结构体为objc_super2
总结可知:
_objc_msgSendSuper2
函数内其实传入的是当前类对象,然后在函数内部获取当前类对象的父类,并且从父类开始查找方法。
2. isMemberOfClass / isKindOfClass
原理
Q: 以下代码的打印内容是什么?
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
@end
@implementation Person
@end
// TODO: ----------------- main -----------------
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"%d", [[NSObject class] isMemberOfClass:[NSObject class]]);
NSLog(@"%d", [[NSObject class] isKindOfClass:[NSObject class]]);
}
return 0;
}
由iOS底层原理 - OC对象的本质(二)可知isa
、superclass
指向图:
根据isa
、superclass
指向图,得到以下结论:
由以上源码可以得出以下结论:
- 结论一:
isMemberOfClass
判断左边类型 是否等于 右边类型;- 结论二:
isKindOfClass
判断左边类型 是否等于 右边类型或其子类;- 结论三:左边(实例对象) 对应 右边(类对象);
- 结论四:左边(类对象) 对应 右边(元类对象);
- 结论五:基类的元类的
superclass
指向基类。
通过结论可以正确分析以下代码:
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
@end
@implementation Person
@end
// TODO: ----------------- main -----------------
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *person = [[Person alloc] init];
NSLog(@"%d", [person isMemberOfClass:[Person class]]); // 1,结论一、三
NSLog(@"%d", [person isMemberOfClass:[NSObject class]]); // 0,结论一、三
NSLog(@"%d", [person isKindOfClass:[Person class]]); // 1,结论二、三
NSLog(@"%d", [person isKindOfClass:[NSObject class]]); // 1,结论二、三
NSLog(@"%d", [[Person class] isMemberOfClass:[NSObject class]]); // 0,结论一、四
NSLog(@"%d", [[NSObject class] isMemberOfClass:[NSObject class]]); // 0,结论一、四
NSLog(@"%d", [[Person class] isKindOfClass:[NSObject class]]); // 1,结论二、四、五
NSLog(@"%d", [[NSObject class] isKindOfClass:[NSObject class]]); // 1,结论二、四、五
}
return 0;
}
3. 一个综合性面试题
对以下代码进行分析:
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
- (void)print;
@end
@implementation Person
- (void)print {
NSLog(@"my name is %@", self.name);
}
@end
// TODO: ----------------- ViewController类 -----------------
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj print];
}
// 打印结果
Demo[1234:567890] my name is <ViewController: 0x7fd3b941a2a0>
(1) Q:为什么可以正常调用方法?
在工作中不会使用到类似的代码,主要考察对iOS底层原理的理解程度。
通过总结的底层调用流程进行分析:
由底层调用流程可知:
- 语句一:创建一个
id
类型的cls
指针指向Person
类对象;- 语句二:创建一个指针变量
obj
,用来存放cls
的地址;- 语句三:创建一个
Person
类型的指针变量person
,通过isa
指针指向Person
类对象;person
实例对象内部实际是取得最前面的8个字节(即isa
),并通过计算得出类对象地址。
所以obj
在调用print
方法时,也会通过其内存地址找到cls
,而cls
中取出最前面8个字节空间其内部存储的刚好是Person
类对象地址。因此obj
是可以调用对象方法的。
(2) 为什么会打印出ViewController
?
1> 栈空间的内存是从高地址到低地址连续分配的。
通过以下代码验证:
int main(int argc, const char * argv[]) {
@autoreleasepool {
// 局部变量分配在栈空间
double a = 10;
double b = 20;
double c = 30;
// 打印结果 - 0x7ffeefbff588, 0x7ffeefbff580, 0x7ffeefbff578
NSLog(@"%p, %p, %p", &a, &b, &c);
}
return 0;
}
由以上代码可知:
栈空间的内存是从高地址到低地址连续分配的。
2> objc_super2
函数
在[super viewDidLoad];
这段代码中,由以上分析可知super
底层调用的是objc_msgSendSuper2
函数,进而调用objc_super2
结构体:
在上面的代码中objc_super2
结构体为:
struct objc_super2 = {
self,
[ViewController Class]
};
那么objc_msgSendSuper2
函数调用之前,会先创建局部变量objc_super2
结构体用于为objc_msgSendSuper2
函数传递的参数。
3> 实例一
对以下代码进行分析:
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj print];
}
// 打印结果
Demo[1234:567890] my name is my name is <ViewController: 0x7ff739c06e30>
可以绘制出局部变量存储结构:
根据代码可总结出以上结构图:
-
person
实例对象内部实际是取出前8个字节(即isa
),并通过计算得出类对象地址。 - 而
obj
在调用print
方法时找到cls
取出前8个字节,也就是Person
类对象的内存地址; - 当访问实例变量
_name
时,会继续向高地址内存查找,此时访问到objc_super
结构体,从中取出前8个字节(即self
),所以self.name
此时为ViewController
对象。
因此上述代码中cls
就相当于isa
,isa
下面的8个字节就相当于_name
成员变量。所以成员变量_name
访问到的值就是cls
地址向高地址位取8个字节地址空间存储的值。
4> 实例二
对以下代码进行分析:
- (void)viewDidLoad {
[super viewDidLoad];
NSString *str = @"abc";
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj test];
}
// 打印结果
Demo[1234:567890] my name is abc
可以绘制出局部变量存储结构:
此时访问_name
成员变量的时候,会向cls
后面的高地址查找8个字节空间,即获取的成员变量为str
。
5> 实例三
对以下代码进行分析:
- (void)viewDidLoad {
[super viewDidLoad];
int *a = 10;
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj test];
}
// 运行结果 - crash
可以绘制出局部变量存储结构:
此时访问_name
成员变量的时候,会向cls
后面的高地址查找8个字节空间,而int
占4个字节空间,那么这8位的字节空间同时存在int
类型数据以及objc_super
结构体,所以会造成坏地址访问崩溃。
6> 实例四
对以下代码进行分析:
// TODO: ----------------- Person类 -----------------
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString * nickname;
- (void)print;
@end
@implementation Person
- (void)print {
NSLog(@"my nickname is %@", self. nickname);
}
@end
// TODO: ----------------- ViewController类 -----------------
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Person class];
void *obj = &cls;
[(__bridge id)obj print];
}
// 打印结果 - my nickname is <ViewController: 0x7fd3b941a2a0>
可以绘制出局部变量存储结构:
此时访问_nickname
成员变量的时候,会向cls
后面的高地址查找8个字节空间(_name
所占),再向后面的高地址查找8个字节空间(_nickname
所占)即为self
,所以self.nickname
此时为ViewController
对象。
这道面试题考察了以下内容:
super
的底层本质为调用objc_msgSendSuper2
函数,传入objc_super2
结构体,结构体内部存储消息接收者与当前类,告知系统从父类开始查找方法;- 局部变量分布在栈空间,栈空间的内存是从高地址到低地址连续分配的,即局部变量按照创建顺序从高地址到低地址连续分配;
- 消息机制,通过
isa
指针找到类对象进行消息发送;- 访问成员变量的本质,找到成员变量的地址,按照成员变量所占的字节数,取出地址中存储的成员变量的值。