写在前面
平时一直和 @臧成威 等小伙伴做我们美团 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 tosuperis a way to call through to a method implementation defined by a superclass further up the inheritance chain. The most common use ofsuperis 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,可以尽可能的减少我们平时因为使用不当造成的奇奇怪怪的问题,也可以加深我们对这个动态语言的理解~