前言
关于运行时的一道面试题
[(__bridge id)obj intruduceName]能否打印,如果能打印打印结果是什么?
能否调用:答案是可以的
[user intruduceName]打印结果为张三,都很清楚,实例变量调用兑现方法,但是为什么能打印值得探索一下
在对user发送消息转化为objc_msgSend(id receiver, SEL _cmd)时,进行方法的查找,而方法存在于类的结构体objc_class中,查找流程为通过我们传进去的user对象的isa指针,通过ISA_MASK进行位运算 & 操作,得到类的信息,进行方法的查找,所以关键是得到objc_class的isa指针地址,就可以进行方法的调用,而不是objc_msgSend传的是什么值
[(__bridge id)obj intruduceName] cls代表的是Class的首地址,而Class的首地址和objc_class中的isa地址是相同的,所以obj进行调用时,转到objc_msgSend的流程中,正是传的UserModel的isa地址,所以可以调用UserModel的方法。
如果能够调用,调用结果是什么?
ViewController.m执行clang命令变为cpp文件时,可以清除的看到,获取name值时,通过类的isa指针,加上name成员变量的偏移量得到 name的值,C,具体怎么将.m变为.cpp文件,可以自行搜索
在对类进行实例化申请内存空间时,申请的内存空间大小和类的成员变量多少以及字节大小相关,类UserModel转化为底层的objc_class结构体时,主要包含的变量有 isa & 成员变量,地址依次进行偏移。而调用name的get方法时,也是根据了的存储原理,获得类的isa指针地址,然后计算name的地址偏移后,获得name的值。
先说一下打印结果为:<ViewController: 0x7fbcd5e09240>,打印的是viewcontroller的实例对象self,为什么是如此结果?
在进行打印时,self.name需要根据传进去的obj,也就是cls指针在栈的存储位置加上一定的偏移量得到的,这里的偏移量为 UserModel 5 个成员变量大小,也就是8字节 * 5 = 40,这些平移从哪里开始,到哪里结束,结束时的打印结果是什么
参数入栈 & 结构体入栈问题
方法在调用过程中,方法体内局部变量都是存储在占空间的,我们都知道。但是方法中还有哪些数据是存储在栈中的呢
在ViewController中分别定义了一个结构体 & 一个带有两个参数的函数,通过打印栈地址可以发现
- 结构体入栈,并且结构体成员变量按照逆序存储,也就是num2,存储在高地址,num1存储在地地址
- 函数参数入栈,并且是顺序入栈,第一个参数在高地址,剩下的一次排开
隐藏参数入栈:每个方法在调用时,转化为objc_msgSend(id receiver, SEL _cmd)时,都会默认带两个参数,在viewDidLoad中也存在,
super结构体入栈:[super viewDidLoad]会转化为objc_msgSendSuper(objc_super super, SEL sel,) objc_super是一个结构体,下面是他的结构体形式
struct objc_super {
//消息接收者
__unsafe_unretained _Nonnull id receiver;
//objc2版本和上个版本作区分
#if !defined(__cplusplus) && !__OBJC2__
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
//这里的super_class,可以传输任何的值,代表的是第一个搜索类,如果搜索不到会继续向父类寻找
};
现在我们所用的objc都是2版本,所以结构体中有两个参数,一个是 receiver, 一个是super_class
最后viewDidLoad方法中,总共入栈的情况如下
打印结果揭晓
所以 &cls指针平移 40个字节,结果过就正好是隐藏参数 self,所以会打印出 <ViewController: 0x7ffee1c580c8>