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+1
occupied
:被占用的槽位,因为缓存是以散列表的形式存在的,所以会有空槽,而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代码的注释,我们也完全可以了解这一点。不顾一切地追求完美和性能,是一种品质。