前文提要
iOS 底层探索文章系列
经过上一篇 objc_msgSend
快速流程分析,通过汇编查询 cache
,如果 缓存命中 就直接进行发送消息。但是如果没有命中缓存接下来就需要走慢速流程了,也就是来到上一篇结尾处所说的 lookUpImpOrForward
函数里面,接下来本文将对慢速流程进行探索。
1、objc_msgSend
慢速查找流程验证
我们在上一篇文章里面可以看到,在快速查找流程中,如果没有找到方法实现,最终会来到 _objc_msgSend_uncached
函数中,那么我们怎么在工程中验证呢?我们可以通过汇编调试来进行验证。
- 我们在
main
函数中,在调用[student sayBad]
方法处下一个断点,然后开启汇编调试【Debug -> Debug workflow -> Always Show Disassembly】
- 然后我们在汇编调用
objc_msgSend
处下一个断点,执行到此处之后,我们按住control + stepinto
,进入到objc_msgSend
的汇编部分。
- 然后我们再在调用
_objc_msgSend_uncached
函数处下一个断点,继续stepinto
。
- 最终我们可以看到它会走到
lookUpImpOrForward
函数,所以从这里我们也可以看出,他在汇编快速查找流程没有找到方法实现的时候,会来到慢速查找流程lookUpImpOrForward
处。
2、慢速查找方法流程分析
因为 lookUpImpOrForward
函数是支持多线程的,所以内部有很多锁操作,然后通过 runtimeLock
控制读写锁。其内部有很多逻辑代码。
通过类对象的 isRealized
函数,判断当前类是是否被实现,如果没有被实现,则通过 realizeClassMaybeSwiftAndLeaveLocked
函数实现该类。在 realizeClassMaybeSwiftAndLeaveLocked
函数中,会设置 rw
、ro
、supercls
、metacls
等一些信息。
2.1 lookUpImpOrForward
分析
/***********************************************************************
* 标准 IMP 查找
* initialize != LOOKUP_INITIALIZE 时尝试避免+初始化(但有时会失败)
* cache != LOOKUP_CACHE 时跳过乐观解锁查找(但在其他地方使用缓存)
* 大多数调用者应该使用 initialize == LOOKUP_INITIALIZE 和 cache == LOOKUP_CACHE。
* inst 是 cls 或其子类的一个实例,如果不知道,则为nil。
* 如果 cls 是一个未初始化的元类,那么非空的 inst 会更快。
* 可能返回 _objc_msgForward_impcache。用于外部使用的 imp 必须转换为 _objc_msgForward 或 _objc_msgForward_stret。
* 如果根本不想转发,可以使用lookUpImpOrNil()。
**********************************************************************/
IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
{
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
// 乐观的缓存查找,如果条件满足,则从缓存中查找 IMP。
if (fastpath(behavior & LOOKUP_CACHE)) {
// 通过 `cache_getImp` 函数查找 IMP,查找到则返回 IMP 并结束调用,其实这个函数又会执行到汇编里面去查找缓存。
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
// runtimeLock 在 isRealized 和 isInitialized 检查过程中被持有,以防止对多线程并发实现的竞争。
// runtimeLock 在方法搜索过程中保持,使方法查找+缓存填充原子相对于方法添加。
// 否则,可以添加一个类别,但是无限期地忽略它,因为在代表类别的缓存刷新之后,缓存会用旧值重新填充。
// 上方的说明就是对这里加锁的解释
runtimeLock.lock();
// 如果运行时知道这个类(位于共享缓存中,加载的图像的数据段中,或者已经用 objc_duplicateClass、objc_initializeClassPair、obj_allocateClassPair 分配了),则返回true,如果没有就崩溃了。
// 在流程启动期间,此方法的检查的成本很高。
checkIsKnownClass(cls);
// 判断类是否已经被创建,如果没有被创建,则将类实例化
// 锁定:为了防止并发实现,持有runtimeLock。
if (slowpath(!cls->isRealized())) {
// 对类进行实例化操作
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
// 第一次调用当前类的话,执行 initialize 的代码
if (slowpath((behavior & LOOKUP_INITIALIZE) && !cls->isInitialized())) {
// 对类进行初始化,并开辟内存空间
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
runtimeLock.assertLocked();
curClass = cls;
// 在该对象的所属的类的方法列表中查找,这一步会进入死循环
for (unsigned attempts = unreasonableClassCount();;) {
// 从方法列表中获取 Method,使用二分查找法
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
// 如果找到了就跳转到 done
imp = meth->imp;
goto done;
}
// 如果查找 NSObject 的父类,也就是 nil,还没有查到相应的 imp,那就设置 imp 为 forward_imp
if (slowpath((curClass = curClass->superclass) == nil)) {
imp = forward_imp;
break;
}
// 获取父类的 IMP,跳转到汇编`CacheLookup GETIMP`,没有找到的话,继续死循环,获取父类的父类,一直到 NSObject
imp = cache_getImp(curClass, sel);
if (slowpath(imp == forward_imp)) {
break;
}
if (fastpath(imp)) {
goto done;
}
}
// 如果都没有找到,则尝试动态方法决议,
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
// 查找到了对应的 Method,那么就填充到缓存
log_and_fill_cache(cls, imp, sel, inst, curClass);
runtimeLock.unlock();
done_nolock:
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
return imp;
}
2.2 getMethodNoSuper_nolock
二分查找法的分析
在方法不是第一次调用时,可以通过 cache_getImp
函数查找到缓存的 IMP
。但如果是第一次调用,就查找不到缓存的 IMP
,那么就会进入到 getMethodNoSuper_nolock
函数中执行。下面是 getMethodNoSuper_nolock
函数的实现代码。
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
auto const methods = cls->data()->methods();
// 二分查找
// 在 objc_object 的 class_rw_t *data() 的 methods 。
// beginLists : 第一个方法的指针地址。
// endLists : 最后一个方法的指针地址。
// 每次遍历后向后移动一位地址。
for (auto mlists = methods.beginLists(),
end = methods.endLists();
mlists != end;
++mlists)
{
// 对 `sel` 参数和 `method_t` 做匹配,如果匹配上则返回。
method_t *m = search_method_list_inline(*mlists, sel);
if (m) return m;
}
return nil;
}
2.3 search_method_list
分析
当调用一个对象的方法时,查找对象的方法,本质上就是遍历对象 isa
所指向类的方法列表,并用调用方法的 SEL
和遍历的 method_t
结构体的 name
字段做对比,如果相等则将 IMP
函数指针返回。
// 根据传入的 SEL,查找对应的 method_t 结构体
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 {
for (auto& meth : *mlist) {
// SEL 本质上就是字符串,查找的过程就是进行字符串对比
if (meth.name == sel) return &meth;
}
}
return nil;
}
2.4 findMethodInSortedMethodList
分析
二分查找关键点和注意点:
- 排序方法
fixupMethodList
中使用std::stable_sort
进行文档排序,确保分类的method
在前。 - 二分查找找到
SEL
相同的method
之后,会继续向前查找是否还有SEL
相同的method
,找到之后,那个才是最终要找的method
。这样就确保了分类的method
被优先调用。
findMethodInSortedMethodList
执行逻辑
- count: 假设初始值为方法列表的个数为 48
- 如果 count != 0; 循环条件每次右移一位,也就是说除以 2;
- 第一次进入从一半 24 开始找起,如果 keyValue > probeValue 那么在右边,否则在左边;
- 第二次是从 12 开始找起,也不满足 keyValue > probeValue 的条件;
- 第三次从 6 开始找起,满足条件 keyValue > probeValue,将初始值移动到当前 6 的后一位,也就是从 7 开始查找,然后 count—,可以看到当前 count = 5 ,然后在对 > 6 且 < 12 进行查找,也就是 7 - 11 ,count >> 1 为 2, 7+2 = 9,刚好是 7 - 11 的中心。
- 这就是 二分查找法,但是前提必须是有序数组。
如果还是对于二分查找不太理解的同学,可以参考一下这篇文章 二分查找
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
const method_t * const first = &list->first;
const method_t *base = first;
const method_t *probe;
uintptr_t keyValue = (uintptr_t)key;
uint32_t count;
for (count = list->count; count != 0; count >>= 1) {
// 刚开始时从一半的位置开始查找
probe = base + (count >> 1);
uintptr_t probeValue = (uintptr_t)probe->name;
if (keyValue == probeValue) {
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
2.5 找不到实现方法, Xcode
崩溃
如果没有实现 动态方法决议和消息转发 就进入 _objc_msgForward_impcache
汇编了。
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
上述代码发现调用了 _objc_forward_handler
函数,继续搜索,得到如下结果:
__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;
看到这里大家应该都明白了,这不就是我们经常在 Xcode
控制台看到的找不到方法的崩溃信息吗?所以也证明了一件事,在底层是 没有对象方法和类方法之分 的。
3、慢速查找方法流程图
4、总结
4.1 消息调用总结
- 消息的查找有快速流程通过
objc_msgSend
通过cache
查找、慢速流程lookUpImpOrForward
进行查找。 - 从快速查找流程进入慢速查找流程一开始是不会进行
cache
查找的,而是直接从方法列表中进行查找。 - 从方法的缓存列表中查找,通过
cache_getImp
函数进行查找,如果找打缓存则直接返回IMP
。 - 首先会查找当前类的
method list
,查找是否有对应的SEL
,如果有则获取到Method
对象,并从Method
对象中获取IMP
,并返回IMP
(这一步查找的结果是Method
对象)。 - 如果在当前类没有找到
SEL
,则进行死循环去父类的缓存列表和方法列表中查找。 - 如果在类的继承体系中,一直都没有查找到对应的
SEL
,则进去动态方法决议。可以在+ resolveInstanceMethod
和+ resolveClassMethod
两个方法中动态添加实现。 - 如果动态方法决议阶段没有做出任何响应,则进入动态消息转发阶段。此时可以在动态消息转发阶段做一下处理,如果还不进行处理,就会引发
Crash
。
4.2 整体分析
总体可以被分为以下三个部分
- 刚调用
objc_msgSend
函数后,内部会做一些处理逻辑。 - 复杂的查找
IMP
的过程,会涉及到缓存列表和方法列表等等信息。 - 进入动态方法决议和消息转发阶段。
下一篇我们将探索 动态方法决议和动态消息转发流程。