简谈 OC 中陌生而又熟悉的 super

写在前面

平时一直和 @臧成威 等小伙伴做我们美团 iOS 客户端的发布工程相关事情,感觉和 iOS 越来越没啥关系了,有一天晚上吃饭,我们几个同学讨论要不然让成威在平时抽空给我们讲述一些 iOS 中好玩的东西,我们总结下来也算是一种技术积累和提升。

于是在我们的威逼利诱下,成威答应了这个事情。

此篇文章是在听了成威某一天的分享后,我抽空稍加调研总结出来的。

熟悉的 super

我们平时利用 OC 开发 iOS 的时候,会经常用到 super 这一关键字,比如我们在创建一个新的 ViewController 后会首先看到的:

1
2
3
- (void)viewDidLoad {
[super viewDidLoad];
}

或者是我们在一个类的初始化器 (initializer) 里面,我们需要这样写:

1
2
3
4
5
6
7
- (instancetype)init {
self = [super init];
if (self) {
// init something
}
return self;
}

有时候我们调用一些其他的 CocoaTouch 方法,也需要我们手动调用 super 的方法,例如:

1
2
3
4
5
- (void)updateConstraints {
// do something to setup views constraints
[super updateConstraints];
}

以上,都是我们平时比较常见的一些带有 super 的用法,看起来还是比较正常的,也比较能够清楚地知道这些用法的含义。

接下来我们看一个不太常见的代码。

陌生的 super

1
2
3
4
5
6
7
8
9
10
11
12
@implementation OCTChild : OCTFather
- (instancetype)init {
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end

那么在 OCTChild- init 的时候,它会输出什么?

1
2
2017-02-25 16:38:07.064 OC Test Project[4633:537030] OCTChild
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 to super is a way to call through to a method implementation defined by a superclass further up the inheritance chain. The most common use of super is when overriding a method.

用我们中国人的话来说,即为 super 是一个关键字,给 super 发消息实际上是一种调用继承链上父类实现的方法的方式,通常用于子类需要重写父类方法又想保留父类行为的时候。

上面我们提到的 - viewDidLoad 里面的调用,实际上是想在子类初始化自己之前首先执行父类的一些初始化操作。

- init 则是利用父类(甚至是祖父类)的初始化器先创建一个原始的自己,再进行定制化操作,保证 OOP 中的一些继承特性。

最后的 - updateConstraints 我们可以理解为,在完成自己组件的 autolayout约束设定后,需要调用父类的更新设定以保证约束生效。

这没问题,我记住了,我们就是用 super 来调用父类的方法嘛,看起来还是比较容易理解的。

可是,上面那个打印 [super class] 的代码为啥看起来不想这么回事啊!?

这并不是我的本意!

我们再来看一个和 [super class] 类似的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@implementation OCTFather
- (void)selfIntroduce {
[self saySomething];
}
- (void)saySomething {
NSLog(@"%s", "I am a father!");
}
@end
@implementation OCTChild : OCTFather
- (void)selfIntroduce {
[self saySomething];
[super selfIntroduce];
}
- (void)saySomething {
NSLog(@"%s", "I am a child!");
}
@end

这里我们执行一下 OCTChild 的实例方法 - shoutOut

1
2
OCTChild *child = [[OCTChild alloc] init];
[child selfIntroduce];

我们来看下结果

1
2
2017-02-25 17:46:22.014 OC Test Project[6099:782572] I am a child!
2017-02-25 17:46:22.014 OC Test Project[6099:782572] I am a child!

按照上面我们说的 super 的用法,我们是不是该这样理解?

1
2
3
4
5
6
7
路人甲:"这位小朋友,来做个自我介绍!“
小孩:”我是儿子!爸,你自己说吧!“
爸:“我是儿子!”
...

这当爹的傻了吧?

不过我们回顾下上面 [super class] 的那坨代码,可以看出这两个都得出了一个反常理的结果!

这并不是我的本意!

runtime 问题吧?

虽说上面的两个问题有些不符合常理,但是面对这样的结果,我们还是要有个突破口来找寻答案。

众所周知,Objective-C 是一个动态语言,所谓的方法调用实际上是在运行时的一个消息发送。

1
id objc_msgSend(id theReceiver, SEL theSelector, ...)

那么,super 的话,receiver 应该是自己的父类吧?可是这样的话,应该是个父类的实例,可是什么时候生成的实例呢?每调用一次都要生成一个实例,什么时候销毁的呢?整个生命周期是啥样的呢?

感觉跑偏了,不要瞎想了,我们用 clang 看一下咋回事应该是比较直接的。

利用 clang 重写的 C++ 代码,我们找到了这样一些东西:

1
2
3
4
static void _I_OCTChild_selfIntroduce(OCTChild * self, SEL _cmd) {
((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("saySomething"));
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("OCTChild"))}, sel_registerName("selfIntroduce"));
}

