美文网首页
iOS Runtime经典面试题解析

iOS Runtime经典面试题解析

作者: MambaYong | 来源:发表于2023-05-07 16:14 被阅读0次

面试题

这道面试题如下,问最后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]的调用,同样是消息发送,首先找到objisa指针,从上面的分析可以看出来,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_classreceiverClass对象,由于此结构体要当参数传入方法,所以在开辟的栈空间内会存放receivercurrent_class这二个临时变量,在这里receiverselfcurrent_classViewController class。由于栈空间是从高地址到地址的,占空间的内部大致如下:

最后按照上面寻找成员变量的方式,跳过isa指针就是成员变量,由于上面已经分析指导isa指针就是cls,所以self就是找到的第一个成员变量,由于person只有一个成员变量_name,所以这里self就等于_name这个成员变量,最后的打印结果为:my name is <ViewController: 0x13a110720>

调试打印

首先我们打印出obj的地址值,然后打印后面的连续48字节的地址,分别打印地址的内容:

这很好的证明了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底层知识的理解。

相关文章

网友评论

      本文标题:iOS Runtime经典面试题解析

      本文链接:https://www.haomeiwen.com/subject/ywzhsdtx.html