我们在 C底层原理11-objc_msgSend源码分析(方法查找快流程) 一文中,探索了objc_msgSend
方法快速查找流程(从缓存中获取方法),cache
类型的源码在OC底层全部用汇编实现
,优点是快,我们发现如果CacheLookup
流程找不到的话,就会进入CheckMiss
-> __objc_msgSend_uncached
-> MethodTableLookup
-> lookUpImpOrForward
流程,lookUpImpOrForward
也就是我们今天要研究的慢速查找流程,它的源码在objc
层中,用OC
实现
一、准备工作
1.1、objc4可编译源码,可直接跳到文章最后,下载调试好的源码
1.2、在源码中新增类GomuPerson
如下
GomuPerson.h
- (void)sayNO;
GomuPerson.m
//不实现方法,方便后面二分法研究
二、lookUpImpOrForward源码探索
2.1 lookUpImpOrForward
方法源码
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
//: -- 创建一个默认forward_imp,并赋值_objc_msgForward_impcache
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
//: -- 创建一个imp,值为nil
IMP imp = nil;
//: -- 创建一个类curClass
Class curClass;
runtimeLock.assertUnlocked();
//: -- 因为是多线程,并且后面要开始耗时操作了,防止获取的时候正在cache的情况没取到,所以再取一次cache
//: -- 如果在当前cls的cache没有找到imp,则继续执行
//: -- 如果找到了,则跳转到done_nolock
if (fastpath(behavior & LOOKUP_CACHE)) {
//: -- 从cls的缓存中取imp
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
runtimeLock.lock();
//: -- 判断当前cls是否为已知类
//: -- 防止人为制造类,进行CFI攻击
checkIsKnownClass(cls);
//: -- 如果类没有实现,则去实现,并对其继承链进行实现关联
//: -- 类是双向链表,所以需要双向绑定
//: -- cls->superclass = supercls 给cls的父类赋值supercls
//: -- addSubClass(supercls, cls) 给supercls添加subClass
//: -- 猜想,是分类走的流程
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
//: -- 递归实现类/元类继承链的initialize方法
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
runtimeLock.assertLocked();
//: -- 把cls赋值给curClass
curClass = cls;
//: -- 开始for循环,递归查找
//: -- 自己的methods -> 父类的cache -> 父类的methods -> 直到nil
for (unsigned attempts = unreasonableClassCount();;) {
//: -- 首先从自己的methods里面查找,这里牵涉到一个优化算法,我们后面单独聊
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
//: -- 如果找到了meth,则拿到imp,返回
imp = meth->imp;
goto done;
}
//: -- 把curClass的父类赋值给curClass,然后判断是否为nil
//: -- NSObject的父类为nil,当遍历到nil的时候就默认给imp赋值forward_imp,跳出循环
//: -- 如果不为nil,继续往下走
if (slowpath((curClass = curClass->superclass) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;
}
//: -- 防止死循环,给一个出口
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
//: -- curClass已经被赋值为它的父类,所以这里从父类的缓存里面找imp
imp = cache_getImp(curClass, sel);
//: -- 当curClass的父类为nil的时候,imp会被赋值forward_imp,就会走这里,跳出循环
if (slowpath(imp == forward_imp)) {
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
//: -- 如果找到imp,则跳转到done
//: -- 找自己的时候不会走这里,父类中存在imp才走这里
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
//: -- 如果遍历完了父类都没有找到imp,则进行消息处理机制,动态方法决议,这个后面我们会单独开一期来进行讲解
if (slowpath(behavior & LOOKUP_RESOLVER)) {
//: -- 这个算法超级牛 只走一次
//: -- behavior不管是什么,假设是3,0011
//: -- LOOKUP_RESOLVER = 2,0010
//: -- 第一次 0011 & 0010 = 0010 为真
//: -- 进入判断中 behavior = 0011 ^ 0010 = 0001
//: -- 第二次进入 0001 & 0010 = 0000
//: -- 第一次求&,behavior第二位必须为1,才能进入条件,进入之后,求 ^ ,第二位就会变成0,那下次再进入的时候,再求&必然为0,不再进判断
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
//: -- 把找到的imp存进缓存,方便下次快速查找
log_and_fill_cache(cls, imp, sel, inst, curClass);
runtimeLock.unlock();
done_nolock:
//: -- 判断imp是否被赋值了forward_imp,如果是则返回nil
//: -- 防止多线程操作imp没找到,被赋值为forward_imp时,成功返回了错误的imp
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
//: -- 如果在开始慢查询之前,从缓存中找到了则直接返回当前的imp
return imp;
}
- 得出以下流程:
自己的cache
-> 自己的methods
-> 父亲的cache
-> 父亲的methods
-> 父亲的父亲的cache
-> 父亲的父亲的methods
->nil
->resolveMethod_locked
(动态方法决议) -
附流程图
2.2 getMethodNoSuper_nolock
方法源码
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
//: -- 拿到cls的data()里面存的methods()
auto const methods = cls->data()->methods();
//: -- 循环(目的不清楚),断点调试的时候要走很多次,加载很多系统的方法列表
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
//: -- 从方法列表中找当前sel
//: -- 类中自定义的方法第一次循环的时候走这里,存在methods.beginLists()
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
- 进入
search_method_list_inline
后会调用findMethodInSortedMethodList
2.3 findMethodInSortedMethodList
源码
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
ASSERT(list);
//: -- 把第list一个元素赋值给first
const method_t * const first = &list->first;
//: -- 把fistr赋值给base
const method_t *base = first;
const method_t *probe;
//: -- 把当前传入的sel转成uintptr_t类型,并赋值给keyValue
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
//: -- 二分法查找sel,向前查找
for (count = list->count; count != 0; count >>= 1) {
//: -- 指针平移,相当于地址平移到中间位置,count >> 1相当于count/2
probe = base + (count >> 1);
//: -- 拿到当前probe中的name,这里对应sel
uintptr_t probeValue = (uintptr_t)probe->name;
//: -- 判断当前keyValue是否等于probeValue
if (keyValue == probeValue) {
//: -- 判断是否有分类方法
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
//: -- 如果有分类方法,并且keyValue不是第一个元素
//: -- 如果类和分类都有同一个方法,则会走这里
//: -- 取当前类前面一位,即分类方法,分类方法后加如methods,优先读取
probe--;
}
//: -- 返回当前找到的method_t
return (method_t *)probe;
}
//: -- 查keyValue比二分位大的时候需要走这里
if (keyValue > probeValue) {
//: -- 如果目标在二分位右边,则讲当前base右移一位
base = probe + 1;
//: -- count自减
count--;
}
}
return nil;
}
2.3.1 对象方法和属性调用前在methods中的储存顺序:
- 先存
对象方法
,按照对象方法
在.m中的实现顺序依次存储
属性
按照属性
在.h/.m中的声明顺序逆序存储
,先get
,再set
如下图:(.h中改成.m实现,画错了)
2.3.2 对象方法调用之后在methods中的储存顺序:
- 判断当前
类的类别
中是否有该对象方法
,有就先存储类别中的对象方法
,再存储类中的对象方法
(如果类中有,类中可能没有,只有.m实现也会存储),如果没有,则直接存当前类中的对象方法
- 先调用的
存储在前面
,后调用
的存储在上一个方法的后面
(即相对最前面),最先调用
的存在最前面
没有调用的
按照以前的顺序排在后面
如下图:
2.3.3 findMethodInSortedMethodList
中,二分法算法介绍
-
查询目标一直小于遍历中位数,流程图如下
-
查询目标大于遍历中位数,流程图如下
三、拓展知识
3.1 交替进入的if的位运算
//: -- LOOKUP_RESOLVER = 2
//: -- 省略外层for循环
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
- 把
LOOKUP_RESOLVER
换成2进制0010
-
behavior
分2种情况,一种是第二位为1,第二种是第二位不为1,分别用1111
,1101
举例 - 随着
behavior
的累加,当第二位为1的时候就能进,第二位为0的时候就不能进(为1不进,为2进,为3不进,为4进...)
第一种情况
1. 第一种`behavior`为1111,`behavior & LOOKUP_RESOLVER`=`1111 & 0010`,为真,进入`if`
2. 进入后`behavior ^= LOOKUP_RESOLVER`,`behavior ` = `1111 ^ 0010` = `1101`
3. 下次循环过来,`behavior & LOOKUP_RESOLVER` = `1101 & 0010`,为假,不再进入循环
得出结论:第二位为1,必然可以进入
循环一次
,进入后与0010
异或之后,第二位变0,下次与0010
求与的时候,必定为假,这样就保证了if
只走一次
第二种情况
第二种`behavior`为1101,`behavior & LOOKUP_RESOLVER`=`1101 & 0010`,为假,不能进入`if`
得出结论:第二位为0,连if都进不去
3.2 方法调用的本质,就是找imp,没有实现就找不到imp,和方法声明没关系
类
中声明并实现
了方法
,类别
中未声明且未实现
,则调用类中的方法
类
中声明并实现
了方法
,类别
中声明但未实现
,则调用类中的方法
类
中声明并实现
了方法
,类别
中未声明但实现
,则调用类别中的方法
类
中声明并实现
了方法
,类别
中声明且实现
,则调用类别中的方法
类
中声明未实现
方法
,类别
中未声明但实现
,则调用类别中的方法
类
中未声明但实现
方法
,类别
中声明但未实现
,则调用类中的方法
总结:
类和类别
中有一个实现
,则谁实现就调用谁的
类和类别
中都实现
,则优先调用类别
的类和类别
如果都没有声明
,则不能直接通过[p sayNO]
方式调用,可以通过performSelector
,objc_msgSend
调用,即方法声明不是必须的
objc_msgSend(p, sel_registerName("sayNO"));
[p performSelector:@selector(sayNO)];