在上一篇文章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对象的缓存。