iOS Objective-C消息查找流程
iOS Objective-C 消息的查找
1. 引入消息查找
通过上一篇文章我们对方法的本质的分析,我们知道方法的本质就是发送消息,通过objc_msgSend
发送消息来查找方法。最后我们分析到lookUpImpOrForward
处的实现已经由汇编转变为C++,现在我们通过这篇文章来分析一下这个慢速查找流程。
在上一篇文章中我们一开始全局搜索_lookUpImpOrForward
并没有找到,但是我们知道C++方法前面加一个_
,说出才会轻松的找打该方法的代码。下面我们在通过汇编来验证一下。
首先我们打开Debug -> Debug Workflow -> Always Show Disassembly
编写一个类,实现一个方法,调用一下。在调用处打个断点。
实现代码:
实现代码.jpg 运行并在objc_msgSend添加断点跳转到objc_msgSend
,并在_objc_msgSend_uncached
处添加断点
跳转到_objc_msgSend_uncached
,找到lookUpImpOrForward
在汇编中我们看到,实际调用的是lookUpImpOrForward
在objc-runtime-new.mm
文件的 5989 行处。
2.方法查找流程
2.1 对象方法
- 自己有 - 直接调用
- 自己没有 - 找父类(有)- 调用父类
- 自己没有 - 父类没有 - 找父类的父类最终到NSObject(有)- 调用
- 自己没有 - 父类没有 - 父类的父类直到NSObject都没有 - 崩溃
2.2 类方法
- 自己有 - 直接调用
- 自己没有 - 找父类(有)- 调用父类
- 自己没有 - 父类没有 - 找父类的父类直到NSObject(有)- 调用
- 自己没有 - 父类没有 - 父类的父类直到NSObject没有 - NSObject的对象方法(有)- 调用
- 都没有 - 崩溃
3. lookUpImpOrForward
分析
我们定位到 objc-runtime-new.mm
文件的 5989 行处。
lookUpImpOrForward
源码:
/***********************************************************************
* lookUpImpOrForward.
* The standard IMP lookup.
* Without LOOKUP_INITIALIZE: tries to avoid +initialize (but sometimes fails)
* Without LOOKUP_CACHE: skips optimistic unlocked lookup (but uses cache elsewhere)
* Most callers should use LOOKUP_INITIALIZE and LOOKUP_CACHE
* inst is an instance of cls or a subclass thereof, or nil if none is known.
* If cls is an un-initialized metaclass then a non-nil inst is faster.
* May return _objc_msgForward_impcache. IMPs destined for external use
* must be converted to _objc_msgForward or _objc_msgForward_stret.
* If you don't want forwarding at all, use LOOKUP_NIL.
**********************************************************************/
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
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
// runtimeLock is held during isRealized and isInitialized checking
// to prevent races against concurrent realization.
// runtimeLock is held during method search to make
// method-lookup + cache-fill atomic with respect to method addition.
// 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();
// We don't want people to be able to craft a binary blob that looks like
// a class but really isn't one and do a CFI attack.
//
// To make these harder we want to make sure this is a class that was
// either built into the binary or legitimately registered through
// objc_duplicateClass, objc_initializeClassPair or objc_allocateClassPair.
//
// TODO: this check is quite costly during process startup.
checkIsKnownClass(cls);
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}
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().
for (unsigned attempts = unreasonableClassCount();;) {
// curClass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp;
goto done;
}
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.
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;
}
}
// No implementation found. Try method resolver once.
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
done:
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;
}
3.1 初始化一些变量
const IMP forward_imp = (IMP)_objc_msgForward_impcache;
IMP imp = nil;
Class curClass;
初始化一个forward_imp
、imp
、Class
,以便后续使用。
3.2 在cache中再次查找一遍
cache中查找以及相关宏定义和源码:
// Optimistic cache lookup
if (fastpath(behavior & LOOKUP_CACHE)) {
imp = cache_getImp(cls, sel);
if (imp) goto done_nolock;
}
#define fastpath(x) (__builtin_expect(bool(x), 1))
#define slowpath(x) (__builtin_expect(bool(x), 0))
done_nolock:
if (slowpath((behavior & LOOKUP_NIL) && imp == forward_imp)) {
return nil;
}
此处是做了一个判断,在老版本里面判断的是一个传入的参数cache
,是否去cache
里面查找,因为我们一开始走的是快速流程即汇编实现的objc_msgSend
,此处已经在cache
里面进行了查找,所以进入到该方法后则不再进行查找cache
,但是为了方法的通用性,有点流程并不是先走了objc_msgSend
,可能还没有去cache
查找过,所以先去cache
查找一遍,如果找到则go to done_nolock
,此处判断是否找到的是转发的imp
,以及非空判断,如果不是则返回imp
,是的话就返回你俩
,如果没找到就继续进行下面的流程。
3.3 准备工作
3.3.1 检查类是否存在
// TODO: this check is quite costly during process startup. -> 这种检查在进程启动期间是非常昂贵的
checkIsKnownClass(cls);
// checkIsKnownClass
static void
checkIsKnownClass(Class cls)
{
if (slowpath(!isKnownClass(cls))) {
_objc_fatal("Attempt to use unknown class %p.", cls);
}
}
// isKnownClass
static bool
isKnownClass(Class cls)
{
if (fastpath(objc::dataSegmentsRanges.contains(cls->data()->witness, (uintptr_t)cls))) {
return true;
}
auto &set = objc::allocatedClasses.get();
return set.find(cls) != set.end() || dataSegmentsContain(cls);
}
通过上面的源码检测类是否加载进来,这种检查在进程启动期间是非常昂贵的,所以才有了缓存的概念,这种调用过于昂贵,减少调用才能加快程序的运行,给用户带来优质的体验。如果类都不在则直接报错了,类存在才有继续查找的可能。
3.3.2 准备类信息
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}
// realizeClassMaybeSwiftAndLeaveLocked
static Class
realizeClassMaybeSwiftAndLeaveLocked(Class cls, mutex_t& lock)
{
return realizeClassMaybeSwiftMaybeRelock(cls, lock, true);
}
// realizeClassMaybeSwiftMaybeRelock
static Class
realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked)
{
lock.assertLocked();
if (!cls->isSwiftStable_ButAllowLegacyForNow()) {
// Non-Swift class. Realize it now with the lock still held.
// fixme wrong in the future for objc subclasses of swift classes
realizeClassWithoutSwift(cls, nil);
if (!leaveLocked) lock.unlock();
} else {
// Swift class. We need to drop locks and call the Swift
// runtime to initialize it.
lock.unlock();
cls = realizeSwiftClass(cls);
ASSERT(cls->isRealized()); // callback must have provoked realization
if (leaveLocked) lock.lock();
}
return cls;
}
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
}
// initializeAndLeaveLocked
// Locking: caller must hold runtimeLock; this may drop and re-acquire it
static Class initializeAndLeaveLocked(Class cls, id obj, mutex_t& lock)
{
return initializeAndMaybeRelock(cls, obj, lock, true);
}
// initializeAndMaybeRelock
/***********************************************************************
* class_initialize. Send the '+initialize' message on demand to any
* uninitialized class. Force initialization of superclasses first.
* inst is an instance of cls, or nil. Non-nil is better for performance.
* Returns the class pointer. If the class was unrealized then
* it may be reallocated.
* Locking:
* runtimeLock must be held by the caller
* This function may drop the lock.
* On exit the lock is re-acquired or dropped as requested by leaveLocked.
**********************************************************************/
static Class initializeAndMaybeRelock(Class cls, id inst,
mutex_t& lock, bool leaveLocked)
{
lock.assertLocked();
ASSERT(cls->isRealized());
if (cls->isInitialized()) {
if (!leaveLocked) lock.unlock();
return cls;
}
// Find the non-meta class for cls, if it is not already one.
// The +initialize message is sent to the non-meta class object.
Class nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// Realize the non-meta class if necessary.
if (nonmeta->isRealized()) {
// nonmeta is cls, which was already realized
// OR nonmeta is distinct, but is already realized
// - nothing else to do
lock.unlock();
} else {
nonmeta = realizeClassMaybeSwiftAndUnlock(nonmeta, lock);
// runtimeLock is now unlocked
// fixme Swift can't relocate the class today,
// but someday it will:
cls = object_getClass(nonmeta);
}
// runtimeLock is now unlocked, for +initialize dispatch
ASSERT(nonmeta->isRealized());
initializeNonMetaClass(nonmeta);
if (leaveLocked) runtimeLock.lock();
return cls;
}
以上代码主要作用就是为当前需要查找的类 准备了充分的内容,包括对Swift
的处理,以及各种get
和set
方法,把值赋给我们的cls
,最终将cls
的内容存储到我们一开始初始化的curClass
中。
3.4 消息查找
3.4.1 核心代码
消息查找核心代码:
for (unsigned attempts = unreasonableClassCount();;) {
// curClass method list.
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
imp = meth->imp;
goto done;
}
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.
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;
}
}
- 开启一个循环
- 在当前类找方法,找到就
goto done
- 没找到则判断父类是否为空,如果为
nil
则将imp
赋值为forward_imp
并跳出循环 - 父类不为空则判断
--attempts == 0
,成立则报错,不成立则继续 - 此时已经获取到了父类,去父类的
cache
中查找,如果找到并且不等于forward_imp
则goto done
否则继续循环 - 如果最终找到了则
goto done
调用log_and_fill_cache
填充缓存,log_and_fill_cache
会调用cache_fill
然后调用insert
将慢速查找到的方法放入缓存中,以便后续能通过快速查找的方法找到。 - 如果最终也没找到则会进入
resolveMethod_locked
中做方法解析
3.4.1 getMethodNoSuper_nolock
// getMethodNoSuper_nolock
/***********************************************************************
* getMethodNoSuper_nolock
* fixme
* Locking: runtimeLock must be read- or write-locked by the caller
**********************************************************************/
static method_t *
getMethodNoSuper_nolock(Class cls, SEL sel)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
// fixme nil cls?
// fixme nil sel?
for (auto mlists = cls->data()->methods.beginLists(),
end = cls->data()->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
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;
}
getMethodNoSuper_nolock
主要就是从cls
的data
的methods
里面循环循环查找,然后调用search_method_list_inline
与sel
进行匹配,找到后就返回,找不到返回nil。
3.4.2 findMethodInSortedMethodList
ALWAYS_INLINE static method_t *
findMethodInSortedMethodList(SEL key, const method_list_t *list)
{
ASSERT(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) {
// `probe` is a match.
// Rewind looking for the *first* occurrence of this value.
// This is required for correct category overrides.
while (probe > first && keyValue == (uintptr_t)probe[-1].name) {
probe--;
}
return (method_t *)probe;
}
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
findMethodInSortedMethodList
就是通过一个二分查找,找到方法返回的过程。
3.5 找不到的处理
如果最终都没有找到需要查找的方法,会进入动态解析流程,这是Runtime
给我们提供的一种容错处理。
3.5.1 resolveMethod_locked
/***********************************************************************
* resolveMethod_locked
* Call +resolveClassMethod or +resolveInstanceMethod.
*
* Called with the runtimeLock held to avoid pressure in the caller
* Tail calls into lookUpImpOrForward, also to avoid pressure in the callerb
**********************************************************************/
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
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);
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
resolveMethod_locked
主要作用是判断类是否是元类
- 如果不是则进入
resolveInstanceMethod
继续处理 - 如果是则进入
resolveClassMethod
继续处理,并且通过lookUpImpOrNil
判断非空,最后也会调用resolveInstanceMethod
进行对象方法的动态解析,因为根据isa
走位图,万物皆对象,最终都会继承自NSObject
,都会找到NSObject
的对象方法中。
3.5.2 resolveInstanceMethod
/***********************************************************************
* resolveInstanceMethod
* Call +resolveInstanceMethod, looking for a method to be added to class cls.
* cls may be a metaclass or a non-meta class.
* Does not check if the method already exists.
**********************************************************************/
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
IMP imp = lookUpImpOrNil(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveInstanceMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
该函数实质是做了一次方法的解析操作
- 初始化一个sel
resolveInstanceMethod
- 然后查找该sel,找到后则继续处理,找不到就直接返回
- 通过
objc_msgSend
发送消息,这里发送的是resolveInstanceMethod
消息,如果返回YES
则说明该方法被实现,否则未实现。 - 如果实现并且解析处做了转发,说明该
sel
指向了新的imp
,并通过下面的打印来说明新IMP
被动态实现,或者没找到。
3.5.3 resolveClassMethod
/***********************************************************************
* resolveClassMethod
* Call +resolveClassMethod, looking for a method to be added to class cls.
* cls should be a metaclass.
* Does not check if the method already exists.
**********************************************************************/
static void resolveClassMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
ASSERT(cls->isMetaClass());
if (!lookUpImpOrNil(inst, @selector(resolveClassMethod:), cls)) {
// Resolver not implemented.
return;
}
Class nonmeta;
{
mutex_locker_t lock(runtimeLock);
nonmeta = getMaybeUnrealizedNonMetaClass(cls, inst);
// +initialize path should have realized nonmeta already
if (!nonmeta->isRealized()) {
_objc_fatal("nonmeta class %s (%p) unexpectedly not realized",
nonmeta->nameForLogging(), nonmeta);
}
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(nonmeta, @selector(resolveClassMethod:), sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveClassMethod adds to self->ISA() a.k.a. cls
IMP imp = lookUpImpOrNil(inst, sel, cls);
if (resolved && PrintResolving) {
if (imp) {
_objc_inform("RESOLVE: method %c[%s %s] "
"dynamically resolved to %p",
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel), imp);
}
else {
// Method resolver didn't add anything?
_objc_inform("RESOLVE: +[%s resolveClassMethod:%s] returned YES"
", but no new implementation of %c[%s %s] was found",
cls->nameForLogging(), sel_getName(sel),
cls->isMetaClass() ? '+' : '-',
cls->nameForLogging(), sel_getName(sel));
}
}
}
该函数跟resolveInstanceMethod
差不多,唯一的区别就是发消息的时候是向元类发送消息。其余的就不在赘述了。
3.6 消息转发
如果没有做动态解析处理,最后会来到消息转发,这也是为什么一开始会在lookUpImpOrForward
函数中初始化一个_objc_msgForward_impcache
的IMP
,然后填充到cls
的cache
里面。到此我们的消息查找流程就结束了,那么什么是消息的转发机制呢,我们后续再详细讲解。
4. 总结
- 消息的查找有快速流程通过
objc_msgSend
通过cache
查找、慢速流程lookUpImpOrForward
进行查找; - 从快速查找进入慢速查找一开始是不会进行
cache
查找的,而是直接从方法列表进行查找; - 查找前会做好准备,确保类信息完整
- 首先从当前类进行查找,找到就可返回
- 如果没找到则去父类的缓存进行查找,如果找不到则查找父类的方法列表,找到就可返回,找不到就继续向父类的父类进行查找,直到NSObject;
- 如果还是没找到就根据当前类是元类还是进行方法的动态解析,解析成功则返回,如果失败就会进入消息转发流程。