美文网首页
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