注册

lookUpImpOrForward 消息慢速查找(上)

上篇文章分析到了_obje_msgSend查找cache消息快速查找,最终会从汇编代码进入_lookUpImpOrForward进行慢速查找。这篇文章将详细分析这个流程。

一、汇编中找不到缓存

在汇编代码中只有_lookUpImpOrForward的调用而没有实现,代码中直接搜这个也是搜不到的。因为实现在c/c++代码中,需要搜索lookUpImpOrForward。声明如下:

extern IMP lookUpImpOrForward(id obj, SEL, Class cls, int behavior);

那么参数肯定也就是汇编中传过来的,汇编中调用如下:

.macro MethodTableLookup

SAVE_REGS MSGSEND

// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
//x2 = cls
mov x2, x16
//x3 = LOOKUP_INITIALIZE|LOOKUP_RESOLVER //是否初始化,imp没有实现尝试resolver
//_lookUpImpOrForward(receiver,selector,cls,LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
mov x3, #3
bl _lookUpImpOrForward

// IMP in x0
mov x17, x0

RESTORE_REGS MSGSEND

.endmacro

  • 3个参数没有什么好说的,behaviorLOOKUP_INITIALIZE | LOOKUP_RESOLVER。那就证明lookUpImpOrForward是有查找模式的。
  • 调用完_lookUpImpOrForward后有mov x17, x0说明是有返回值的,与c/c++lookUpImpOrForward的声明对应上了。

那么就有一个问题了,为什么cache查找要使用汇编?
1.汇编更接近机器语言,执行速度快。为了快速找到方法,优化方法查找时间。
2.消息发送参数是未知参数(比如可变参数),c参数必须明确,汇编相对能够更加动态化。
3.更安全。


二、 慢速查找流程

慢速查找就是不断遍历methodlist的过程,遍历是一个耗时的过程,所以是使用c/c++来实现的。

2.1 lookUpImpOrForward

首先明确慢速查找流程的目标是找到sel对应的imp。所以核心就是lookUpImpOrForward中返回imp的逻辑,精简后源码如下:


NEVER_INLINE
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
//forward_imp赋值
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
//要返回的imp
IMP imp = nil;
//当前查找的cls
Class curClass;

//初始化的一些处理,如果类没有初始化behavior会增加 LOOKUP_NOCACHE,判断是否初始化取的是data()->flags的第29位。
if (slowpath(!cls->isInitialized())) {
behavior |= LOOKUP_NOCACHE;
}

//类是否已经注册,注册后会加入allocatedClasses表中
checkIsKnownClass(cls);
//初始化需要的类。由于要去类中查找方法,如果rw,ro没有准备好那就没有办法查了。也就是为后面的查找代码做好准备。LOOKUP_INITIALIZE用在了这里
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
//赋值要查找的类
curClass = cls;
//死循环,除非return/break
for (unsigned attempts = unreasonableClassCount();;) {//……}

//参数LOOKUP_RESOLVER用在了这里,动态方法决议
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}

