一、面试题
- 创建
OC项目, 定义Person类, 添加name属性和实现print方法, 如下图所示

- 在
ViewController中的-viewDidLoad方法里实现下面的代码

问: 代码是否会报错? 是否能够顺利运行? 是否能打印?, 如果能, 会打印什么?
- 运行程序, 可以看到程序没有报错, 且正常运行, 并有如下打印

Person name is <ViewController: 0x7ff78ef08880>
问: 为什么这么打印?
二、解析
- 现有如下代码

person是一个指针, 存储着[[Person alloc] init]的地址, 所以person指向刚创建的Person实例对象

- 我们知道, 一个对象在底层就是一个结构体, 它的第一个成员变量是
isa, 所以[[Person alloc] init]在底层就是下面的样子

- 因为
isa是Person对象中的第一个成员变量, 所以isa的地址与[[Person alloc] init]的地址相同

- 当
person调用-print方法时, 本质是通过isa找到[Person class]对象, 然后找到-print方法来调用, 所以指针关系图如下

- 在
-print方法中, 有打印成员变量_name, 所以需要通过[[Person alloc] init]找到_name存储的值 - 在底层是通过
isa的地址 + 8找到_name的地址, 然后访问_name存储的内容 - 这样就可以顺利打印了

obj能调用print方法的原因
- 接着我们看回面试题中的代码
id cls = [Person class];
- 这一句代码表示
cls指针指向Person的类对象

- 接着下一句,
obj存储着cls的地址
void *obj = &cls;
- 也就是说
obj指向cls

- 这个指向和上面的
person指向类似, 有下面这种指针结构

- 最后看调用方法这一句
[(__bridge id)obj print];
-
在结构上
person调用方法的流程是:person->isa->[Person class]obj调用的流程与person类似.obj->cls->[Person class]
-
这就是
obj能够调用-print方法的原因
局部变量在栈中的排列顺序
- 定义三个局部变量, 查看他们的地址

- 三个局部变量的地址
0x7ffeeec0c9d8
0x7ffeeec0c9d0
0x7ffeeec0c9c8
-
可以看到, 连续的三个局部变量, 在栈中的存储顺序是连续的
-
接着我们看下面的这种情况, 在
cls前面添加test变量, 运行看一下打印

- 此时在内存中的排序如下

obj通过找到cls, 然后用cls的地址+8, 找到的就是test的地址, 所以打印时, 找到的_name就是test的值123
那么面试题中的打印为什么是
ViewController的实例对象呢?
super的底层结构
- 查看
ViewController.cpp底层结构

- 其中
[super viewDidLoad];这一句在底层代码是下面这一句
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
- 整理后, 代码如下
objc_msgSendSuper({self, class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
- 也就是说, 在栈中有个结构体
{self, class_getSuperclass(objc_getClass("ViewController"))} - 此时, 栈中的结构如下图所示

- 所以此时通过
cls的地址+8, 找到的就是self, 也就是当前控制器对象