这就是上面我们提到的 OCTChild 中的 - selfIntroduce 方法,方法块中第一行的 [self saySomething] 已经被转成了我们比较熟知的样子:

1
((void (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("saySomething"));

下面那句好像看起来和上面不一样。这个 objc_msgSendSuper 应该就是 super 调用方法时候真正的实现。

我们不难看出,这个函数有两个参数:

  1. (__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("OCTChild"))}
  2. sel_registerName("selfIntroduce")

而第一个参数实际上是个名为 objc_superstruct

1
2
3
4
5
6
7
8
9
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
__unsafe_unretained Class super_class;
/* super_class is the first class to search */
};

所以,大体上我们可以确定,在编译期的时候,我们的这个 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
2
objc_msgSend(self, @selector(selfIntrduce))
objc_msgSendSuper({self, class_getSuperclass(objc_getClass("OCTChild"))},@selector(selfIntroduce))
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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@implementation OCTFather
- (void)selfIntroduce {
NSLog(@"%@: %s", self, "Begin to introduce myself at superclass!");
[self saySomething];
}
- (void)saySomething {
NSLog(@"%s", "I am a father!");
}
@end
@implementation OCTChild : OCTFather
- (void)selfIntroduce {
NSLog(@"%@: %s", self, "Begin to introduce myself!");
[self saySomething];
[super selfIntroduce];
}
- (void)saySomething {
NSLog(@"%s", "I am a child!");
}
@end

其实就是加了两个在自我介绍开始时的 log

结果证明了上面我们的分析:

1
2
3
4
2017-02-25 21:35:34.293 OC Test Project[9496:1418664] <OCTChild: 0x6100000182c0>: Begin to introduce myself!
2017-02-25 21:35:34.294 OC Test Project[9496:1418664] I am a child!
2017-02-25 21:35:34.294 OC Test Project[9496:1418664] <OCTChild: 0x6100000182c0>: Begin to introduce myself at superclass!
2017-02-25 21:35:34.294 OC Test Project[9496:1418664] I am a child!

简单总结下我们之前的所有分析:

  1. super 不同于 self,它不是个对象,而是个 flag
  2. 用于 objc_msgSendSuper 的结构体 objc_super 是编译时确定的,里面包含了当前类的父类信息
  3. [super someMethod] 会去利用结构体中的父类信息,从这个父类开始顺着继承链向上查找,直到找到第一个实现 - someMethod 这个方法的类
  4. 找到方法后,利用结构体中的 receiver,也就是一开始触发这个方法调用的实例,调用这个方法实现。

所以说,[super class] 经过这么一大圈的转换,实际上变成了 [self class] 了。

而我们刚才的那个自我介绍的例子,也是这样实现方式的体现,不过由于碰巧和父类有同样的实现逻辑,并且还覆写了同样的方法,最后才有这样的奇怪结果。

想象着还挺危险的,不过也可以利用这样的黑魔法去玩一些高端的实现。

为什么不在运行时确定?

既然 OC 是一个动态语言,那么为什么 super 不能和 self 一样运行时来查找所属关系呢?

这个问题问得好,我们先来看下 self 在运行时是怎么玩的?

1
NSLog(@"%@", self);

得到的答案很明确,self 是自己这个类的实例对象。

很好,我们就按照这个思路假设 super 就是一个 “self 的父类”。

那么就简单的重新回顾下我们之前的那个例子:

self 是一个 OCTChild 的实例对象,那么 super 是这个对象的父类。

super-1

看上去没啥问题,那么我们现在如果加一个 OCTGrandFather 类,使 OCTFather 继承这个类,那结果呢?

self 还是 OCTChild 的一个实例对象,只不过在 OCTFather 中增加一个 super 的方法调用,按照我们的期望,应该是会找到 OCTGrandFather 这个类。

但实际上呢?

super-2

如图所示,一开始调用没啥问题,刚才我们也验证过了,但是当 OCTFather 调用 super 方法的时候,由于是运行时确定 super 为 “self 的父类”,那么它就又会回到 self 所表示的 OCTChild 的实例对象这里来,然后重新向上找父类。

等于说,如果是运行时确定的,那么 super 仅仅只能找到 self 自己的父类,再往上就会回来往复循环。

正是因为这个原因,所以要在编译期就确定 super 的从属关系而不是在运行时去做。

同理,如果你想要打印一个 super 或者添加一个带有 super 方法调用的运行时方法,编译器就会立刻告诉你我不认识这个 super

super-3

小结

super 这个东西虽说平时用的很多,并且在 Xcode 的语法高亮上和 self 一样,但是本质上却是和 self 有着天壤之别。通过各种手段去认识了解 super,可以尽可能的减少我们平时因为使用不当造成的奇奇怪怪的问题,也可以加深我们对这个动态语言的理解~