done:
//没有初始化LOOKUP_NOCACHE就有值了,也就是查完后不要插入缓存。在这个流程中是插入
if (fastpath((behavior & LOOKUP_NOCACHE) == 0)) {
#if CONFIG_USE_PREOPT_CACHES
//共享缓存
while (cls->cache.isConstantOptimizedCache(/* strict */true)) {
cls = cls->cache.preoptFallbackClass();
}
#endif
//填充缓存,这里填充的是`cls`。也就是父类如果有缓存也会被加进子类。
log_and_fill_cache(cls, imp, sel, inst, curClass);
}
done_unlock:
runtimeLock.unlock();
//forward_imp 并且有 LOOKUP_NIL 的时候直接返回nil。也就是不进行forward_imp
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
  • 先给forward_imp赋值_objc_msgForward_impcache,这个函数的实现是在汇编中。
  • impcurClass定义。
  • cls->isInitialized()类没有初始化则behavior增加LOOKUP_NOCACHE,类有没有初始化时由data()->flags的第29位决定的。
    bool isInitialized() {
//#define uint32_t RW_INITIALIZED (1<<29)
return getMeta()->data()->flags & RW_INITIALIZED;
}

  • checkIsKnownClass判断类是否已经注册,注册后会加入allocatedClasses表中。
  • realizeAndInitializeIfNeeded_locked初始化需要的类,由于要去类中查找方法,如果rw ro没有准备好那就没有办法查了(methods就存在其中)。也就是为后面的查找代码做好准备。汇编中调用的时候传递的behaviorLOOKUP_INITIALIZE用在了这里。它的流程会在后面介绍。
  • 进入for死循环查找imp,核心肯定就是找imp赋值的地方了。那么就只有breakreturngoto才能停止循环,否则一直查找。
  • 如果上面imp没有找到,LOOKUP_RESOLVER是有值的,会进入动态方法决议。
  • 如果找到imp会跳转到done,判断是否需要插入缓存会调用log_and_fill_cache最终调用到cache.insert。父类如果有缓存找到也会加入到子类,这里是因为写入的时候参数是cls
  • 根据LOOKUP_NIL判断是否需要forward,不需要直接返回nil,需要返回imp

2.1.1 behavior 说明

在从汇编调入lookUpImpOrForward的时候传入的behavior参数是LOOKUP_INITIALIZELOOKUP_RESOLVER
behavior类型如下:

/* method lookup */
enum {
LOOKUP_INITIALIZE = 1,
LOOKUP_RESOLVER = 2,
LOOKUP_NIL = 4,
LOOKUP_NOCACHE = 8,
};

根据上面的分析可以得到大致结论:

  • LOOKUP_INITIALIZE: 控制是否去进行类的初始化。有值初始化,没有不初始化。
  • LOOKUP_RESOLVER:是否进行动态方法决议。有值决议,没有值不决议。
  • LOOKUP_NIL:是否进行forward。有值不进行,没有值进行。
  • LOOKUP_NOCACHE:是否插入缓存。有值不插入缓存,没有值插入。

2.2 realizeAndInitializeIfNeeded_locked

在这里主要进行类的实例化和初始化,有两个分支:RealizeInitialize

2.2.1 Realize

(这个分支一般在_read_images的时候就处理好了)
在进行类的实例化的时候调用流程是这样的realizeAndInitializeIfNeeded_locked->realizeClassMaybeSwiftAndLeaveLocked->realizeClassMaybeSwiftMaybeRelock->realizeClassWithoutSwift,最终会调用realizeClassWithoutSwiftswift会调用realizeSwiftClass。这个不是这篇文章的重点,分析下主要代码如下:


static Class realizeClassWithoutSwift(Class cls, Class previously)
{
class_rw_t *rw;
Class supercls;
Class metacls;
auto ro = (const class_ro_t *)cls->data();
auto isMeta = ro->flags & RO_META;
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro();
ASSERT(!isMeta);
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
rw = objc::zalloc<class_rw_t>();
rw->set_ro(ro);
rw->flags = RW_REALIZED|RW_REALIZING|isMeta;
cls->setData(rw);
}
//赋类和元类的操作
supercls = realizeClassWithoutSwift(remapClass(cls->getSuperclass()), nil);
metacls = realizeClassWithoutSwift(remapClass(cls->ISA()), nil);

//关联类
cls->setSuperclass(supercls);
cls->initClassIsa(metacls);
return cls;
}
  • 对类的ro以及rw进行处理。
  • 循环调用了父类和元类的realizeClassWithoutSwift
  • 关联了父类和元类。

当对象调用方法的时候判断类是否初始化,如果初始化了再判断类的父类以及元类,相当于是递归操作了,一直到NSObject->nil为止。也就是说只要有一个类进行初始化它的上层(也就是父类和元类)都会进行初始化,是一个连锁反应。

