当前位置:   article > 正文

Runtime笔记(八)—— 记一道变态的runtime面试题

Runtime笔记(八)—— 记一道变态的runtime面试题
Runtime系列文章

Runtime笔记(一)—— isa的深入体会(苹果对isa的优化)
Runtime笔记(二)—— Class结构的深入分析
Runtime笔记(三)—— OC Class的方法缓存cache_t
Runtime笔记(四)—— 刨根问底消息机制
Runtime笔记(五)—— super的本质
[Runtime笔记(六)—— Runtime的应用…待续]-()
[Runtime笔记(七)—— Runtime的API…待续]-()
Runtime笔记(八)—— 记一道变态的runtime面试题

先上面试题
//***********♦️♦️CLPerson.h♦️♦️************

#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface CLPerson : NSObject
@property (nonatomic, copy) NSString *name;
-(void)print;
@end

NS_ASSUME_NONNULL_END


//***********♥️♥️CLPerson.m♥️♥️************ 

#import "CLPerson.h"

@implementation CLPerson

-(void)print {
    NSLog(@"My name's %@", self.name);
}

@end

//***********??ViewController.m??************ 

#import "ViewController.h"
#import "CLPerson.h"

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [CLPerson class];
    void *obj = &cls;
    [(__bridge id)obj print]}

@end

问题1 [(__bridge id)obj print];中的print方法可以被正常调用吗?
问题2 print方法最终的打印结果是什么?

运行结果

    2019-08-13 17:10:58.075381+0800 iOS-Runtime[29076:3163099] My name's <ViewController: 0x7fce43e08aa0>

从运行结果,print方法可以被成功调用,打印结果是My name's <ViewController: 0x7fce43e08aa0>,从代码到运行结果,似乎莫名其妙。如果我在毫无防备的情况下碰到这样的面试题,我会选择选择直接起身,优雅离去,同时心里默念WHAT THE FUCK!!!

现在,我们就静下心来,好好来搞一搞。

[(__bridge id)obj print];中的print方法为什么可以被正常调用?

我们先回顾一下正常人是怎么调用方法的

CLPerson *person = [[CLPerson alloc] init];
[person print];

相信对于上面的代码没有人会有疑问,我们通过一张图来说明一下,这两行代码运行时,内存里面的情况
再看看我们面试题里面的代码

- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [CLPerson class];
    void *obj = &cls;
    [(__bridge id)obj print]}

可以看出,cls指向CLPersonClass对象,而obj指向cls,如下图示

请看图中的文字说明,因为从本质上说,
指针person–>指针isa–>[CLPerson class]
指针obj–>指针cls–>[CLPerson class]
因此[person print]效果 == [(__bridge id)obj print]效果,这里需要仔细体会一下。

回想一下消息发送的本质[person print]是从person所指向的结构体(实例对象)取出第一个成员变量isa,然后根据isa找到对应Class对象的内存空间,最后在Class对象的方法列表里面进行方法查找,最后调用方法。

那么[(__bridge id)obj print],同样会遵从上面的流程,因为obj所指向的是一个cls指针变量地址,恰巧,这个cls指针指向的就是CLPersonClass对象的内存空间,所以同样可以进入到它的方法列表进行查找,最后找到print方法进行调用,到此问题①解释完毕。



②打印结果为什么是<ViewController: 0x7fce43e08aa0>

这个问题有点小复杂,不过没关系,我们一步一步来

print方法找到后的调用过程
我们知道任何OC方法的底层都是一个C函数,并且函数头两个参数是默认参数id selfSEL _cmd,那么self是谁呢?以上面代码为例

CLPerson *person = [[CLPerson alloc] init];
[person print];

**********
-(void)print {
    NSLog(@"My name's %@", self.name);
}

print方法对应的C函数里面,self就是person,而print的内容是打印self.name,也就是必然要通过self,找到成员变量_name,如何找呢,这就需要我们来了解一下实例对象的内存布局,根据我们上面有关CLPerson类的定义,实例变量person的内存布局如下图self.name相当于self->_name,因为_nameisa后面紧接着的成员变量,而_name是一个指针,占8个字节大小,因此self->_name实际上得到的就是从self所指向的内存地址往高地址偏移8个字节(isa的大小)后的内存地址,指向一段8字节大小的内存空间,从而获得person对象的成员变量_name

如果你还不太了解OC对象内存布局相关知识的,可以参考
OC对象的本质(上) —— OC对象的底层实现原理
OC对象的本质(下)—— 详解isa&superclass指针

