objc_msgSend的消息分发的步骤:
- 判断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,否则
- 报错,抛出异常
struct objc_cache { unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE; unsigned int occupied OBJC2_UNAVAILABLE; Method _Nullable buckets[1] OBJC2_UNAVAILABLE; };
objc_cache的定义包含三个变量:
- mask:可以认为是当前能达到的最大index(从0开始),所以缓存的size(total)是mask+1
- occupied:被占用的槽位,因为缓存是以散列表的形式存在的,所以会有空槽,而occupied表示当前被占用的数目
- buckets:用数据表示的方法数组; Method的定义:包含方法名,方法类型以及方法实现;
缓存和散列 缓存的存储使用了散列表;为什么要用散列表呢?因为散列表检索起来比较快;
-
方法缓存存在什么地方? 我们通过runtime的头文件可以看到类的定义里就有cache字段,没错,类的所有缓存都存在metaclass上,所以每个类都只有一份方法缓存,而不是每一个类的object都保存一份;
-
父类方法的缓存只存在父类吗?还是子类也会缓存父类的方法? 在前面对objc_msgSend的追溯中我们可以看到,即便是从父类取到的方法,也会存在类本身的方法缓存里;也就是子类也会缓存父类的方法;而当用一个父类对象去调用哪个方法的时候,也会在父类的metaclass里缓存一份;
-
类的方法缓存大小有没有限制? 当_class_slow_grow是非0值的时候,只有当方法缓存第奇数次满(使用的槽位超过3/4)的时候,方法缓存的大小才会增长(会清空缓存,否则hash值就不对了);当第偶数次满的时候,方法缓存会被清空并重新利用;如果_class_slow_grow值为0,那么每一次方法缓存满的时候,其大小都会增长; 答案是没有限制,虽然这个值被设置为1,方法缓存的大小增速会慢一点,但确实是没有上限的;
- 为什么类的方法列表不直接做成散列表呢?做成list,还要单独缓存,多费事?
- 散列表是没有顺序的,Objective-C的方法列表是一个list,是有顺序的;Objective-C在查找方法的时候会顺着list一次查找,并且category的方法在原始方法list的前面,需要先被找到,如果直接用hash存方法,方法的顺序就没法保证了;
- List的方法还保存了除了selector和imp之外其他很多属性
- 散列表是有空槽的,会浪费空间