小码哥iOS学习笔记第十五天: super面试题

1,582 阅读3分钟

一、面试题

  • 创建OC项目, 定义Person类, 添加name属性和实现print方法, 如下图所示

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

问: 代码是否会报错? 是否能够顺利运行? 是否能打印?, 如果能, 会打印什么?

  • 运行程序, 可以看到程序没有报错, 且正常运行, 并有如下打印

Person name is <ViewController: 0x7ff78ef08880>

问: 为什么这么打印?

二、解析

  • 现有如下代码

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

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

  • 因为isaPerson对象中的第一个成员变量, 所以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, 也就是当前控制器对象