我在其中进行了详细阐述。 如果对于上面的内容没有疑问,那么下面接着看面试题中设置的场景,在分析print方法为何能被调用的过程中,我们可以看到实际上

  • obj指针相当于person指针(也就是print方法里面的self
  • cls指针相当于person指针所指向的实例对象里面的isa指针
    所以对于面试题的场景,实际上是这样的两张图本质是一样的,只不过在面试题的场景里,print方法被调用的时候,其内部的self = obj,因此self.name作用就是从obj所指向的内存空间,往高地址偏移8个字节,而obj指向了cls的内存地址,cls也是是一个指针,所以占8个字节,因此self.name取到的实际上恰好是指针变量cls之后接下来的一段8字节内存空间,所以最终print打印出的就是这段内存里面存储的内容。而结果我们已经看到了,打印的是<ViewController: 0x7fce43e08aa0>,接下来我们就要分析一下为啥cls下面存着的是ViewController对象。

因为objcls都是viewDidLoad方法(函数)里面的局部变量,我们知道函数的局部变量都是放在栈空间里面的。那么你了解函数的栈空间吗?我们来简单科普一下。

函数的栈空间简介

栈空间的作用,是用来存放被调用函数其内部所定义的局部变量的。对于arm64架构来说,这么理解就够了,如果你恰好了解过8086汇编,那么可能知道,栈空间里面还会存放函数的参数,但是对于arm64来说,函数的参数通常会放到寄存器里面,所以我们就先简单的认为,函数的栈空间里面放的就是函数的局部变量。而且局部变量的存放顺序,是根据定义的先后顺序,从函数栈底开始,一个一个排列,最先定义的局部变量位于栈底(高地址),通过下图来描绘一下

那么我们就来看一下viewDidLoad里面总共有哪些局部变量,再贴一下代码

- (void)viewDidLoad {
    [super viewDidLoad];
    id cls = [CLPerson class];
    void *obj = &cls;
    [(__bridge id)obj print]}

我们看到,viewDidLoad内部只有两个局部变量,分别是id clsvoid *obj,其余的都是方法调用。那么栈里面的情况应该就是可以看出如果按图中的分析,print方法将会最终打印栈底之外8个字节里面的内容,但是我们知道一个函数内部是不能访问其他函数的栈空间的,上图中的这8个字节明显超出了当前函数的栈空间,所以无法解释我们上面看到的打印结果。

其实,这个面试题里面设计了一个很隐藏的猫腻。问题的出口其实是在[super viewDidLoad];这句代码上,关于super问题,可以参考我在Runtime笔记(五)—— super的本质一文中的解析。这里就直接基于文章中的知识来解决我们当前的问题了。

[super viewDidLoad];展开成底层函数就是

objc_msgSendSuper((__rw_objc_super){
            (id)self,   
            (id)class_getSuperclass(objc_getClass("ViewController"))
           },   
            @selector(viewDidLoad));

注意这个函数的第一个参数是一个结构体__rw_objc_super,那么这个结构体参数实际上是在当前viewDidLoad函数的作用域里面被定义赋值,然后再传入objc_msgSendSuper作为参数的。说白了viewDidLoad还含有一个隐藏局部变量,其内部实际上等同于这么写

//    [super viewDidLoad];
    struct __rw_objc_super arg = {
        (id)self,
        (id)class_getSuperclass(objc_getClass("ViewController"))
    };
    
    objc_msgSendSuper(arg, @selector(viewDidLoad));
    
    id cls = [CLPerson class];
    void *obj = &cls;
    [(__bridge id)obj print];

所以,viewDidLoad内部第一个局部变量实际上是一个结构体类型struct __rw_objc_super的变量,该结构体内部有两个id类型(也就是指针变量)的成员变量,并且注意,第一个成员变量是 self,而这个self正式当前方法的消息接受者,也就是ViewController实例对象。**需要说明的是,这个self跟我们上面讨论print方法里面用到的那个self是不同的两个对象哦,请用心体会。**好了,说多了太绕,直接上图综上所述,print里面通过self.name所拿到的变量,就是图中cls下面的那8个字节,也就是当前方法的消息接受者selfViewController实例对象),因此打印的结果是<ViewController: 0x7fce43e08aa0>,好了,所有的问题就都得到解释了。

这道面试题确实有点扯,项目中也绝不会这么写代码,但从面试的角度,这里面涉及了对于函数栈空间的理解对于super本质的理解对于消息机制的理解对于OC对象本质的理解,在高考里面,属于最后一道大题的难度级别,本文之前,你可能祈祷千万别碰到这种变态的面试题,但是本文过后,如果你能完全掌握里面的精髓,我相信大家肯定会祈祷面试碰到这道题,因为光是把里面涉及到的四个对于...的理解都展开讲一遍,那一般的面试官估计就要被您给反虐了:)

好了,关于面试的话题,到此结束,希望对大家有帮助,文中如有解释的不透彻或者不正确的地方,欢迎交流指正,程序员的世界没有容易二字,加油,与诸君共勉???。



Runtime系列文章

Runtime笔记(一)—— isa的深入体会(苹果对isa的优化)
Runtime笔记(二)—— Class结构的深入分析
Runtime笔记(三)—— OC Class的方法缓存cache_t
Runtime笔记(四)—— 刨根问底消息机制
Runtime笔记(五)—— super的本质
[Runtime笔记(六)—— Runtime的应用…待续]-()
[Runtime笔记(七)—— Runtime的API…待续]-()
Runtime笔记(八)—— 记一道变态的runtime面试题

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号