本章内容
- 本章的目的是什么
- lookUpImpOrForward的源码
- 消息的慢速查找流程
- 消息的动态协议,实例方法的动态协议,类方法的动态协议
本章的目的
在消息的汇编流程中,也就是消息缓存查找流程中。如果说消息并没有在缓存中命中需要查找的方法就会走 _objc_msgSend_uncached
流程
大致流程是:_objc_msgSend_uncached(汇编)
-> MethodTableLookup(汇编)
-> lookUpImpOrForward(C/C++)
-> TailCallFunctionPointer(汇编)
-> 执行方法
目的:我们看这个方法的目的就是理清楚消息的查找机制中,如果说缓存没有查找到,那么他慢速的查找流程是如何的。而这个方法也包含了消息的动态协议流程(苹果在正常查找流程中给我们一次修复方法的机会)。但是如果这个流程也没找到呢?
lookUpImpOrForward
大致流程就是:
1.前期准备(如果要查找的类没有被初始化去初始化也就是注册类以及他的元类,父类和父类的元类)
2.死循环遍历类以及其父类所有的方法列表(如果没被找到就走3,找到就走4)
3.方法遍历完没被找到就给一次动态协议修复机会(找到就走返回imp,没找到就返回nil)
4.插入到消息接收者自己的类缓存里面(3流程不走4)
源码
贴出源码,尽可能备注,可以不看。苹果备注太长所以删掉
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
//我们在消息未实现时候经常报一个错就是下面这个forward_imp的原因,就是提醒未找到方法的错误
//等于说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();
//看看cls是不是已经知道的类。防止程序员去做一个二进制blob,看起来像一个类但不是真正的类。
//做了CFI攻击,希望通过合法注册。苹果的防护机制,不重要
checkIsKnownClass(cls);
//1.该方法就是 类没实现去实现,没初始化去初始化,并且包含它所有的父类,父类元类。
cls = realizeAndInitializeIfNeeded_locked(inst, cls, behavior & LOOKUP_INITIALIZE);
// runtimeLock may have been dropped but is now locked again
runtimeLock.assertLocked();
curClass = cls;
//2.死循环查找其类的方法,父类的方法列表。如果说有序的话会用二分查找算法
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.
// 方法查找
Method meth = getMethodNoSuper_nolock(curClass, sel);
//如果方法被找到走 done流程
if (meth) {
imp = meth->imp(false);
goto done;
}
//当方法没查到,就看看有父类没,如果没有,imp为报错imp。跳出循环
//这里的curClss是cls的父类了(这么说其实也不准确。因为curClss是它本身的父类了)
if (slowpath((curClass = curClass->getSuperclass()) == nil)) {
// No implementation found, and method resolver didn't help.
// Use forwarding.
imp = forward_imp;
break;
}
}
//如果父类查找流程有循环,毕竟万物NSObject。就报错停止。不重要
// Halt if there is a cycle in the superclass chain.
if (slowpath(--attempts == 0)) {
_objc_fatal("Memory corruption in class list.");
}
// 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;
}
// 找到的话走done,将方法插入自己类缓存
if (fastpath(imp)) {
// Found the method in a superclass. Cache it in this class.
goto done;
}
}
// No implementation found. Try method resolver once.
// 3.如果说上面循环结束也没找到,走一次消息动态协议 。
// behavior为3,这是从汇编传过来的值 ,LOOKUP_RESOLVER为2
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
// 4.done流程,将方法插入缓存,如果共享缓存不重要,
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;
}
复制代码
消息的慢速查找流程
我们通过lookUpImpOrForward那一段差不多已经知道流程了。但是无非就是有一个getMethodNoSuper_nolock
方法里面执行不太知道而已,其实这个也不是很重要。我们知道流程已经足以应付大部分面试了。感兴趣可以看
源码 getMethodNoSuper_nolock
源码很简单,无非就是一个遍历类的方法列表而已。而且还很短,但是我们需要注意的是methods
是一个二维数组。至于为什么要这么设计,我也不晓得
static method_t * getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
auto const methods = cls->data()->methods();
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
// <rdar://problem/46904873> 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;
}
复制代码
源码 search_method_list_inline
这就是上面的方法进来去遍历的。看看上面获得的方法列表是无序的还是有序的。有序的就去二分查找,无序的话就只能线性搜索,可以看他的备注。(方法的获取也有点特殊,他分M1和咱们正常的系统。M1的话就是在smallList里面)
ALWAYS_INLINE static method_t *
search_method_list_inline(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->isExpectedSize();
if (fastpath(methodListIsFixedUp && methodListHasExpectedSize)) {
return findMethodInSortedMethodList(sel, mlist);
} else {
// Linear search of unsorted method list
if (auto *m = findMethodInUnsortedMethodList(sel, mlist))
return m;
}
#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;
}
复制代码
消息的动态协议
我们如果说在以上流程都没有找到方法怎么办。苹果给了一个修复的机会就是消息动态协议(看上面lookUpImpOrForward源码的resolveMethod_locked)。但是消息的快慢转发呢?其实消息的快慢转发是在动态协议之后的。
大致流程:动态协议未找到 -> 消息快速转发 未找到 -> 消息慢速转发 未找到 -> 报错
消息动态协议最重要的两个方法分别为,实例对象的动态协议resolveInstanceMethod
,和类方法的动态协议resolveClassMethod
源码 resolveMethod_locked
苹果会走这个方法去判断,消息的接收者的类是类还是元类,因为我们知道,对象方法是在类的中,类方法是在元类中存储,具体存储看这里类和元类本质文章中的补充了解。本文章不展示其具体resolveInstanceMethod
和resolveClassMethod
可以自己查看,里面就是看看类有没有实现这两个方法,实现的话就通过objc_msgSend去调起这个方法
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
// 如果cls 不是元类,就走实例动态协议方法
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {
// 走类动态协议方法
resolveClassMethod(inst, sel, cls);
//这里为什么会再次执行resolveInstanceMethod?
//因为根元类是继承NSObject的(NSProxy除外),浪费性能圆类方法和实例方法的谎
if (!lookUpImpOrNilTryCache(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
// 再次去尝试查找一遍,看看缓存中是不是又已经处理了
return lookUpImpOrForwardTryCache(inst, sel, cls, behavior);
}
复制代码
示例与问题
描述:我创建一个类Person,没有去实现任何方法,创建对象p,让p去执行一个不存在的方法,毫无疑问报错。在Person类中实现实例动态协议方法,然后在动态协议方法中进行打印。
问题1:resolveInstanceMethod为什么走了两遍?
1.正常查找,2系统在经过消息转发后仍未找到就回调回来再找一遍。
(本回答不详细,实质是底层在第一遍动态协议,快慢转发都走完后没找到,会经过回调流程___forwarding___(CF库的函数)进行回调。然后在libsystem库和CF之间有一个叫_forwardStackInvocation函数进行了操作回调。具体流程需要自己先反汇编CoreFoundation库,再找到最终调用结果后可以回项目断点慢速转发方法后汇编查看)
第一遍是正常查找流程,如果说Person类里面实现了动态协议方法后就走,并且去调用。
那么第二遍呢?这时候我们需要断点然后借助方法调用栈去查看,如下图
问题2:报错信息unrecognized selector sent to instance 0x100552570…是如何来的?
看lookUpImpOrForward方法中会给forward_imp一个默认值_objc_msgForward_impcache
,它其实是汇编函数。流程是:_objc_msgForward_impcache
-> __objc_msgForward 获取报错地址
-> _objc_forward_handler C/C++函数,其实为objc_defaultForwardHandler
-> 报错。如果说有人问起,就直接说没找到方法的话底层会给一个默认imp的报错方法指向
问题3:类动态协议为什么又要去resolveInstanceMethod中查找
看源码备注
补充
消息的动态协议,以及消息快慢转发,优点都是无侵入,对业务代码没什么损害。
其实消息的动态协议有很多致命问题,如:会记录系统很多方法(不是我们需要修复的),而且代码冗余。所以如果做AOP(面相切面编程)一定不要在此机制中进行消息转发。