objc_msgSend函数
当某个对象使用语法[receiver message]来调用某个方法时,其实[receiver message]被编译器转化为:
1 | id objc_msgSend ( id self, SEL op, ... ); |
现在让我们看一下objc_msgSend它具体是如何发送消息:
- 首先根据
receiver对象的isa指针获取它对应的class - 优先在
class的cache查找message方法,如果找不到,再到methodLists查找 - 如果没有在
class找到,再到super_class查找 - 一旦找到
message这个方法,就执行它实现的IMP
我们都知道Objective-C的方法决议是动态的,但是在底层一个方法究竟是怎么找到的,方法缓存又是怎么运作的却鲜为人知。本文主要从源码角度探究了Objective-C在runtime层的方法决议(Method resolving)过程和方法缓存(Method cache)的实现。
我们来看下runtime层objc_msgSend的源码。
在objc-msg-arm.s中,objc_msgSend的代码如下:
(ps:Apple为了高度优化objc_msgSend的性能,这个文件是汇编写成的,不过即使我们不懂汇编,详尽的注释也可以让我们一窥其真面目)
1 |
|
从上述代码中可以看到,objc_msgSend(就arm平台而言)的消息分发分为以下几个步骤:
- 判断
receiver是否为nil,也就是objc_msgSend的第一个参数self,也就是要调用的那个方法所属对象 - 从缓存里寻找,找到了则分发,否则
- 利用
objc-class.mm中_class_lookupMethodAndLoadCache3方法去寻找selector- 如果支持GC,忽略掉非GC环境的方法(
retain等) - 从本
class的method list寻找selector,如果找到,填充到缓存中,并返回selector,否则 - 寻找父类的
method list,并依次往上寻找,直到找到selector,填充到缓存中,并返回selector,否则 - 调用
_class_resolveMethod,如果可以动态resolve为一个selector,不缓存,方法返回,否则 - 转发这个
selector,否则
- 如果支持GC,忽略掉非GC环境的方法(
- 报错,抛出异常
objc_cache(缓存)
在objc-cache.mm中,objc_cache的定义如下:
1 | struct objc_cache { |
它包含了下面三个变量:
mask:可以认为是当前能达到的最大index(从0开始的),所以缓存的size(total)是mask+1occupied:被占用的槽位,因为缓存是以散列表的形式存在的,所以会有空槽,而occupied表示当前被占用的数目buckets:用数组表示的hash表,cache_entry类型,每一个cache_entry代表一个方法缓存(buckets定义在objc_cache的最后,说明这是一个可变长度的数组)
而cache_entry的定义如下:
1 | typedef struct { |
很明显通过 SEL 和 IMP 就可以调用缓存的方法了。
缓存和散列
缓存的存储使用了散列表。
为什么要用散列表呢?因为散列表检索起来更快(时间复杂度往往是O(1))
插入缓存
我们来看下是方法缓存如何散列和检索的:
1 | /*********************************************************************** |
这是往方法缓存里存放一个方法的代码(cache fill),我们可以看到sel被散列后找到一个空槽放在buckets中,而CACHE_HASH的定义如下:
1 |
这段代码就是利用了sel的指针地址和mask做了一下简单计算得出的。
查找缓存
从散列表取缓存则是利用汇编语言写成的(是为了高度优化objc_msgSend而使用汇编的)。我们看objc-msg-arm.mm 里面的CacheLookup方法:
1 | .macro CacheLookup /* selReg, classReg, missLabel */ |
先求hash,去buckets里找,找不到按照hash冲突的规则继续向下,直到最后。
特殊方法
为什么会有_class_lookupMethodAndLoadCache3这个方法?
这个方法的实现如下所示:
1 | /*********************************************************************** |
如果单纯看方法名,这个方法应该会从缓存和方法列表中查找一个方法,但是如第一节所讲,在调用这个方法之前,我们已经是从缓存无法找到这个方法了,所以这个方法避免了再去扫描缓存查找方法的过程,而是直接从方法列表找起。从Apple代码的注释,我们也完全可以了解这一点。不顾一切地追求完美和性能,是一种品质。