前言
相關文章
相關代碼:
objc4_752源碼
上篇文章講述了方法的本質實際就是調用objc_msgSend,然後經過彙編代碼進行快速調用查找,或者根據C代碼慢速查找;
彙編代碼快速查找,已經在源碼中做了詳細的註釋,並且在其中加了一個objc_msgSend反彙編僞代碼,可以配合彙編代碼一起查看,大家可以自行查看;
方法在類中的查找流程
首先我們來看一下代碼來回顧一下之前的東西:查看Demo
int main(int argc, const char * argv[]) {
@autoreleasepool {
#pragma clang diagnostic push
// // 讓編譯器忽略錯誤,不然調用沒有的方法,編譯器編譯完語法分析會報錯,導致不能運行
#pragma clang diagnostic ignored "-Wundeclared-selector"
XZTeacher *teacher = [[XZTeacher alloc] init];
// 對象方法
// 自己有 - 返回自己
[teacher sayHello];
// 自己沒有 - 老爸 -
[teacher sayNB];
// // 自己沒有 - 老爸沒有 - NSObject
[teacher sayMaster];
// 自己沒有 - 老爸沒有 - NSObject 沒有
// unrecognized selector sent to instance 0x101806f30 調用下面會直接報錯
// [teacher performSelector:@selector(saySomething)];
// 類方法
// 自己有 - 返回自己
[XZTeacher sayObjc];
// // 自己沒有 - 老爸 -
[XZTeacher sayHappay];
// // 自己沒有 - 老爸沒有 - NSObject
[XZTeacher sayEasy];
// 自己沒有 - 老爸沒有 - NSObject 沒有
// unrecognized selector sent to instance 0x100001368
// [XZTeacher performSelector:@selector(sayLove)];
// sayMaster]這個是NSObject中的實例方法
[XZTeacher performSelector:@selector(sayMaster)];
#pragma clang diagnostic pop
}
return 0;
}
我們來看一下這個方法的打印結果:
我們可以看到,我們使用[XZTeacher performSelector:@selector(sayMaster)],調用NSObject中的實例方法,也調用成功了;這個實際上,我們在之前的文章中也有提到過什麼原因,這裏再來回顧一下,首先類方法實際就是在元類中以實例方法的方式進行存儲的,而元類的根源類,也是繼承於NSObject的,所以在方法查找的過程中,類方法會最終查找到NSObject類的實例方法,找到並執行了;但是還有一部分方法是在本類,以及父類都找不到的時候就會崩潰,下面我們就進行分析一下
就是根據上圖的查找結果,最終會找到sayMaster並執行
這個我們可以進行總結一下:
對象的實例方法:
-
自己有
-
自己沒有-->父類有
-
自己沒有-->父類沒有-->父類的父類----直到NSObject
-
自己沒有-->父類沒有-->直到NSObject也沒有--->崩潰
對象的類方法:
-
自己有
-
自己沒有-->父類有
-
自己沒有-->父類沒有-->父類的父類----直到NSObject
-
自己沒有-->父類沒有-->直到NSObject類方法沒有-->NSObject實例方法
-
自己沒有-->父類沒有-->直到NSObject類方法沒有-->NSObject實例方法沒有-->崩潰
方法的慢速查找流程
得到上面的結論之後,我們從底層源碼進行分析,首先我們調用方法同過objc_msgSend 查找cachet-->進入彙編快速查找-->最終進入代碼_class_lookupMethodAndLoadCache3 慢速查找,由於都是新方法,所以我們可以直接進入方法最後進行查看
我們直接查看_class_lookupMethodAndLoadCache3方法源碼
//這裏的extern 標示外部也可以調用這個方法
extern IMP _class_lookupMethodAndLoadCache3(id, SEL, Class);
IMP _class_lookupMethodAndLoadCache3(id obj, SEL sel, Class cls)
{
/**
obj 當前對象
sel 方法名稱
cls 實例方法爲類 類方法爲元類
NO cache 沒有緩存纔會進入這裏,有的話,從緩存哪裏就返回出去了
*/
return lookUpImpOrForward(cls, sel, obj,
YES/*initialize*/, NO/*cache*/, YES/*resolver*/);
}
我們可以看出,這個方法中,會調用lookUpImpOrForward,並傳入幾個參數cls(當前類,如果是類方法這裏就是元類)sel 方法編號,obj (當前對象)以及initialize(YES) 和cache(NO),resilver(NO),繼續查看lookUpImpOrForward方法:
IMP lookUpImpOrForward(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = nil;
bool triedResolver = NO;
runtimeLock.assertUnlocked();
// Optimistic cache lookup
// 查找方法進入的時候這裏是NO
if (cache) {
imp = cache_getImp(cls, sel);
if (imp) return imp;
}
// 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();
// 內部對class 的判斷,是否被編譯
checkIsKnownClass(cls);
// 這裏是拿出類中的bits中的 class_rw_t 中的 data中的Method查看是否有
//爲查找方法做準備條件,判斷類有沒有加載好,如果沒有加載好,那就先加載一下類信息,準備好父類、元類
if (!cls->isRealized()) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
// runtimeLock may have been dropped but is now locked again
}
//確定類已經加載完成,
if (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
}
retry:
runtimeLock.assertLocked();
// Try this class's cache.
//判斷該方法從緩存裏面查找一遍,如果是方法調用,會先走cache,一般這裏imp 是取不到的,別的方法可能會調用這裏
imp = cache_getImp(cls, sel);
if (imp) goto done;
// Try this class's method lists.
// 這裏的括號是爲了行程局部作用域,避免局部變量命名重
{
// 在MethodList 中進行查找方法
Method meth = getMethodNoSuper_nolock(cls, sel);
if (meth) {
// 找到方法後,填充到緩存中去,內部調用cache_fill_nolock
log_and_fill_cache(cls, meth->imp, sel, inst, cls);
imp = meth->imp;
//直接跳到done這個點,C語言調用方式goto
goto done;
}
}
// Try superclass caches and method lists.查找父類方法
{
unsigned attempts = unreasonableClassCount();
// 這裏循環衝父類中進行查找
for (Class curClass = cls->superclass;
curClass != nil;
curClass = curClass->superclass)
{
// Halt if there is a cycle in the superclass chain.
if (--attempts == 0) {//內存出錯
_objc_fatal("Memory corruption in class list.");
}
// Superclass cache. //從父類的緩存中查找一下
imp = cache_getImp(curClass, sel);
if (imp) {
// 可能是蘋果大大還有其他的類型吧
if (imp != (IMP)_objc_msgForward_impcache) {
// Found the method in a superclass. Cache it in this class.
log_and_fill_cache(cls, imp, sel, inst, curClass);
goto done;
}
else {
//如果是_objc_msgForward_impcache方法,就退出,不需要遍歷了,因爲_objc_msgForward_impcache這個方法就是未實現時候轉發用的
// Found a forward:: entry in a superclass.
// Stop searching, but don't cache yet; call method
// resolver for this class first.
break;
}
}
// Superclass method list.
// 父類緩存中沒找到就去父類的方法列表中查找
Method meth = getMethodNoSuper_nolock(curClass, sel);
if (meth) {
log_and_fill_cache(cls, meth->imp, sel, inst, curClass);
imp = meth->imp;
goto done;
}
}
}
// No implementation found. Try method resolver once.
//如果方法仍然沒找到,就開始做方法消息動態解析了
if (resolver && !triedResolver) {
runtimeLock.unlock();
// 實例方法解析:resolveInstanceMethod
// 類方法解析:resolveClassMethod
resolveMethod(cls, sel, inst);
runtimeLock.lock();
// Don't cache the result; we don't hold the lock so it may have
// changed already. Re-do the search from scratch instead.
//設置爲NO,防止因爲消息動態解析導致死循環
triedResolver = YES;
goto retry;
}
// No implementation found, and method resolver didn't help.
// Use forwarding.
//如果第一次轉發還是沒用的話,就取出_objc_msgForward_impcache這個方法指針
imp = (IMP)_objc_msgForward_impcache;
//緩存起來將指針返回回去執行
cache_fill(cls, sel, imp, inst);
done:
runtimeLock.unlock();
return imp;
}
這個方法就有幾個重點要觀察的:
-
runtimeLock.lock()
爲了防止多線程操作 -
realizeClassMaybeSwiftAndLeaveLocked 爲查找方法做準備條件,如果類沒有初始化時,初始化類和父類、元類、分類等
-
imp = cache_getImp(cls, sel)
爲了容錯從緩存
中再找一遍,如果找到直接goto done(8)
-
局部作用域中查找本類getMethodNoSuper_nolock中,如果找到進行log_and_fill_cache 保存緩存直接
goto done(8)
-
局部作用與中在父類中進行查找首先
在父類緩存
中查找,若有直接log_and_fill_cache
並goto done
;沒有再去父類的方法列表中
查找方法,若有直接log_and_fill_cache
並goto done
(8)
-
如果還沒找到就動態方法解析
_class_resolveMethod
,標記爲triedResolver = YES(已自我拯救過)
,動態方法解析結束後跳轉慢速流程第三步進行再一次查找
-
如果動態方法解析之後再找一遍仍然沒找到
imp
,就拋出錯誤_objc_msgForward_impcache
得到imp
並cache_fill
-
done
:多線程解鎖,返回imp
這個方法cache_getImp 後續進行詳細解釋
getMethodNoSuper_nolock 方法
這裏對其中部分代碼進行解釋第四步getMethodNoSuper_nolock 方法遍歷調用search_method_list方法
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)
{
// 利用二分查找尋找方法
method_t *m = search_method_list(*mlists, sel);
if (m) return m;
}
return nil;
}
其中在search_method_list過程之後,通過findMethodInSortedMethodList(sel, mlist)進行二分查找,保證能夠快速尋找到目標方法
static method_t *search_method_list(const method_list_t *mlist, SEL sel)
{
int methodListIsFixedUp = mlist->isFixedUp();
int methodListHasExpectedSize = mlist->entsize() == sizeof(method_t);
if (__builtin_expect(methodListIsFixedUp && methodListHasExpectedSize, 1)) {
// 如果方法列表已經排序好了,則通過二分查找法查找方法,以節省時間
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方法二分查找算法的具體實現(可自行了解)
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;
// 二分查找方法
// >>1 表示將變量n的各個二進制位順序右移1位,最高位補二進制0
// count >>= 1 如果count爲偶數則值變爲(count / 2);如果count爲奇數則值變爲(count-1) / 2
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;
}
// 如果keyValue > probeValue 則折半向後查詢
if (keyValue > probeValue) {
base = probe + 1;
count--;
}
}
return nil;
}
log_and_fill_cache -->cache_fill--->cache_fill_nolock連續調用這個方法可以看類的分析下
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (objcMsgLogEnabled) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
// 寫入緩存
cache_fill (cls, sel, imp, receiver);
}
void cache_fill(Class cls, SEL sel, IMP imp, id receiver)
{
#if !DEBUG_TASK_THREADS
mutex_locker_t lock(cacheUpdateLock);
cache_fill_nolock(cls, sel, imp, receiver);
#else
_collecting_in_critical();
return;
#endif
}
_class_resolveMethod
動態方法解析:在找不到imp時的自我拯救操作 ,下面會進行分析
_objc_msgForward_impcache方法
這個方法中就是如果方法在本類,父類都找不到方法後執行到這裏_objc_msgForward_impcache 方法
extern void _objc_msgForward_impcache(void);//這個方法會進入彙編
STATIC_ENTRY __objc_msgForward_impcache
// No stret specialization.
b __objc_msgForward
END_ENTRY __objc_msgForward_impcache
ENTRY __objc_msgForward
adrp x17, __objc_forward_handler@PAGE
ldr p17, [x17, __objc_forward_handler@PAGEOFF] //調用_objc_forward_handler 方法
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
_objc_msgForward_impcache這個方法調用到彙編之後,彙編方法又會調用會C方法_objc_forward_handler
void *_objc_forward_handler = nil;
void *_objc_forward_stret_handler = nil;
#else
// Default forward handler halts the process.
__attribute__((noreturn)) 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;
這裏我們就能看到熟悉的unrecognized selector sent to instance 崩潰信息了
消息轉發機制初探
我們已經看到了如果沒有實現方法,然後調用,系統會直接崩潰,那麼如何避免這些問題呢,蘋果大大給我們提供了另一個解決思路,就是消息轉發機制,這篇我們先進行一個初步的查看
首先我們現在main函數中調用一個沒有實現的方法
int main(int argc, const char * argv[]) {
@autoreleasepool {
#pragma clang diagnostic push
// // 讓編譯器忽略錯誤,不然調用沒有的方法,編譯器編譯完語法分析會報錯,導致不能運行
#pragma clang diagnostic ignored "-Wundeclared-selector"
XZTeacher *teacher = [[XZTeacher alloc] init];
// 消息轉發流程這個方法.h中之聲明,不實現
[teacher saySomthing];
#pragma clang diagnostic pop
}
return 0;
}
根據上面文章的分析,我們很快可以定位到,如果都沒有找到這個方法,那麼就會走到動態解析方法_class_resolveMethod
static void resolveMethod(Class cls, SEL sel, id inst)
{
runtimeLock.assertUnlocked();
assert(cls->isRealized());
if (! cls->isMetaClass()) {//不是元類
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
resolveInstanceMethod(cls, sel, inst);
}
}
}
因爲外部我們寫的是實力方法,我們這裏先看以下方法resolveInstanceMethod方法
static void resolveInstanceMethod(Class cls, SEL sel, id inst)
{
runtimeLock.assertUnlocked();
assert(cls->isRealized());
//查找方法中是否有實現resolveInstanceMethod這個方法
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
//發送SEL_resolveInstanceMethod消息,
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
//發送消息SEL_resolveInstanceMethod 傳參爲我們的方法編號
//崩潰的方法不是這個方法,說明再NSObject方法中有實現這個方法
bool resolved = msg(cls, SEL_resolveInstanceMethod, sel);
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
//再次查找類中是否sel方法,因爲resolveInstanceMethod方法執行後可能動態進行添加了,resolver是不要進行消息轉發了
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
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方法,其實也就是+ (BOOL)resolveInstanceMethod:(SEL)sel方法,我們通過在源碼搜索可以看到在NSObject類中已經都實現了,並且都是一個空方法,並且返回NO,
//在NSObject.mm 文件中有看到這個方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
在lookUpImpOrNil 方法中實際就是有調用了一次方法查找流程這裏的參數是NO( initialize) YES (Cache ) NO (resolver)不會造成遞歸
IMP lookUpImpOrNil(Class cls, SEL sel, id inst,
bool initialize, bool cache, bool resolver)
{
IMP imp = lookUpImpOrForward(cls, sel, inst, initialize, cache, resolver);
if (imp == _objc_msgForward_impcache) return nil;
else return imp;
}
這裏我們看到系統會自動調用+resolveInstanceMethod方法,這說明我們就有一些可操作性,那麼我們在類中實現以下這個方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"XZTeacher 走了resolveInstanceMethod %p",sel);
return [super resolveInstanceMethod:sel];
}
運行程序看到輸出結果,我們可以看到執行了我們輸出日誌,但是崩潰了,這裏我們就考慮我們是可以做一些操作
這裏我們可以考慮到這裏實際上的一些列操作就是爲了找到函數的IMP ,那我們是不是可以直接在這裏將函數IMP實現使得下面lookUpImpOrNil方法的時候可以找到IMP,程序就不會崩潰了;
具體實現方法,引入頭文件#import <objc/message.h> 如下所示:
#import <objc/message.h>
- (void)sayHello{
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(saySomthing)) {
NSLog(@"進入方法了");
IMP saySomeIMP = class_getMethodImplementation(self, @selector(sayHello));
Method sayMethod = class_getInstanceMethod(self, @selector(sayHello));
const char *sayType = method_getTypeEncoding(sayMethod);
return class_addMethod(self, sel, saySomeIMP, sayType);
}
return [super resolveInstanceMethod:sel];
}
我們來看一下輸出結果:
可以看出我們在調用saySomthing 方法的時候進入了我們寫的代碼,並且還調用了我們想讓他調用的sayHello方法,而且沒有崩潰;
總結
這篇文章主要是根據上篇文章職工objc_msgSend方法快速查找流程彙編過程找到_class_lookupMethodAndLoadCache3 方法,並進行分析了這個代碼中消息查找的流程,並對消息轉發防止崩潰進行了初步的瞭解,如果有錯誤的地方還請指正,大家一起討論,(文章中有錯誤後,我發現會第一時間對博文進行修改),還望大家體諒!
歡迎大家點贊,關注我的CSDN,我會定期做一些技術分享!
寫給自己
時光流逝,討厭的人會帶着討人厭的話離開,喜歡的人會帶着美好的事到來。把目光放在遠處,灑脫還給自己。忘掉路人,放過自己,未完待續...