在上一篇文章iOS-底層原理11:消息流程分析之慢速查找 中夸溶,分析了消息慢速查找流程
足绅,如果查找不到將進行動態(tài)方法決議
,如果動態(tài)方法決議仍然沒有找到實現(xiàn),則進行消息轉(zhuǎn)發(fā)
粥航。
案例
step1:
新建一個LBHPerson
類缀程,定義一個實例方法instanceMethod1
和一個類方法classMethod1
摆昧,只聲明不實現(xiàn)
//.h
@interface LBHPerson : NSObject
- (void)instanceMethod1;
+ (void)classMethod1;
@end
//.m
@implementation LBHPerson
@end
step2:
在main
函數(shù)中調(diào)用LBHPerson
類的實例方法instanceMethod1
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LBHPerson *person = [LBHPerson alloc];
[person instanceMethod1];
}
return 0;
}
step3:
運行結(jié)果
調(diào)用類方法
[LBHPerson classMethod1];
運行結(jié)果
unrecognized selector sent to instance 0xxxxx
找不到方法實現(xiàn)
這是一個開發(fā)中很常見的奔潰問題伪嫁,先學(xué)習(xí)這篇文章晶伦,然后用動態(tài)方法決議和消息轉(zhuǎn)發(fā)解決這個問題。
1. 動態(tài)方法決議
動態(tài)方法決議
:慢速查找流程未找到方法,會給一次機會
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
// 對象方法
if (! cls->isMetaClass()) {
resolveInstanceMethod(inst, sel, cls);
}
// 類方法
else {
resolveClassMethod(inst, sel, cls);
//為什么要有這行代碼? -- 類方法在元類中是對象方法,所以還是需要查詢元類中對象方法的動態(tài)方法決議
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// 重新查詢一次
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
分為以下幾步:
part1:
判斷cls
是否是元類
- 如果是
類
蝇庭,調(diào)用實例方法
的動態(tài)方法決議resolveInstanceMethod
- 如果是
元類
北发,調(diào)用類方法
的動態(tài)方法決議resolveClassMethod
逼蒙,如果在元類
中沒有找到或者為空
陕截,則在元類
的實例方法
的動態(tài)方法決議resolveInstanceMethod
中查找, 是因為類方法存儲在元類中合呐,是元類的實例方法拆祈,所以還需要查找元類中實例方法的動態(tài)方法決議
part2:
如果動態(tài)方法決議中淤年,將其實現(xiàn)指向了其他方法
余素,則繼續(xù)查找
指定的imp
洛搀,即繼續(xù)慢速查找lookUpImpOrForward
流程
此時
behavior = 1
,LOOKUP_CACHE = 4
谎砾,lookUpImpOrForward
函數(shù)中形參behavior
變成了1 | 4 = 5
挚币,這決定了進入lookUpImpOrForward后:
fastpath(behavior & LOOKUP_CACHE) = 5 & 4 = 4
凄吏,條件成立任连,會優(yōu)先cache_getImp
讀取一次緩存slowpath(behavior & LOOKUP_RESOLVER) = 5 & 2 = 0
拱她,條件成立唬复,不會進入resolveMethod_locked
動態(tài)方法決議。lookUpImpOrForward
會循環(huán)遍歷cls繼承鏈的所有類的cache和methodList來尋找imp
流程圖:
1.1 實例方法決議
step1:
實例方法在快速查找
-> 慢速查找
都沒有找到的情況下壤巷,會走到 resolveInstanceMethod
方法,源碼如下:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
// 1. 查找元類對象(cls->ISA())的類中是否有`resolveInstanceMethod`的imp悲没。
// (根元類中默認實現(xiàn)了`resolveInstanceMethod`方法,所以永遠不會return)
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
return;
}
// 2. 發(fā)送resolve_sel消息
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// 3. 再搜索一次sel的imp
//(如果在上面resolveInstanceMethod函數(shù)實現(xiàn)了sel,我們就拿到imp了褂痰,成功將sel和imp寫入cls的緩存中)
IMP imp = lookUpImpOrNil(inst, sel, cls);
// 做Log記錄
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));
}
}
}
分步解析:
part1:
查找resolve_sel
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
// Resolver not implemented.
return;
}
問題
:lookUpImpOrNil
到底做了什么?
解答
:
查看lookUpImpOrNil
源碼static inline IMP lookUpImpOrNil(id obj, SEL sel, Class cls, int behavior = 0) { // behavior = 0, LOOKUP_CACHE = 4屋厘, LOOKUP_NIL = 8 return lookUpImpOrForward(obj, sel, cls, behavior | LOOKUP_CACHE | LOOKUP_NIL); }
可以看到在
lookUpImpOrNil
中又調(diào)用了lookUpImpOrForward慢速查找流程
lookUpImpOrForward
函數(shù)中形參behavior
變成了0 | 4 | 8 = 12
瞻凤, 這決定了進入lookUpImpOrForward
后:
fastpath(behavior & LOOKUP_CACHE) = 12 & 4 = 4
肝集,條件成立,會優(yōu)先cache_getImp
讀取一次緩存slowpath(behavior & LOOKUP_RESOLVER) = 12 & 2 = 0
忧吟,條件成立劣像,不會進入resolveMethod_locked
動態(tài)方法決議摧玫。lookUpImpOrNil
中的lookUpImpOrForward
會循環(huán)遍歷cls繼承鏈的所有類的cache和methodList來尋找imp
判斷能否在慢速查找流程中
找到resolveInstanceMethod
方法實現(xiàn)。實際上根本不會進入if
條件,因為在NSObject元類
存在resolveInstanceMethod
類方法。
問題
:為什么不會進if
條件榜配?NSObject元類
中存在resolveInstanceMethod
類方法能證明嗎?
解答
:/// 遍歷方法 -(void) printMethodes: (Class)cls { // 記錄函數(shù)個數(shù) unsigned int count = 0; // 讀取函數(shù)列表 Method *methodList = class_copyMethodList(cls, &count); for (int i = 0; i < count; i++) { Method method = methodList[i]; SEL sel = method_getName(method); IMP imp = class_getMethodImplementation(cls, sel); NSLog(@"method: %@-%p", NSStringFromSelector(sel), imp); } free(methodList); } //調(diào)用 [self printMethodes:objc_getMetaClass("NSObject")];
運行結(jié)果
在
NSObject元類
方法列表中可以找到resolveInstanceMethod
類方法
part2:
發(fā)送resolve_sel
消息
// 2. 消息發(fā)送
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
part3:
通過慢速查找流程
獲取用戶調(diào)用的方法sel
(demo中為instanceMethod1)的方法實現(xiàn)imp
辆苔,此處的獲取是為了日志使用
IMP imp = lookUpImpOrNil(inst, sel, cls);
奔潰修改
step1:
在LBHPerson
中新增一個lbhInstanceMethod
的實例方法,聲明并實現(xiàn)
//.h
@interface LBHPerson : NSObject
- (void)lbhInstanceMethod;
@end
//.m
@implementation LBHPerson
- (void)lbhInstanceMethod
{
NSLog(@"%s",__func__);
}
@end
step2:
在LBHPerson
類中重寫resolveInstanceMethod
類方法
@implementation LBHPerson
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"%@ 來了", NSStringFromSelector(sel));
if (sel == @selector(instanceMethod1)) {
//獲取lbhInstanceMethod方法的imp
IMP imp = class_getMethodImplementation(self, @selector(lbhInstanceMethod));
//獲取lbhInstanceMethod的實例方法
Method lbhInstanceMethod = class_getInstanceMethod(self, @selector(lbhInstanceMethod));
//獲取lbhInstanceMethod的豐富簽名
const char *type = method_getTypeEncoding(lbhInstanceMethod);
//將sel的實現(xiàn)指向lbhInstanceMethod
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
// return NO;
}
@end
運行
崩潰解決了啄寡,實際上這么寫是比較雞肋的,都已經(jīng)知道某個方法沒有實現(xiàn),那直接實現(xiàn)就好了墨技,當(dāng)然可以將if
條件去掉,所有未實現(xiàn)的方法都走這個實現(xiàn),那么有沒有更好的方法呢娃善?繼續(xù)往下學(xué)習(xí)外厂。
1.2 類方法決議
類方法在快速查找
-> 慢速查找
都沒有找到的情況下冕象,會走到resolveClassMethod
方法代承,源碼如下:
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));
}
}
}
resolveClassMethod
方法流程與resolveInstanceMethod
方法流程類似汁蝶。
崩潰解決
在LBHPerson
中添加一個lbhClassMethod
類方法的,重寫resolveClassMethod
類方法
+ (void)lbhClassMethod
{
NSLog(@"%s",__func__);
}
+ (BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"%@ 來了", NSStringFromSelector(sel));
if (sel == @selector(classMethod1)) {
IMP imp = class_getMethodImplementation(objc_getMetaClass("LBHPerson"), @selector(lbhClassMethod));
Method lgClassMethod1 = class_getInstanceMethod(objc_getMetaClass("LBHPerson"), @selector(lbhClassMethod));
const char *type = method_getTypeEncoding(lgClassMethod1);
return class_addMethod(objc_getMetaClass("LBHPerson"), sel, imp, type);
}
return [super resolveClassMethod:sel];
}
1.3 優(yōu)化
上面解決方法都是在單獨的某個類中重寫動態(tài)決議方法论悴,這意味著每個類中都需要重寫這兩個方法掖棉,這樣太麻煩了,怎么做呢膀估? 相信大家都會幔亥。
- 實例方法:
類
-->父類
-->根類
-->nil
- 類方法 :
元類
-->根元類
-->根類
-->nil
如果在當(dāng)前類
或元類
中沒有找到方法實現(xiàn),會沿著它們的繼承鏈向上查找察纯,它們都會經(jīng)過根類即NSObject
帕棉。
問題
: 是否可以將上述的兩個方法統(tǒng)一整合在一起呢?
解答
:是可以的饼记,可以通過NSObject分類
的方式來實現(xiàn)統(tǒng)一處理
香伴,而且由于類方法的查找,在其繼承鏈具则,查找的也是實例方法即纲,所以可以將實例方法
和類方法
的統(tǒng)一放在resolveInstanceMethod
方法中處理。
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"%@ 來了", NSStringFromSelector(sel));
if (sel == @selector(classMethod1)) {
IMP imp = class_getMethodImplementation(objc_getMetaClass("LBHPerson"), @selector(lbhClassMethod));
Method lgClassMethod1 = class_getInstanceMethod(objc_getMetaClass("LBHPerson"), @selector(lbhClassMethod));
const char *type = method_getTypeEncoding(lgClassMethod1);
return class_addMethod(objc_getMetaClass("LBHPerson"), sel, imp, type);
}else if (sel == @selector(instanceMethod1)) {
//獲取lbhInstanceMethod方法的imp
IMP imp = class_getMethodImplementation(self, @selector(lbhInstanceMethod));
//獲取lbhInstanceMethod的實例方法
Method lbhInstanceMethod1 = class_getInstanceMethod(self, @selector(lbhInstanceMethod));
//獲取lbhInstanceMethod的豐富簽名
const char *type = method_getTypeEncoding(lbhInstanceMethod1);
//將sel的實現(xiàn)指向lbhInstanceMethod
return class_addMethod(self, sel, imp, type);
}
// return [super resolveInstanceMethod:sel];
return NO;
}
這種方式的實現(xiàn)博肋,正好與源碼中針對類方法的處理邏輯是一致的低斋,即完美闡述為什么調(diào)用了類方法動態(tài)方法決議,還要調(diào)用對象方法動態(tài)方法決議匪凡,其根本原因是類方法是元類中的實例方法
膊畴。
2. 消息轉(zhuǎn)發(fā)
我們了解到,如果快速+慢速沒有找到方法實現(xiàn)病游,動態(tài)方法決議也不行唇跨,就使用消息轉(zhuǎn)發(fā)
,但是,我們找遍了源碼也沒有發(fā)現(xiàn)消息轉(zhuǎn)發(fā)的相關(guān)源碼轻绞,可以通過以下方式來了解:
- 通過
instrumentObjcMessageSends
方式打印發(fā)送消息的日志 - 通過
hopper/IDA反編譯
2.1 instrumentObjcMessageSends
通過lookUpImpOrForward
--> log_and_fill_cache
--> logMessageSend
采记,在logMessageSend
源碼下方找到instrumentObjcMessageSends
的源碼實現(xiàn)。
在main中調(diào)用
instrumentObjcMessageSends
打印方法調(diào)用的日志信息政勃,有以下兩點準備工作
1唧龄、打開 objcMsgLogEnabled
開關(guān),即調(diào)用instrumentObjcMessageSends
方法時奸远,傳入YES
2既棺、在main中通過extern
聲明instrumentObjcMessageSends
方法
//最好使用命令行
extern void instrumentObjcMessageSends(BOOL flag);
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LBHPerson *person = [LBHPerson alloc];
instrumentObjcMessageSends(YES);
[person instanceMethod1];
// [LBHPerson classMethod];
instrumentObjcMessageSends(NO);
}
return 0;
}
- 通過
logMessageSend
源碼,了解到消息發(fā)送打印信息存儲在/tmp
文件夾下懒叛,
- 運行代碼丸冕,并前往
/tmp
文件夾,發(fā)現(xiàn)有msgSends
開頭的日志文件薛窥,打開發(fā)現(xiàn)在崩潰前胖烛,執(zhí)行了以下方法
2.2 通過hopper/IDA反編譯
Hopper
和IDA
是一個可以幫助我們靜態(tài)分析可視性文件的工具,可以將可執(zhí)行文件反匯編成偽代碼诅迷、控制流程圖
等佩番,下面以Hopper為例 (針對比較簡單的反匯編,demo版本即可)
step1:
在上面的例子中罢杉,查看下崩潰的堆棧信息
發(fā)現(xiàn)___forwarding___
來自CoreFoundation
step2:
通過image list
趟畏,讀取整個鏡像文件,然后搜索CoreFoundation
,查看其可執(zhí)行文件的路徑
step3:
通過文件路徑滩租,找到CoreFoundation
的可執(zhí)行文件
step4:
打開hopper
赋秀,選擇Try the Demo
,然后將上一步的可執(zhí)行文件拖入hopper
進行反匯編律想,選擇x86(64 bits)
step5:
以下是反匯編后的界面猎莲,主要使用上面的三個功能,分別是 匯編蜘欲、流程圖益眉、偽代碼
step6:
通過左側(cè)的搜索框搜索__forwarding_prep_0___
,然后選擇偽代碼
step7:
進入___forwarding___
的偽代碼實現(xiàn)姥份,首先是查看是否實現(xiàn)forwardingTargetForSelector
方法郭脂,如果沒有響應(yīng),跳轉(zhuǎn)至loc_6459b
即快速轉(zhuǎn)發(fā)沒有響應(yīng)澈歉,進入慢速轉(zhuǎn)發(fā)流程
step8:
跳轉(zhuǎn)至loc_64a67
展鸡,在其下方判斷是否響應(yīng)methodSignatureForSelector
方法
- 如果
沒有響應(yīng)
,跳轉(zhuǎn)至loc_64dd7
埃难,則直接報錯 - 如果獲取
methodSignatureForSelector
的方法簽名為nil
莹弊,也是直接報錯
step9:
如果methodSignatureForSelector
返回值不為空涤久,則在forwardInvocation
方法中對invocation
進行處理
所以,通過上面兩種查找方式可以驗證忍弛,消息轉(zhuǎn)發(fā)的方法有3個
步驟 | 方法 |
---|---|
快速轉(zhuǎn)發(fā) | forwardingTargetForSelector |
慢速轉(zhuǎn)發(fā) |
methodSignatureForSelector + forwardInvocation
|
綜上所述响迂,消息轉(zhuǎn)發(fā)整體的流程如下
3. 消息轉(zhuǎn)發(fā)之快速轉(zhuǎn)發(fā)
針對前面的崩潰問題,如果動態(tài)方法決議也沒有找到實現(xiàn)
细疚,則需要在LBHPerson中重寫forwardingTargetForSelector
方法蔗彤,將LBHPerson的實例方法的接收者指定為LBHStudent 的對象
(LBHStudent類中有instanceMethod1的具體實現(xiàn)),如下所示
//LBHPerson
@interface LBHPerson : NSObject
- (void)instanceMethod1;
@end
@implementation LBHPerson
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
// runtime + aSelector + addMethod + imp
//將消息的接收者指定為LBHStudent疯兼,在LBHStudent中查找instanceMethod1的實現(xiàn)
return [LBHStudent alloc];
}
@end
//LBHStudent
@interface LBHStudent : LBHPerson
@end
@implementation LBHStudent
- (void)instanceMethod1
{
NSLog(@"%s",__func__);
}
@end
運行結(jié)果
問題:
如果將LBHStudent
的instanceMethod1
方法注釋掉然遏,程序運行并不會崩潰注釋掉
LBHStudent
的instanceMethod1
方法,在forwardingTargetForSelector
打上斷點吧彪,發(fā)現(xiàn)程序在不停的執(zhí)行這個方法待侵,具體原因后續(xù)再去查找,不停的執(zhí)行這個方法是很不好的姨裸。
實際上這么寫是很雞肋的秧倾,除非把所有方法都寫在一個類中,顯然這是不現(xiàn)實的啦扬,而且指定的類中如果沒有這個方法的實現(xiàn)中狂,forwardingTargetForSelector
方法會一直被調(diào)用
將forwardingTargetForSelector
方法改一下
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"LBHPerson %s %@",__func__,NSStringFromSelector(aSelector));
// return [NSObject alloc];
return [super forwardingTargetForSelector:aSelector];
}
這么寫可以解決方法找不到而一直執(zhí)行forwardingTargetForSelector
,但是并不能解決崩潰扑毡,需要配合慢速轉(zhuǎn)發(fā)使用。
4. 消息轉(zhuǎn)發(fā)之慢速轉(zhuǎn)發(fā)
如果快速轉(zhuǎn)發(fā)中還是沒有找到
盛险,則進入最后的一次挽救機會瞄摊,即在LBHPerson中重寫methodSignatureForSelector
,如下所示
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
}
運行結(jié)果
當(dāng)然此時可以將快速轉(zhuǎn)發(fā)的代碼注釋掉
苦掘,只保留慢速轉(zhuǎn)發(fā)
也可以處理invocation
事務(wù)换帜,如下所示,修改invocation的target
為[LBHStudent alloc]
鹤啡,調(diào)用 [anInvocation invoke]
觸發(fā) LBHStudent
類的instanceMethod1
實例方法
不過實際開發(fā)中這么寫比較雞肋惯驼,有種畫蛇添足的感覺。