在iOS底层之objc_msgSend快速查找流程里分析里调用方法的本质,就是消息发送,查找类的方法缓存,那么如果经历CacheLookup
后没找到缓存,即快速查找流程找不到,则会开始慢速查找,从methodList
查找,这一篇文章我们来分析方法的查找流程。
在CacheLookup
快速查找流程中,当没有找到方法imp
缓存,无论是走到CheckMiss
还是JumpMiss
,最终都会走到__objc_msgSend_uncached
汇编函数。其定义是:
STATIC_ENTRY __objc_msgSend_uncached
UNWIND __objc_msgSend_uncached, FrameWithNoSaves
// THIS IS NOT A CALLABLE C FUNCTION
// Out-of-band r10 is the searched class
// r10 is already the class to search
MethodTableLookup NORMAL // r11 = IMP
jmp *%r11 // goto *imp
END_ENTRY __objc_msgSend_uncached
可以看到关键代码MethodTableLookup
,会查找方法表。
其定义是:
.macro MethodTableLookup
// push frame
SignLR
stp fp, lr, [sp, #-16]!
mov fp, sp
// save parameter registers: x0..x8, q0..q7
sub sp, sp, #(10*8 + 8*16)
stp q0, q1, [sp, #(0*16)]
stp q2, q3, [sp, #(2*16)]
stp q4, q5, [sp, #(4*16)]
stp q6, q7, [sp, #(6*16)]
stp x0, x1, [sp, #(8*16+0*8)]
stp x2, x3, [sp, #(8*16+2*8)]
stp x4, x5, [sp, #(8*16+4*8)]
stp x6, x7, [sp, #(8*16+6*8)]
str x8, [sp, #(8*16+8*8)]
// lookUpImpOrForward(obj, sel, cls, LOOKUP_INITIALIZE | LOOKUP_RESOLVER)
// receiver and selector already in x0 and x1
mov x2, x16
mov x3, #3
bl _lookUpImpOrForward
// IMP in x0
mov x17, x0
// restore registers and return
ldp q0, q1, [sp, #(0*16)]
ldp q2, q3, [sp, #(2*16)]
ldp q4, q5, [sp, #(4*16)]
ldp q6, q7, [sp, #(6*16)]
ldp x0, x1, [sp, #(8*16+0*8)]
ldp x2, x3, [sp, #(8*16+2*8)]
ldp x4, x5, [sp, #(8*16+4*8)]
ldp x6, x7, [sp, #(8*16+6*8)]
ldr x8, [sp, #(8*16+8*8)]
mov sp, fp
ldp fp, lr, [sp], #16
AuthenticateLR
.endmacro
其中关键代码是_lookUpImpOrForward
。为什么?
通过调用一个实例方法[person sayHello]
,并打断点。打开汇编调试查看:
可以看到汇编停在了
[person sayHello]
调用之前,下一步开始发送消息调用sayHello
。打断点objc_msgSend
,按住control+点击step into
进入内部调用。
_objc_msgSend_uncached
之上完成了类信息获取,方法快速查找流程,没找到缓存时来到了_objc_msgSend_uncached
,断点跳到这一句,继续step into
进入内部。
可以看到来到了lookUpImpOrForward函数,显示这个函数在
objc-runtime-new.mm
文件的6099
行。
也就验证了上面我们说的
_objc_msgSend_uncached
之后会来到_lookUpImpOrForward
。
通过汇编源码查找
C/C++
源码的技巧
例如_lookUpImpOrForward
,
汇编中查找C/C++
方法时,需要将汇编调用的方法_lookUpImpOrForward
去掉一个下划线
搜索找到lookUpImpOrForward
的定义,主要的步骤分析我都写在注释里了。
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
//如果消息转发不成功,则打印方法未识别的错误
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
//可以防止多线程操作时,调用函数时,此时另一线程缓存进来了,可以查快速查找一遍缓存,因为下面的慢速查找很耗时
//当从MethodTableLookup过来时,behavior=3,3&4=0为假,往下执行
//1.动态方法决议回来第一次时'sel=resolveInstanceMethod',此时behavior=12 12&4=4为真,而获取imp缓存为空,往下执行)
//2. 动态方法决议回来第二次时'sel=say666',此时behavior=12 12&4=4为真,imp为空,往下执行
//3.动态方法决议处理后,从resolveMethod_locked的lookUpImpOrForward重新进来,此时behavior=5,5&4=4为真,此时找到的imp为动态方法决议时缓存的_objc_msgForward_impcache,跳转done_nolock(不缓存)
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
//在isRealized和isInitialized检查期间保持锁,防止对并发实现的竞争
//runtimeLock在方法搜索过程中被保持
//方法查找+缓存填充相对于方法添加而言是原子的
//保证方法查找过程中method-lookup + cache-fill中方法添加的原子性。
// Otherwise, a category could be added but ignored indefinitely because
// the cache was re-filled with the old value after the cache flush on
// behalf of the category.
runtimeLock.lock();
// TODO: this check is quite costly during process startup.
//检查是否是已知类(已经加载的类)
//确保这个类是通过objc_duplicateClass, objc_initializeClassPair或objc_allocateClassPair合法注册的,或者内置到二进制文件中的,而不是创建一个看起来像类而实际并不是的二进制的blob,做CFI攻击
checkIsKnownClass(cls);
//小概率情况下,当前类未实现时
//如果没有,需要先实现,目的是为了能确定父类继承链和元类继承链,后面查找递归,当前类没找到,则需要从继承链查找
//内部有双向链表会更新子类和父类
//还会把类的相关属性协议方法贴到rw.ext()中
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}
//判断如果类没有初始化,则会递归父类继承链执行初始化所有的类
//没实现时会callInitialize对类发送消息调用initialize方法
//所以侧面说明了initialize和load方法一样,不需要主动调用,底层实现时帮你调用
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
// runtimeLock may have been dropped but is now locked again
// If sel == initialize, class_initialize will send +initialize and
// then the messenger will send +initialize again after this
// procedure finishes. Of course, if this is not being called
// from the messenger then it won't happen. 2778172
}
runtimeLock.assertLocked();
curClass = cls;
// The code used to lookpu the class's cache again right after
// we take the lock but for the vast majority of the cases
// evidence shows this is a miss most of the time, hence a time loss.
//
// The only codepath calling into this without having performed some
// kind of cache lookup is class_getInstanceMethod().
//重点!!!!从这里对imp赋值
for (unsigned attempts = unreasonableClassCount();;) {
// curClass method list.
//查找curClass方法列表,如果有,就不需要去找curClass的父类了
Method meth = getMethodNoSuper_nolock(curClass, sel);
//如果找到了,则跳转到done(缓存方法、解锁)
if (meth) {
imp = meth->imp;
goto done;
}
//本类没找到,则将curClass赋值为当前类的父类
//如果父类是nil,也就是继承链走完了,那么会imp = forward_imp走消息转发,跳出循环
if (slowpath((curClass = curClass->superclass) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
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.
//查找当前类的缓存,注意,上面已经将curClass父类赋值给当前类,所以是查找父类的缓存
//并不是递归了(cache_getImp - lookup - lookUpImpOrForward)
imp = cache_getImp(curClass, sel);
//上面当父类链的父类为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(缓存方法、解锁),缓存在curClass
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
//当前父类没有找到,由于for循环没有跳出的条件判断,会一直死循环查找父类继承链,直到break
}
// No implementation found. Try method resolver once.
//这一步代表了流程只走一次下面这段代码(也就是只会走一次动态方法决议),因为 behavior ^= LOOKUP_RESOLVER改变了behavior,与条件behavior & LOOKUP_RESOLVER = 1不能同时满足
//behavior & LOOKUP_RESOLVER 3&2=2,执行条件语句代码块
//1.从CachLookup后进来behavior 第一次:3,为真
//2.从动态方法决议回来第一次:12,此时sel为resolveInstanceMethod,为假
//3.从动态方法决议回来第一次:12,此时sel为say666,为假
if (slowpath(behavior & LOOKUP_RESOLVER)) {
//behavior = 3^2 = 1
behavior ^= LOOKUP_RESOLVER;
//没有找到imp,则开始走本类的动态方法决议
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
//缓存方法
//1.当动态方法决议第一次进来,从父类方法列表中获取到resolveInstanceMethod的imp,sel='resolveInstanceMethod',缓存imp'+[NSObject resolveInstanceMethod:]'
//2.当动态方法决议第二次进来,此时sel='say666',imp为_objc_msgForward_impcache,缓存sel和imp
log_and_fill_cache(cls, imp, sel, inst, curClass);
//解锁
runtimeLock.unlock();
done_nolock:
//1.第一次从动态方法决议进来,此时sel = "resolveInstanceMethod:",behavior=12 12&8=8为真,imp为`+[NSObject resolveInstanceMethod:]` imp==forward_imp为假,返回imp,也就是objc_msgForward_impcache
//2.第二次从动态方法决议进来,此时sel = 'say666',此时behavior=12 12&8=8为真,imp为`_objc_msgForward_impcache` imp==forward_imp为真,返回nil
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
//当动态方法决议整个流程没处理时,resolveMethod_locked语句返回的imp(也就是最初始lookUpImpOrForward的返回值)是_objc_msgForward_impcache,回到MethodTableLookup,把imp的值给x17,接着TailCallFunctionPointer x17,也就是调用默认的转发处理_objc_msgForward_impcache这个方法,终止进程。但是在此之前会先进行消息转发和方法签名处理。
return imp;
}
主要步骤是:
【第一步】由于多线程可能导致方法缓存在另外线程添加,为了避免下面慢速查找继承链方法列表需要消耗大量时间,所以再次从
cache
缓存中进行查找,即快速查找
,找到则直接返回imp
,否则,则进入【第二步】-
【第二步】判断
cls
是不是已知类如果不是,则
报错
类是否已实现,如果没有,则需要先实现,确定其父类链或元类继承链,此时实例化的目的是为了确定父类链、
ro
、以及rw
等,方法后续数据的读取以及查找需要是否已初始化
initialize
,如果没有,则初始化
-
【第三步】
for循环
,按照类继承链 或者 元类继承链
的顺序查找当前类的方法列表中使用
二分查找算法
查找方法,如果找到,则将方法写入cache
(在iOS底层之cache_t探究中分析了写入过程),并返回imp
,如果没有找到,则返回nil
当前
cls
被赋值为父类,如果父类等于nil
,则imp = forward_imp
,并跳出循环,进入【第四步】如果父类链中存在循环,则报错,终止循环
-
从父类缓存中查找方法
如果未找到,进入下一次循环,
Method meth = getMethodNoSuper_nolock(curClass, sel);
,curClass变为其父类,查找父类方法列表如果找到,则直接返回
imp
,执行cache
写入流程
-
【第四步】判断是否执行过
动态方法决议
- 如果没有,执行
动态方法决议
- 如果没有,执行
其中部分函数的源码
- 二分法查找方法列表的imp
-
getMethodNoSuper_nolock
查找当前类的方法列表
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
//查找data里的methods() 方法列表
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
// getMethodNoSuper_nolock is the hottest
// caller of search_method_list, inlining it turns
// getMethodNoSuper_nolock into a frame-less function and eliminates
// any store from this codepath.
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
for (auto& meth : *mlist) {
if (meth.name == sel) return &meth;
}
}
#if DEBUG
// sanity-check negative results
if (mlist->isFixedUp()) {
for (auto& meth : *mlist) {
if (meth.name == sel) {
_objc_fatal("linear search worked when binary search did not");
}
}
}
#endif
return nil;
}
- 重点!!上面的函数,重点在于
findMethodInSortedMethodList(sel, mlist);
以二分法查找
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
//list为data()中的方法列表
ASSERT(list);
const method_t * const first = &list->first;
//取base为第一个元素
const method_t *base = first;
const method_t *probe;
//key为传进来的cmd,也就是我们调用的sayHello
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
//count赋值为列表元素个数,每一次循环都将count/2
for (count = list->count; count != 0; count >>= 1) {
//base为二分法区间的最小元素。将base平移count/2,也就是平移到列表中间的位置,probe为中间的元素
probe = base + (count >> 1);
//获取probe存储的名字,也就是sel方法名
uintptr_t probeValue = (uintptr_t)probe->name;
//如果查找的cmd等于sel
if (keyValue == probeValue) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
//由于分类的重写的方法加载到内存中是会插在类的同名方法前面,所以这里需要循环向前一个查找sel,如果有,则返回分类的sel。
//如果存在多个分类重写,则看哪个分类先加载
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
//如果cmd大于sel,则需要往后面查找,赋值最小值base为中间值probe+1,如果cmd小于sel,则不走进来
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
//没找到一致的sel,则进入下一次循环,再取base和count的中间值
}
//如果循环到count==0,也没找到,则返回nil
return nil;
}
其执行流程图如下
- 查找父类
imp
源码:
STATIC_ENTRY _cache_getImp
GetClassFromIsa_p16 p0
CacheLookup GETIMP, _cache_getImp
LGetImpMiss:
mov p0, #0
ret
END_ENTRY _cache_getImp
其流程图:
- 如果父类链没找到
imp
,则进入动态方法决议resolveMethod_locked
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
//lookUpImpOrForward没找到imp,再给你一次机会,去处理
runtimeLock.unlock();
//如果不是元类
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
//直接走当前类的动态方法决议
resolveInstanceMethod(inst, sel, cls);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
//元类,走元类的动态方法决议
resolveClassMethod(inst, sel, cls);
//再查找一次methodList,如果还是nil
if (!lookUpImpOrNil(inst, sel, cls)) {
//走当前类的动态方法决议
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
//上面给你机会去处理,现在重新找一次lookUpImpOrForward
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
先调用了resolveInstanceMethod
去看看resolver
中帮没帮实现,如果帮实现了,在走lookUpImpOrNil
过程中就会把方法缓存起来。然后会回到resolveMethod_locked
方法中调用lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE)
,如果找到就直接进入done_nolock
, 返回方法的imp
;如果没有找到,就会返回存着forward_imp
的imp
。然后就又会回到MethodTableLookup
,把imp
的值给x17
,接着TailCallFunctionPointer x17
,调用forward_imp
也就是_objc_msgForward_impcache
这个方法。
- 下面来看看
_objc_msgForward_impcache
做了什么__objc_msgForward_impcache
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
//进入__objc_msgForward
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
//把存有__objc_forward_handler的页存入X17
adrp x17, __objc_forward_handler@PAGE
//把x17向后偏移了__objc_forward_handler@PAGEOFF,然后把X17中地址的值存入p17,
ldr p17, [x17, __objc_forward_handler@PAGEOFF]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
TailCallFunctionPointer
是个方法指针,主要还是看上面__objc_forward_handler
做了什么事
.macro TailCallFunctionPointer
// $0 = function pointer value
br $0
.endmacro
由于查找__objc_forward_handler
未找到汇编定义,去掉一个_
查找C/C++
源码:
// Default forward handler halts the process.
__attribute__((noreturn, cold)) void
objc_defaultForwardHandler(id self, SEL sel)
{
_objc_fatal("%c[%s %s]: unrecognized selector sent to instance %p "
"(no message forward handler is installed)",
class_isMetaClass(object_getClass(self)) ? '+' : '-',
object_getClassName(self), sel_getName(sel), self);
}
void *_objc_forward_handler = (void*)objc_defaultForwardHandler;
可以看到是由objc_defaultForwardHandler
定义的。而这个函数里面,我们看到了熟悉的方法选择器未找到的打印信息。从打印信息的组成class_isMetaClass(object_getClass(self)) ? '+' : '-'
,可以看到,系统内部并没有区分方法是实例方法还是类方法,而是通过是否是元类,来区分打印方法类型。
其流程图:
然而在报错之前,还会对消息进行转发和方法签名处理,再次挽救。加上动态方法决议,共有3
次挽救机会。
这个流程留待下一节分析。
总结
查找实例方法
,会在类
中查找,慢速查找的继承链是:类->父类->根类->nil
同理,查找类方法
,则在元类
中查找,其慢速查找的链是:元类->根元类->根类->nil
如果objc_msgSend
快速查找缓存,慢速查找当前类方法列表、父类的缓存和方法列表都没有找到imp
方法实现,则尝试动态方法决议
如果动态方法决议
仍然没有找到,则进行消息转发
如果消息转发
还没有处理,则会报错。