1. 引入消息查找
通過上一篇文章我們對方法的本質的分析,我們知道方法的本質就是發送消息,通過objc_msgSend
發送消息來查找方法。最后我們分析到lookUpImpOrForward
處的實現已經由匯編轉變為C++,現在我們通過這篇文章來分析一下這個慢速查找流程。
在上一篇文章中我們一開始全局搜索_lookUpImpOrForward
并沒有找到,但是我們知道C++方法前面加一個_
,說出才會輕松的找打該方法的代碼。下面我們在通過匯編來驗證一下。
首先我們打開Debug -> Debug Workflow -> Always Show Disassembly
編寫一個類,實現一個方法,調用一下。在調用處打個斷點。
實現代碼:
跳轉到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;
- 如果還是沒找到就根據當前類是元類還是進行方法的動態解析,解析成功則返回,如果失敗就會進入消息轉發流程。