在上一篇文章objc_msgSend快速查找中,探究了函数调用的本质,即消息发送:objc_msgSend
,并用汇编代码探究了objc_msgSend
快速方法查找(即缓存查找)的流程。
在快速方法查找流程中,如果缓存命中则执行相应的方法;如果没有命中MissLabelDynamic
,则会进入慢速方法查找流程,即执行C/C++
代码中的lookUpImpOrForward
方法。
1.探索慢速查找
除通过解读汇编代码外,还可以采用debug
的方式来探索慢速方法查找的入口lookUpImpOrForward
方法。设置debug
显示汇编运行流程,见下图:
在示例代码[user sayHello8];
处设置断点,执行程序。堆栈显示如下:
方法调用本质是发送消息objc_msgSend
。在断点处按下ctrl键
,点击step into
,进入objc_msgSend
流程。
流程解析:
流程很长看不懂,流程与objc_msg_arm64.s
中的汇编流程是一致的。从一些关键点上还是可以发现一些端倪。例如掩码0x7ffffffffff8
,很熟悉,其实就是X86_64环境下的ISA_Mask值
。
# elif __x86_64__
# define ISA_MASK 0x00007ffffffffff8ULL
不难理解,即通过isa
指针的掩码运算获取类对象,然后在对象中进行一系列的操作。如果缓存没有命中,进入汇编函数_objc_msg_Send_uncached
。同样在此处设置断点,程序运行到该出后,按下ctrl键
,点击step into
,进入_objc_msg_Send_uncached
流程。见下图:
找到慢速方法查找入口lookUpImpOrForward at objc-runtime-new.mm:6394
。
2.方法查找探索与思考
用一个简单的案例,Son
继承自Father
,Son
类只有方法的声明,没有方法的实现;Father
类对方法进行了声明并实现了相关方法。
@interface Father : NSObject
-(void)sayHello;
+(void)sayNB;
@end
@implementation Father
-(void)sayHello{
NSLog(@"sayHello %s", __func__);
}
+(void)sayNB{
NSLog(@"sayNB %s", __func__);
}
@end
@interface Son : Father
-(void)say666;
-(void)sayHello;
+(void)sayNB;
@end
@implementation Son
-(void)say666{
NSLog(@"say666 %s", __func__);
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
Son *son = [[Son alloc] init];
[son say666];
[son sayHello];
[Son sayNB];
}
return 0;
}
运行结构见下图:
运行结果解析:
- 调用对象方法
say666
、sayHello
,调用类方法sayNB
,均没有报错,均成功的调用了父类Father
中的实现。 son
对象方法存储在Son
类中,son
对象isa
指向Son
类,Son
类superclass
指向Father
类对象。Son
类方法存储在Son
元类中,Son
类isa
指向Son
元类,Son
元类superclass
指向Father
元类。Son
类和Father
类有上面的2
层关系,那对象方法sayHello
和类方法sayNB
肯定是因为这个关系,从而成功找到方法实现,才会使程序没有报错。
到底是不是这样呢?继续进行源码的探究。
3.慢速查找探索
在objc runtime
源码libobjc.A.dylib
中全局搜索lookUpImpOrForward
方法。在objc_runtime_new.mm
文件中查找到lookUpImpOrForward
的方法实现。代码如下:
// 慢速查找
// 方法调用 objc_msgSend
// 1 —— 发送消息objc_msgSend 缓存快速查找(cache_t)
// 2 —— 没有命中,lookUpImpOrForward慢速查找
NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
// forward_imp
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
if (slowpath(!cls->isInitialized())) {
behavior |= LOOKUP_NOCACHE;
}
// 加锁,目的是保证读取的线程安全
runtimeLock.lock();
// 是否是已知类:判断当前类是否是已经被认可的类,即已经加载的类
checkIsKnownClass(cls);
// 判断类是否实现,如果没有,需要先实现
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// runtimeLock may have been dropped but is now locked again
runtimeLock.assertLocked();
curClass = cls;
// 递归
for (unsigned attempts = unreasonableClassCount();;) {
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
imp = cache_getImp(curClass, sel);
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.
// 在当前的类的方法列表中查找方法(采用二分查找算法),如果找到,则返回,将方法缓存到cache中
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp(false);
goto done;
}
// 未找到,superclass找到父类或者父元类继续查找,如果父类是nil,默认赋值forward_imp
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
// 父类为nil,即继承链都未找到方法实现,跳出循环
imp = forward_imp;
break;
}
}
// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// Superclass找到父类,在父类的cache中查找.
// 从父类缓存中查找 - 再次进入汇变查找
// - 如果查找到done
// - 查找不到,循环superclass,再查找父类
// Superclass cache.
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
break;
}
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
1.源码流程分析
-
初始化
forward_imp
。 -
判断当前类是否是已经被认可的类,即已经加载的类;然后建立类、父类的双向链表关系,把类的继承链确定下来,此时的目的是为了确定父类链,方便后续的循环。
// 是否是已知类:判断当前类是否是已经被认可的类,即已经加载的类 checkIsKnownClass(cls); // 判断类是否实现,如果没有,需要先实现 cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE); // runtimeLock may have been dropped but is now locked again runtimeLock.assertLocked(); curClass = cls;
-
循环开始方法查找,
for (unsigned attempts = unreasonableClassCount();;)
,当前curClass
,可以是类,也可以是元类,但是最终调用的都是对象方法。 -
首先在当前类的方法列表中查找,采用
二分查找
方式。// curClass method list. // 在当前的类的方法列表中查找方法(采用二分查找算法),如果找到,则返回,将方法缓存到cache中 Method meth = getMethodNoSuper_nolock(curClass, sel); if (meth) { imp = meth->imp(false); goto done; }
如果方法列表中找到该方法,跳转至
done
,并将方法插入缓存,也就是cache_t
中的buckets
中,并返回imp
,即步骤8
。如果方法列表中没有找到,则进入步骤5
。 -
根据
superclass
找到父类或者父元类,并赋值给curClass
。// 未找到,superclass找到父类或者父元类继续查找,如果父类是nil,默认赋值forward_imp if (slowpath((curClass = curClass->getSuperclass()) == nil)) { // No implementation found, and method resolver didn't help. // Use forwarding. // 父类为nil,即继承链都未找到方法实现,跳出循环 imp = forward_imp; break; }
如果父类是
nil
,说明已经找到NSObject类
了,默认赋值forward_imp
,跳出循环,进入步骤7
。如果父类不是nil
,进入步骤6
。 -
再对当前类进行缓存查找(汇编流程),如果缓存依然未找到,
imp = nil
,继续循环,进入步骤3
。如果找到imp
,跳转至done
,并将方法插入缓存,也就是cache_t
中的buckets
中,即步骤8
。// Superclass找到父类,在父类的cache中查找. // 从父类缓存中查找 - 再次进入汇变查找 // - 如果查找到done // - 查找不到,循环superclass,再查找父类 // Superclass cache. imp = cache_getImp(curClass, sel); 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; } if (fastpath(imp)) { // Found the method in a superclass. Cache it in this class. goto done; }
-
动态方法决议流程,可以理解为再给一次机会进行补救。
if (slowpath(behavior & LOOKUP_RESOLVER)) { behavior ^= LOOKUP_RESOLVER; return resolveMethod_locked(inst, sel, cls, behavior); }
-
查找成功,将查找到的
imp sel
插入缓存。也就是cache_t
中的buckets
中。为下次直接从缓存里面快速查找做准备。done: if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) { #if CONFIG_USE_PREOPT_CACHES while (cls->cache.isConstantOptimizedCache(/* strict */true)) { cls = cls->cache.preoptFallbackClass(); } #endif log_and_fill_cache(cls, imp, sel, inst, curClass); }
-
返回
imp
,流程结束。
总结:
源码实现确实验证了在第二部分的猜想,确实是通过类的superclass
一路向上查找方法。
源码很好理解,和我们设想的也一致,但是需要调试跟踪验证一下!依然是上面的案例进行跟踪调试!
2.本类已实现方法
对象方法say666
,Son
类中有实现,运行代码,跟踪到慢速方法查找流程中,在通过二分查找
,从方法列表中,成功获取了对应的对象方法。此时的curClass
也就是传入的cls
,即Son类
,还没有通过superclass
查找父类。在见下图:
在方法列表中查找到方法后,跳转至done
。将方法插入到方法缓存,也就是cache_t
中,形成闭环!
最终会调用cache::insert
方法,形成了闭环!下次再调用相同的方法时,就会直接从缓存中进行快速方法查找。也就是上一篇文章快速方法查找中探究的内容。
如果此过程中发生过cache_t
的扩容,则还需要重复上面的流程,快速方法查找,查找不到,慢速方法查找,插入缓存!
3.父类中有实现
对象方法sayHello
,Son
类中没有实现,父类Father
中有实现。运行代码,跟踪到慢速方法查找流程中,二分查找
从方法列表中查找方法,方法返回为空。在见下图:
继续跟踪源码,Son
通过superclass
找到了父类Father
,此时的curClass
设置为Father
,通过cache_getImp
从父类的缓存中查找对应的方法,没有找到imp
返回为空。见下图:
父类的缓存中没有找到方法后,继续for
循环,从当前类curClass
也就是Father
类的方法列表中查找,成功找到了方法。见下图:
方法查找到方法后,跳转至done
。将方法插入到方法缓存,也就是cache_t
中。
4.类方法
调用sayNB
类方法,进入慢速方法查找流程,此时curClass
为Son
元类,在元类中没有找到对应meth
。见下图:
在Son
元类中没有找到对应的方法,通过superclass
找到Father
元类,在父元类中找到了对应的方法实现。
方法查找到方法后,跳转至done
。将方法插入到方法缓存,也就是cache_t
中。同时,通过该流程可以说明,在底层没有类方法,全部是对象方法。类就是元类的对象!
5.cache_getImp分析
cache_getImp
从方法缓存中查找方法实现,cache_getImp
的具体流程是怎么样的呢?在objc_msg_arm64.s中找到了汇编实现,见下面源码:
STATIC_ENTRY _cache_getImp
GetClassFromIsa_p16 p0, 0
CacheLookup GETIMP, _cache_getImp, LGetImpMissDynamic, LGetImpMissConstant
LGetImpMissDynamic:
mov p0, #0
ret
LGetImpMissConstant:
mov p0, p2
ret
END_ENTRY _cache_getImp
最终也是调用了宏CacheLookup
,不过参数有所不同,如果没有找到对应的缓存方法,则会LGetImpMissDynamic
,也就是返回空0x0
,而不是进行慢速方法查找!
4.详解二分查找
二分查找
也称折半查找(Binary Search)
,它是一种效率较高的查找方法。但是,折半查找要求线性表必须采用顺序存储结构,而且表中元素按关键字有序排列。源码如下:
/**********************************************************************
* search_method_list_inline
**********************************************************************/
template<class getNameFunc>
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list, const getNameFunc &getName)
{
ASSERT(list);
auto first = list->begin();
auto base = first;
decltype(first) probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
// base相当于low地址,count是max地址,probe是middle地址
for (count = list->count; count != 0; count >>= 1) {
// 指针平移至中间位置
// 从首地址 + 下标 --> 移动到中间位置(count >> 1)
probe = base + (count >> 1);
// 获取该位置的sel名称
uintptr_t probeValue = (uintptr_t)getName(probe);
// 如果查找的key的keyvalue等于中间位置(probe)的probeValue,则直接返回中间位置
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
// 分类方法同名- while 平移 -- 向前在查找,判断是否存在相同的方法,保证调用的是分类的
while (probe > first && keyValue == (uintptr_t)getName((probe - 1))) {
probe--;
}
return &*probe;
}
// 如果keyValue 大于 probeValue,就往probe即中间位置的右边查找,即中间位置再右移
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
案例说明:
把method_list_t
看成[0x10,0x20,0x30,0x40,0x50,0x60,0x70,0x80]
数量为8
的有序数组。
-
现在需要查找方法
0x30
,流程如下:- 初始值:
base = 0x10,count = 8;
- 循环一:
probe = 0x50,未找到,而0x50大于0x30;左侧查找;
- 循环二:
base = 0x10,count = 4;probe = 0x30,找到!
- 初始值:
-
查找方法
0x60
,流程如下:- 初始值:
base = 0x10,count = 8;
- 循环一:
probe = 0x50,未找到,而0x50小于0x60;右侧查找;
- 循环二:
base = 0x60,count = 3;probe = 0x70,未找到;
- 循环三:
base = 0x60,count = 1;probe = 0x60,找到;
- 初始值:
5.方法查找的理解
每个对象都有一个指向所属类的指针isa。通过该指针,对象可以找到它所属的类,也就找到了其全部父类。
当向一个对象发送消息时,objc_msgSend
方法根据对象的isa
指针找到对象的类,然后在类的调度表(dispatch table)
中查找selector
。如果无法找到selector
,objc_msgSend
通过指向父类的指针找到父类,并在父类的调度表(dispatch table)
中查找selector
,以此类推直到NSObject类
。一旦查找到selector
,objc_msgSend
方法根据调度表的内存地址调用该实现。通过这种方式,message
与方法的真正实现在执行阶段才绑定。
为了保证消息发送与执行的效率,系统会将全部selector
和使用过的方法的内存地址缓存起来。每个类都有一个独立的缓存,缓存包含有当前类自己的selector
以及继承自父类的selector
。查找调度表(dispatch table)
前,消息发送系统首先检查receiver对象
的缓存。