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>,好了,所有的问题就都得到解释了。

接下来,我们通过汇编手段来验证一下上面推断,我们先将程序运行至下图所示的断点处


此时, viewDidLoad函数栈上所有的局部变量已经赋值完毕,汇编情况如下

从上面的分析可以看出,viewDidLoad函数栈空间大小为48个字节,存放了6个局部变量,每个局部变量8个字节,栈空间的地址范围是[rbp-0x30] ~ [rbp],因此想要查看当前栈空间里面内容,可以利用如下LLDB指令:
先读出当前栈底位置,也就寄存器rbp的值

(lldb) register read rbp
     rbp = 0x00007ffeeaddd130

rbp - 0x30 = 0x7FFEEADDD100 这样就得到了栈顶的的位置,然后打印出栈顶位置 之后的48字节内容(也就是当前的函数栈空间)


(lldb) x/6xg 0x7FFEEADDD100
0x7ffeeaddd100: 0x00007ffeeaddd108 0x0000000104e245c8
0x7ffeeaddd110: 0x00007f9d01508f50 0x0000000104e24500
0x7ffeeaddd120: 0x00007fff527257c0 0x00007f9d01508f50

也就是下图所示



我们可以挨个打印一下每一个局部变量

(lldb) po 0x00007ffeeaddd108
<CLPerson: 0x7ffeeaddd108>

(lldb) po 0x0000000104e245c8
CLPerson

(lldb) po 0x00007f9d01508f50   -->❗️❗️❗️ 实际上 [(__bridge id)obj print]; 的本质就等同于这一句❗️❗️❗️
<ViewController: 0x7f9d01508f50>

(lldb) po 0x0000000104e24500
ViewController

(lldb) po 0x00007fff527257c0
140734576613312

(lldb) po 0x00007f9d01508f50
<ViewController: 0x7f9d01508f50>

你或许会好奇为什么_cmd所指向的内容打出来的为什么是140734576613312(=0x00007fff527257c0,也就是它自己),根据_cmd的地址0x00007fff527257c0,说明它也是栈空间的地址,因为_cmd其实是viewDidLoad上层函数传过来的参数,因此这个栈空间应该是外层函数的局部变量,也就是说_cmd本质上说是一个指针。那我们看一下所指向的这段内存里面放了什么内容,因为不知道具体的大小,所以我们通过Xcode的内存查看器来看看


原来就是函数viewDidLoad所对应的函数名字符串而已,这样所以的疑问就扫清了。。。☕️☕️☕️

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

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


Runtime系列文章

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270