赞
踩
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];
中的
问题2
运行结果
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
指向CLPerson
的Class
对象,而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
指针指向的就是CLPerson
的Class对象
的内存空间,所以同样可以进入到它的方法列表进行查找,最后找到print
方法进行调用,到此问题①解释完毕。
<ViewController: 0x7fce43e08aa0>
这个问题有点小复杂,不过没关系,我们一步一步来
print方法找到后的调用过程
我们知道任何OC方法的底层都是一个C函数,并且函数头两个参数是默认参数id self
和 SEL _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
,因为_name
是isa
后面紧接着的成员变量,而_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
对象。因为obj
,cls
都是viewDidLoad
方法(函数)里面的局部变量,我们知道函数的局部变量都是放在栈空间里面的。那么你了解函数的栈空间吗?我们来简单科普一下。
栈空间的作用,是用来存放被调用函数其内部所定义的局部变量的。对于arm64
架构来说,这么理解就够了,如果你恰好了解过8086
汇编,那么可能知道,栈空间里面还会存放函数的参数,但是对于arm64
来说,函数的参数通常会放到寄存器里面,所以我们就先简单的认为,函数的栈空间里面放的就是函数的局部变量。而且局部变量的存放顺序,是根据定义的先后顺序,从函数栈底开始,一个一个排列,最先定义的局部变量位于栈底(高地址),通过下图来描绘一下
那么我们就来看一下viewDidLoad
里面总共有哪些局部变量,再贴一下代码
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [CLPerson class];
void *obj = &cls;
[(__bridge id)obj print];
}
我们看到,viewDidLoad
内部只有两个局部变量,分别是id cls
和void *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个字节,也就是当前方法的消息接受者self
(ViewController实例对象
),因此打印的结果是<ViewController: 0x7fce43e08aa0>
,好了,所有的问题就都得到解释了。
这道面试题确实有点扯,项目中也绝不会这么写代码,但从面试的角度,这里面涉及了对于函数栈空间的理解
,对于super本质的理解
,对于消息机制的理解
,对于OC对象本质的理解
,在高考里面,属于最后一道大题的难度级别,本文之前,你可能祈祷千万别碰到这种变态的面试题,但是本文过后,如果你能完全掌握里面的精髓,我相信大家肯定会祈祷面试碰到这道题,因为光是把里面涉及到的四个对于...的理解
都展开讲一遍,那一般的面试官估计就要被您给反虐了:)
好了,关于面试的话题,到此结束,希望对大家有帮助,文中如有解释的不透彻或者不正确的地方,欢迎交流指正,程序员的世界没有容易二字,加油,与诸君共勉???。
Runtime笔记(一)—— isa的深入体会(苹果对isa的优化)
Runtime笔记(二)—— Class结构的深入分析
Runtime笔记(三)—— OC Class的方法缓存cache_t
Runtime笔记(四)—— 刨根问底消息机制
Runtime笔记(五)—— super的本质
[Runtime笔记(六)—— Runtime的应用…待续]-()
[Runtime笔记(七)—— Runtime的API…待续]-()
Runtime笔记(八)—— 记一道变态的runtime面试题
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。