⚠️为什么这么操作?
就是为了查找方法。类没有实例方法的话会找父类,类没有类方法会找元类,所以需要这么操作。

2.2.2 Initialized

realizeAndInitializeIfNeeded_locked->initializeAndLeaveLocked->initializeAndMaybeRelock->initializeNonMetaClass。在initializeNonMetaClass中调用了callInitialize(cls)


void callInitialize(Class cls)
{
((void(*)(Class, SEL))objc_msgSend)(cls, @selector(initialize));
asm("");
}


系统直接objc_msgSend发送了initialize消息。所以initialize是在类第一个方法被调用的时候进行调用的。也就是发送第一个消息的时候:
消息慢速查找开始前进行类初始化的时候发送的initialize消息

三、循环查找

对于慢速查找流程,我们想到的就是先查自己然后再查父类一直找到NSObject->nil
慢速查找流程应该是这样:
1.查自己methodlist->(sel,imp)。
2.查父类->NSObject->nil ->跳出来

查看源码:

//死循环,除非return/break
for (unsigned attempts = unreasonableClassCount();;) {
//先去共享缓存查找,防止这个时候共享缓存中已经写入了该方法。
if (curClass->cache.isConstantOptimizedCache(/* strict */true)) {
#if CONFIG_USE_PREOPT_CACHES
//这里也是调用到了`_cache_getImp`汇编代码,最终调用了`CacheLookup`查找共享缓存。
imp = cache_getImp(curClass, sel);
//找到后直接跳转done_unlock
if (imp) goto done_unlock;
curClass = curClass->cache.preoptFallbackClass();
#endif
} else {
// curClass method list.进行循环查找
Method meth = getMethodNoSuper_nolock(curClass, sel);
//找到method
if (meth) {
//返回imp
imp = meth->imp(false);
//跳转done
goto done;
}
//这里curClass 会赋值,直到找到 NSObject->nil就会返回forward_imp
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
imp = forward_imp;
break;
}
}

// Superclass cache.
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {

break;
}
if (fastpath(imp)) {
goto done;
}
}
  • 可以看到这是一个死循环。
  • 如果有共享缓存,先查找共享缓存,因为前面做了很多准备工作,防止这个时候共享缓存中已经写入了该方法(在汇编中已经查过一次了)。
  • 否则就进行二分查找流程,核心逻辑是在getMethodNoSuper_nolock中调用的,查找完成返回。
  • 如果找到method则获取imp跳转done,如果没有找到将父类赋值给curClass,父类不存在则imp = forward_imp;
    • 找到则进入找到imp done的逻辑。
      • log_and_fill_cache插入缓存,也就是调用cls->cache.insert与分析cache的时候逻辑对上了。
      • 返回imp
    • 没有找到则curClass赋值superclass,没有superclass也就是找到了NSObject->nil的情况下imp = forward_imp
    • 没有找到并且有父类的情况下通过cache_getImp去父类的cache中查找。这里与共享缓存的cache_getImp是一个逻辑,最终都是调用汇编_cache_getImp->CacheLookup

    父类也有快速和慢速查找。

  • 如果父类中也没有找到,则进入递归。直到imp找到或者变为forward_imp才结束循环。

_cache_getImp 说明
源码:

    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实现是mov p0, #0 ret也就是找不到就返回了。不会去走__objc_msgSend_uncached逻辑。

⚠️ 找到父类缓存会插入自己的缓存

3.1 二分查找流程

3.1.1 getMethodNoSuper_nolock

首先进入的是getMethodNoSuper_nolock,实现如下:


static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
//获取methods
auto const methods = cls->data()->methods();
//循环,这个时候找的是methodlist存的是method_list_t,有可能是二维数据。动态加载方法和类导致的
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}



  • 这里只是普通的循环,因为methods获取的数据类型是method_array_t,它存储的是method_list_t。这里的数据结构有可能是二维数据,因为动态加载方法和类导致。
  • 核心逻辑是调用search_method_list_inline实现的


0 个评论

要回复文章请先登录注册