写在前面
平时一直和 @臧成威
等小伙伴做我们美团 iOS 客户端的发布工程相关事情,感觉和 iOS 越来越没啥关系了,有一天晚上吃饭,我们几个同学讨论要不然让成威在平时抽空给我们讲述一些 iOS 中好玩的东西,我们总结下来也算是一种技术积累和提升。
于是在我们的威逼利诱下,成威答应了这个事情。
此篇文章是在听了成威某一天的分享后,我抽空稍加调研总结出来的。
熟悉的 super
我们平时利用 OC 开发 iOS 的时候,会经常用到 super
这一关键字,比如我们在创建一个新的 ViewController
后会首先看到的:
1 | - (void)viewDidLoad { |
或者是我们在一个类的初始化器 (initializer
) 里面,我们需要这样写:
1 | - (instancetype)init { |
有时候我们调用一些其他的 CocoaTouch
方法,也需要我们手动调用 super
的方法,例如:
1 | - (void)updateConstraints { |
以上,都是我们平时比较常见的一些带有 super
的用法,看起来还是比较正常的,也比较能够清楚地知道这些用法的含义。
接下来我们看一个不太常见的代码。
陌生的 super
1 | @implementation OCTChild : OCTFather |
那么在 OCTChild
被 - init
的时候,它会输出什么?
1 | 2017-02-25 16:38:07.064 OC Test Project[4633:537030] OCTChild |
看到这里,有的同学可能就笑了,这不是个常见的 iOS 面试题么?有什么陌生的?
的确,这个代码之前在我们网红 孙源 @sunnyxx
的博文中也出现过,当时他也简单分析了出现上述代码执行结果的原因。
super 到底什个什么鬼?
官方文档中提到
There’s another important keyword available to you in Objective-C, called
super
. Sending a message tosuper
is a way to call through to a method implementation defined by a superclass further up the inheritance chain. The most common use ofsuper
is when overriding a method.
用我们中国人的话来说,即为 super
是一个关键字,给 super
发消息实际上是一种调用继承链上父类实现的方法的方式,通常用于子类需要重写父类方法又想保留父类行为的时候。
上面我们提到的 - viewDidLoad
里面的调用,实际上是想在子类初始化自己之前首先执行父类的一些初始化操作。
而 - init
则是利用父类(甚至是祖父类)的初始化器先创建一个原始的自己,再进行定制化操作,保证 OOP 中的一些继承特性。
最后的 - updateConstraints
我们可以理解为,在完成自己组件的 autolayout
约束设定后,需要调用父类的更新设定以保证约束生效。
这没问题,我记住了,我们就是用 super
来调用父类的方法嘛,看起来还是比较容易理解的。
可是,上面那个打印 [super class]
的代码为啥看起来不想这么回事啊!?
这并不是我的本意!
我们再来看一个和 [super class]
类似的代码:
1 | @implementation OCTFather |
这里我们执行一下 OCTChild
的实例方法 - shoutOut
1 | OCTChild *child = [[OCTChild alloc] init]; |
我们来看下结果
1 | 2017-02-25 17:46:22.014 OC Test Project[6099:782572] I am a child! |
按照上面我们说的 super
的用法,我们是不是该这样理解?
1 | 路人甲:"这位小朋友,来做个自我介绍!“ |
这当爹的傻了吧?
不过我们回顾下上面 [super class]
的那坨代码,可以看出这两个都得出了一个反常理的结果!
这并不是我的本意!
runtime 问题吧?
虽说上面的两个问题有些不符合常理,但是面对这样的结果,我们还是要有个突破口来找寻答案。
众所周知,Objective-C
是一个动态语言,所谓的方法调用实际上是在运行时的一个消息发送。
1 | id objc_msgSend(id theReceiver, SEL theSelector, ...) |
那么,super
的话,receiver 应该是自己的父类吧?可是这样的话,应该是个父类的实例,可是什么时候生成的实例呢?每调用一次都要生成一个实例,什么时候销毁的呢?整个生命周期是啥样的呢?
感觉跑偏了,不要瞎想了,我们用 clang
看一下咋回事应该是比较直接的。
利用 clang
重写的 C++
代码,我们找到了这样一些东西:
1 | static void _I_OCTChild_selfIntroduce(OCTChild * self, SEL _cmd) { |
这就是上面我们提到的 OCTChild
中的 - selfIntroduce
方法,方法块中第一行的 [self saySomething]
已经被转成了我们比较熟知的样子:
1 | ((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("saySomething")); |
下面那句好像看起来和上面不一样。这个 objc_msgSendSuper
应该就是 super
调用方法时候真正的实现。
我们不难看出,这个函数有两个参数:
(__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("OCTChild"))}
sel_registerName("selfIntroduce")
而第一个参数实际上是个名为 objc_super
的 struct
:
1 | /// Specifies the superclass of an instance. |
所以,大体上我们可以确定,在编译期的时候,我们的这个 struct
就已经确定了里面 super_class
的值(虽说 objc_getClass()
是个运行时函数,但是入参是个固定的类名哦)。
回顾下 self
的本质,实际上是一个隐式参数,表示用来接收消息的自己类的实例对象。
而 super
实际上则是个指令符号(就是个 flag),编译器在看到我们写的 [super someMethod]
的时候,会编译成刚才我们看到的 objc_msgSendSuper
。
综上所述,super
相关的方法调用方式实际上在编译期间就已经确定了。
真相只有一个!
原理搞清楚了,我们再回来看下刚才的问题,为啥调用父类的 - selfIntroduce
方法还是会喊出“我是儿子”这个话?
按照刚才我们的研究步骤,我们一步一步来重现。
1 一开始的 child
对象调用自己的实例方法 - selfIntroduce
,实际上底层调用是这样子的:
1 | objc_msgSend(self, @selector(selfIntrduce)) |
2 然后执行方法实现中 [self saySomething]
和 [super selfIntroduce]
1 | objc_msgSend(self, @selector(selfIntrduce)) |
1 | 注意:这里的 self 为我们一开始生成的 OCTChild 的实例对象 child |
3 [self saySomething]
执行,打印出 OCTChild
中的实现 I am a child!
4 [super selfIntroduce]
执行,查找 OCTChild
父类中是否有 - selfIntroduce
方法,按照继承链,找到它的第一个父类 OCTFather
,然后调用其实现,执行 [self saySomething]
1 | objc_msgSend(self, @selector(selfIntrduce)) |
1 | 注意:这里的 self 仍为我们一开始生成的 OCTChild 的实例对象 child,它是通过 objc_msgSendSuper 里面的 objc_super 这一 struct 带过来的。 |
5 [self saySomething]
执行,打印出 OCTChild
中的实现 I am a child!
执行过程完成。
为了直观一些,我们可以稍微调整下代码:
1 | @implementation OCTFather |
其实就是加了两个在自我介绍开始时的 log
结果证明了上面我们的分析:
1 | 2017-02-25 21:35:34.293 OC Test Project[9496:1418664] <OCTChild: 0x6100000182c0>: Begin to introduce myself! |
简单总结下我们之前的所有分析:
super
不同于self
,它不是个对象,而是个 flag- 用于
objc_msgSendSuper
的结构体objc_super
是编译时确定的,里面包含了当前类的父类信息 [super someMethod]
会去利用结构体中的父类信息,从这个父类开始顺着继承链向上查找,直到找到第一个实现- someMethod
这个方法的类- 找到方法后,利用结构体中的 receiver,也就是一开始触发这个方法调用的实例,调用这个方法实现。
所以说,[super class]
经过这么一大圈的转换,实际上变成了 [self class]
了。
而我们刚才的那个自我介绍的例子,也是这样实现方式的体现,不过由于碰巧和父类有同样的实现逻辑,并且还覆写了同样的方法,最后才有这样的奇怪结果。
想象着还挺危险的,不过也可以利用这样的黑魔法去玩一些高端的实现。
为什么不在运行时确定?
既然 OC 是一个动态语言,那么为什么 super
不能和 self
一样运行时来查找所属关系呢?
这个问题问得好,我们先来看下 self
在运行时是怎么玩的?
1 | NSLog(@"%@", self); |
得到的答案很明确,self
是自己这个类的实例对象。
很好,我们就按照这个思路假设 super
就是一个 “self
的父类”。
那么就简单的重新回顾下我们之前的那个例子:
self
是一个 OCTChild
的实例对象,那么 super
是这个对象的父类。
看上去没啥问题,那么我们现在如果加一个 OCTGrandFather
类,使 OCTFather
继承这个类,那结果呢?
self
还是 OCTChild
的一个实例对象,只不过在 OCTFather
中增加一个 super
的方法调用,按照我们的期望,应该是会找到 OCTGrandFather
这个类。
但实际上呢?
如图所示,一开始调用没啥问题,刚才我们也验证过了,但是当 OCTFather
调用 super
方法的时候,由于是运行时确定 super
为 “self
的父类”,那么它就又会回到 self
所表示的 OCTChild
的实例对象这里来,然后重新向上找父类。
等于说,如果是运行时确定的,那么 super
仅仅只能找到 self
自己的父类,再往上就会回来往复循环。
正是因为这个原因,所以要在编译期就确定 super
的从属关系而不是在运行时去做。
同理,如果你想要打印一个 super
或者添加一个带有 super
方法调用的运行时方法,编译器就会立刻告诉你我不认识这个 super
。
小结
super
这个东西虽说平时用的很多,并且在 Xcode 的语法高亮上和 self
一样,但是本质上却是和 self
有着天壤之别。通过各种手段去认识了解 super
,可以尽可能的减少我们平时因为使用不当造成的奇奇怪怪的问题,也可以加深我们对这个动态语言的理解~