面试题
这道面试题如下,问最后print
方法能不能调用成功?如果能最后打印什么?
@interface Person: NSObject
@property (copy,nonatomic) NSString * name;
-(void)print;
@end
@implementation Person
-(void)print{
NSLog(@"my name is %@",self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Person class];
void * obj = &cls;
[(__bridge id)obj print];
}
@end
这是一道非常好的面试题,主要考察了iOS
底层的函数调用机制以及函数调用栈的问题。
解答
pint可以调用成功
首先我们先看print
函数是否可以调用成功的问题,[Person class]
返回的是Person
的类对象,在底层类对象是一个结构体,结构体里首个变量是isa
指针,接下来是superclass
指针,cache
的方法缓存指针以及具体的类信息指针,都指向的是具体结构体,类对象在底层的结构体结构大概如下:
struct objc_class {
Class isa;
Class superclass;
cache_t cache;
class_data_bits bits;
}
struct class_rw_t {
units32_t flags;
units32_t version;
const class_ro_t *ro;
method_list_t *methods;// 方法列表
property_list_t *properties;//属性列表
const protocol_list_t *protocols;//协议列表
Class firstSubclass;
Class nextSiblingClass;
char *demangledName;
}
struct class_ro_t {
unit32_t flags;
unit32_t instanceStart;
unit32_t instanceSize;
#ifdef __LP64__
unit32_t reserved;
#endif
const unit8_t *ivarLayout;
const char *name;//类名
method_list_t *baseMethodlist;
protocol_list_t *baseProtocols;
const ivar_list_t *ivars;//成员变量列表
const unit8_t *weakIvarLayout;
property_list_t *baseProperties;
}
从上面的代码可以看出来obj
存放的是cls
的地址,同时cls
的地址指向的是Person
类对象。根据Runtime
的底层原理,iOS
的函数调用在底层是通过objc_msgSend
函数来给函数调用者发送消息,objc_msgSend
的执行流程有三大阶段:
-
消息发送
-
动态方法解析
-
消息转发
这里我们重点看消息发送阶段,下面这张经典的图基本阐述了消息发送阶段:
首先实例通过isa
指针找到类对象,查看类对象的方法列表里是否存在对应的方法,如果没有则通过类对象里面的superclass
找到其父类的类对象,在父类对象的类对象里继续查找,直至到顶层的NSObject
。
那么在回头看看[(__bridge id)obj print]
的调用,同样是消息发送,首先找到obj
的isa
指针,从上面的分析可以看出来,obj
的前8
个字节存放的应该就是isa
指针,因为不管是实例对象还是类对象其底层的struct
结构体中,前8
个字节就是isa
指针,由于obj
存放的是cls
的地址,所以这里的isa
其实就是cls
的地址,而这个isa
指向的是Person
的类对象,所以最后能在Person
类对象的方法列表中找到print
方法。
最后的打印
既然能调用方法,那么最后打印什么呢?其实最后需要确定的是self->_name
这个成员变量的值是什么。在底层实例对象的成员变量是紧挨着isa
指针的,在isa
指针的下面的一段连续的存储空间中,所以我们需要弄清楚上面的isa
指针紧挨着的8
个字节的存储空间中到底是什么?
栈空间
viewDidLoad
调用时会开辟一段栈空间在作为函数调用的临时空间,函数调用完毕后就回收此空间,当然函数调用时里面的局部变量也是存在这个栈空间里的,里面的[super viewDidLoad]
会继续开辟一段栈空间,二段栈空间是连续的,栈空间的回收是先开辟的后回收,这也符合栈数据结构的特点,[super viewDidLoad]
方法在底层是通过objc_msgSendSuper2
来调用的,其需要接受二个参数:
- struct objc_super2
- SEL
其中objc_super2
结构体如下:
struct objc_super2 {
id receiver;
Class current_class;
}
receiver
是消息接受者,current_class
是receiver
的Class
对象,由于此结构体要当参数传入方法,所以在开辟的栈空间内会存放receiver
和current_class
这二个临时变量,在这里receiver
为self
,current_class
为ViewController class
。由于栈空间是从高地址到地址的,占空间的内部大致如下:
最后按照上面寻找成员变量的方式,跳过isa
指针就是成员变量,由于上面已经分析指导isa
指针就是cls
,所以self
就是找到的第一个成员变量,由于person
只有一个成员变量_name
,所以这里self
就等于_name
这个成员变量,最后的打印结果为:my name is <ViewController: 0x13a110720>。
调试打印
首先我们打印出obj
的地址值,然后打印后面的连续4
个8
字节的地址,分别打印地址的内容:
这很好的证明了cls
后面的8
个字节存储的是viewController
,在往后8
个字节存放的是viewController
的类对象。
思考
如果代码为下面这种情况打印什么?
- (void)viewDidLoad {
[super viewDidLoad];
NSString * str = @"mamba";
id cls = [Person class];
void * obj = &cls;
[(__bridge id)obj print];
}
通过上面的分析,str
这个字符串局部变量会紧挨着cls
的地址,所以最后输出是my name is mamba,如果注释掉
[super viewDidLoad]
的调用,则会发生坏内存访问,程序崩溃。
总结
本文根据一个实际的面试题来回复了Runtime
中的函数调用消息机制以及函数调用栈的相关知识,通过这个面试题能到加深对iOS
底层知识的理解。
网友评论