當(dāng)我們發(fā)送的消息,在消息接受者以及它的整個(gè)繼承鏈緩存(Cache)和方法列表(methodList)中都找不到時(shí),系統(tǒng)給我們留了崩潰前的最后一次挽救機(jī)會(huì)。
本節(jié)將探索方法找不到時(shí)最后的挽救機(jī)會(huì) - 消息轉(zhuǎn)發(fā)機(jī)制。
1. 前期準(zhǔn)備
2. 動(dòng)態(tài)方法決議
3. 消息轉(zhuǎn)發(fā)
4. 異常消息處理流程圖
1.前期準(zhǔn)備
打開objc4源文件,在main.m中加入測(cè)試代碼:
@interface HTPerson : NSObject
- (void)sayHello;
+ (void)say666;
@end
@implementation HTPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [HTPerson alloc];
[person sayHello];
}
return 0;
}
sayHello方法不實(shí)現(xiàn)。模擬方法找不到的場(chǎng)景。
- 熟悉上兩節(jié)快速查找和慢速查找內(nèi)容,知道
消息在匯編層cache中高速查找,找不到imp后,會(huì)進(jìn)入C/C++混編層,調(diào)用lookUpImpOrForward函數(shù)。在查詢cls接收者和cls的整個(gè)繼承鏈對(duì)象的cache和methodlist,都找不到對(duì)應(yīng)的imp時(shí)。我們會(huì)進(jìn)入動(dòng)態(tài)方法決議 resolveMethod_locked。
if (slowpath(behavior & LOOKUP_RESOLVER)) {
behavior ^= LOOKUP_RESOLVER; // behavior取反后,下一次進(jìn)入判斷時(shí),if條件就不成立了。 確保動(dòng)態(tài)決議只執(zhí)行一次
return resolveMethod_locked(inst, sel, cls, behavior);
}
補(bǔ)充
lookUpImpOrForward的入?yún)⒗斫?/p>IMP lookUpImpOrForward(id inst, SEL sel, Class cls, int behavior)
inst:真實(shí)持有sel方法的類(對(duì)象方法存放在類中,類方法存放在元類中)
如對(duì)象方法: [person sayHello],inst是HTPerson。
類方法: [HTPerson say666],inst也是HTPerson元類
sel:方法名
cls:方法接受者的所屬類
如對(duì)象方法: [person sayHello],cls是HTPerson
類方法: [HTPerson say666],cls也是HTPerson
behavior:行為參數(shù), 影響進(jìn)入動(dòng)態(tài)決議等判斷條件。
2. 動(dòng)態(tài)方法決議
- 進(jìn)入
resolveMethod_locked
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
// 對(duì)象方法
if (! cls->isMetaClass()) {
resolveInstanceMethod(inst, sel, cls);
}
// 類方法
else {
resolveClassMethod(inst, sel, cls);
if (!lookUpImpOrNil(inst, sel, cls)) {
resolveInstanceMethod(inst, sel, cls);
}
}
// 重新查詢一次
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
判斷是cls是元類還是本類:
如果
不是元類,就是對(duì)象方法,直接調(diào)用resolveInstanceMethod-
否則,就是
類方法,先調(diào)用resolveClassMethod,再調(diào)用lookUpImpOrNil檢查是否找到imp,沒找到的話再調(diào)用resolveInstanceMethod。意思是,如果
類方法中沿著元類繼承鏈找到NSObject元類了都沒找到,再找就是NSObjet本類了,NSObjet本類存的是對(duì)象方法,所以要用resolveInstanceMethod。
最后再調(diào)用lookUpImpOrForward重新查詢一次。
分析resolveInstanceMethod:
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
// 1. 查找類對(duì)象的元類中是否有`resolveInstanceMethod`的imp。
// (根元類中默認(rèn)實(shí)現(xiàn)了`resolveInstanceMethod`方法,所以永遠(yuǎn)不會(huì)return)
if (!lookUpImpOrNil(cls, resolve_sel, cls->ISA())) {
return;
}
// 2. 調(diào)用一次`resolveInstanceMethod`函數(shù)
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
bool resolved = msg(cls, resolve_sel, sel);
// 3. 再搜索一次sel的imp
//(如果在上面resolveInstanceMethod函數(shù)實(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));
}
}
}
了解下
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); }內(nèi)部繼續(xù)調(diào)用了
lookUpImpOrForward函數(shù),不同的是,behavior變成了 0 | 4 | 8 = 12。 >這決定了進(jìn)入lookUpImpOrForward后:
fastpath(behavior & LOOKUP_CACHE)= 12 & 4 = 4,條件成立,會(huì)優(yōu)先cache_getImp讀取一次緩存slowpath(behavior & LOOKUP_RESOLVER)= 12 & 2 = 0,不成立,不會(huì)進(jìn)入resolveMethod_locked動(dòng)態(tài)方法決議。
lookUpImpOrNil中的lookUpImpOrForward會(huì)循環(huán)遍歷cls繼承鏈的所有類的cache和methodList來(lái)尋找imp
所以resolveInstanceMethod函數(shù),就是系統(tǒng)給開發(fā)者的一次機(jī)會(huì)。
- 你可以在合適的地方加上
resolveInstanceMethod函數(shù)。在函數(shù)內(nèi)部把sel實(shí)現(xiàn)
- 你可以在合適的地方加上
- 系統(tǒng)會(huì)發(fā)送消息,調(diào)用一次
resolveInstanceMethod函數(shù)
- 系統(tǒng)會(huì)發(fā)送消息,調(diào)用一次
- 再次循環(huán)查找
sel,如果在上面resolveInstanceMethod函數(shù)實(shí)現(xiàn)了sel,就可以拿到imp,并將sel和imp寫入cls的緩存中。
- 再次循環(huán)查找
系統(tǒng)大哥用大白話跟你說(shuō):老弟,我檢查到你的
方法沒有實(shí)現(xiàn),我現(xiàn)在給你一次機(jī)會(huì),你識(shí)趣的話,就在我調(diào)用resolveInstanceMethod函數(shù)之前,在這函數(shù)內(nèi)部把你的sel實(shí)現(xiàn)了。等到我調(diào)用完后,你還是沒實(shí)現(xiàn)的話,我就崩了你。
案例:
- 授人以漁
- 分析
imp實(shí)現(xiàn)位置。
- 如:
[person sayHello]是對(duì)象方法,對(duì)象方法是存放在本類中。[HTPerson say666]是類方法,類方法是存放在元類中。
imp寫哪里?
- NSObject是所有類的父類,NSObject中有
resolveInstanceMethod類方法。所以我們可以在繼承鏈的某個(gè)合適的類中重寫resolveInstanceMethod方法。在這個(gè)方法內(nèi)部,實(shí)現(xiàn)imp。
- 授人以魚
#import <Foundation/Foundation.h>
#import <objc/message.h>
@interface BaseObject: NSObject
@end
@implementation BaseObject
- (void)handleErrorImp {
NSLog(@"我?guī)湍銚踝”罎⒘耍蠄?bào)數(shù)據(jù)給后臺(tái)還是咋操作,你自己想");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(sayHello)) {
NSLog(@"???????? %@ 沒實(shí)現(xiàn)!!????????", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(self, @selector(handleErrorImp));
Method method = class_getInstanceMethod(self, @selector(handleErrorImp));
const char * type = method_getTypeEncoding(method);
// 將imp加入當(dāng)前類。
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod: sel];
}
@end
@interface HTPerson : BaseObject
- (void)sayHello;
@end
@implementation HTPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [HTPerson alloc];
[person sayHello];
}
return 0;
}
-
HTPerson繼承自我們自定義的基類BaseObject,HTPerson中的sayHello沒實(shí)現(xiàn),我們?cè)诨愔屑尤霐r截。將臨時(shí)imp和sel寫入基類函數(shù)列表中。成功防止了崩潰。
具體操作時(shí),我們并不知道sel是哪個(gè),按理說(shuō)進(jìn)入resolveInstanceMethod的所有sel都是找不到imp的。我們可以統(tǒng)一攔截進(jìn)行數(shù)據(jù)上報(bào),然后做防崩潰處理。
(比如個(gè)人中心某個(gè)深層頁(yè)面的函數(shù)未實(shí)現(xiàn),我們直接統(tǒng)一返回個(gè)人中心,防止崩潰,同時(shí)將這次找不到的記錄上傳給后臺(tái),讓相應(yīng)的程序員哥哥下個(gè)版本修復(fù)它)但
實(shí)際開發(fā)中,你不確定是否有人在繼承鏈上游就使用了resolveInstanceMethod黑魔法。這樣你的resolveInstanceMethod就被重寫?? 畢竟牛逼的人很多。
這是打印結(jié)果:

-
厲害的人很多,大家都知道用
resolveInstanceMethod。那有沒有更厲害一點(diǎn)的呢?
------------------------------ 來(lái)了,老弟 ?? ------------------------------
3. 消息轉(zhuǎn)發(fā)
當(dāng)我們?cè)?code>動(dòng)態(tài)方法決議中不做任何處理,在崩潰前,系統(tǒng)還有2個(gè)隱藏的挽救點(diǎn)。快速轉(zhuǎn)發(fā)forwardingTargetForSelector 和慢速轉(zhuǎn)發(fā)methodSignatureForSelector
在正式介紹這2個(gè)方法前,我們應(yīng)該知道如何發(fā)現(xiàn)他們的。
方法1: 日志查看
在lookUpImpOrForward中log_and_fill_cache函數(shù)中:
static void
log_and_fill_cache(Class cls, IMP imp, SEL sel, id receiver, Class implementer)
{
#if SUPPORT_MESSAGE_LOGGING
if (slowpath(objcMsgLogEnabled && implementer)) {
bool cacheIt = logMessageSend(implementer->isMetaClass(),
cls->nameForLogging(),
implementer->nameForLogging(),
sel);
if (!cacheIt) return;
}
#endif
cache_fill(cls, sel, imp, receiver);
}
我們看到在cache_fill寫入操作前,會(huì)檢查SUPPORT_MESSAGE_LOGGING是否支持消息記錄。
進(jìn)入logMessageSend函數(shù)內(nèi)部,發(fā)現(xiàn)日志存放路徑在/tmp文件夾:

我們打開任意一個(gè)文件夾,按Command + Shift + G,輸入/tmp文件夾查看。發(fā)現(xiàn)并沒有msgSends名字的文件。
- 因?yàn)?code>系統(tǒng)默認(rèn)是
關(guān)閉日志功能,objcMsgLogEnabled默認(rèn)為false。

文件內(nèi)搜索objcMsgLogEnabled,發(fā)現(xiàn)賦值操作是在instrumentObjcMessageSends中。

所以我們?cè)?code>崩潰前,手動(dòng)調(diào)用instrumentObjcMessageSends進(jìn)行日志的開啟和關(guān)閉:
- 測(cè)試代碼:
注意,這次不是在源碼環(huán)境。而是在任意一個(gè)正常項(xiàng)目中進(jìn)行測(cè)試。
// extern: 聲明當(dāng)前變量或函數(shù)在別的文件中定義了。不要報(bào)錯(cuò)。
extern void instrumentObjcMessageSends(BOOL flag);
@interface HTPerson : NSObject
- (void)sayHello;
@end
@implementation HTPerson
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
HTPerson * person = [HTPerson alloc];
instrumentObjcMessageSends(YES); // 打開記錄
[person sayHello];
instrumentObjcMessageSends(NO); // 關(guān)閉記錄
}
return 0;
}
運(yùn)行程序,crash后,在/tmp文件夾中看不到msgSends-92567文件

打開msgSends-92567文件,發(fā)現(xiàn)最上面的日志記錄為:
+ HTPerson NSObject resolveInstanceMethod:
+ HTPerson NSObject resolveInstanceMethod:
- HTPerson NSObject forwardingTargetForSelector:
- HTPerson NSObject forwardingTargetForSelector:
- HTPerson NSObject methodSignatureForSelector:
- HTPerson NSObject methodSignatureForSelector:
- HTPerson NSObject class
+ HTPerson NSObject resolveInstanceMethod:
+ HTPerson NSObject resolveInstanceMethod:
- HTPerson NSObject doesNotRecognizeSelector:
- HTPerson NSObject doesNotRecognizeSelector:
- HTPerson NSObject class
這是崩潰前調(diào)用的函數(shù)記錄。
我們發(fā)現(xiàn)調(diào)用者是HTPerson,我們直接在HTPerson中加入類似的方法進(jìn)行實(shí)現(xiàn)。
- 在
HTPerosn實(shí)現(xiàn)中打印這四個(gè)函數(shù)
@implementation HTPerosn
+ (BOOL)resolveInstanceMethod:(SEL)sel {
NSLog(@"%s %@", __func__ ,NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector));
return [super methodSignatureForSelector:aSelector];
}
-(void)doesNotRecognizeSelector:(SEL)aSelector {
NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector));
return [super doesNotRecognizeSelector:aSelector];
}
@end

- 打印的
順序和日志一樣,那這四個(gè)函數(shù)有什么用呢?
我們知道,
resolveInstanceMethod動(dòng)態(tài)方法決議是系統(tǒng)給開發(fā)者留的最后一次機(jī)會(huì)。
系統(tǒng)希望開發(fā)者在resolveInstanceMethod中將未實(shí)現(xiàn)的sel完成實(shí)現(xiàn)。并且他會(huì)在給完機(jī)會(huì)后重新執(zhí)行一次lookUpImpOrForward。打印日志中有2次
resolveInstanceMethod,如果我們沒有在resolveInstanceMethod中修改。 那么我們可以在第二次resolveInstanceMethod前的2個(gè)方法進(jìn)行操作。所以我們可以在
forwardingTargetForSelector和methodSignatureForSelector方法里做補(bǔ)救。
大家都知道改resolveInstanceMethod,那就讓你們隨意發(fā)揮。大佬在你們resolveInstanceMethod后面阻擊一切漏網(wǎng)之魚。??
現(xiàn)在,我們來(lái)了解大佬的法器
法器一:快速轉(zhuǎn)發(fā)forwardingTargetForSelector
-
官方文檔:
forwardingTargetForSelector
意思是:aSelector是沒實(shí)現(xiàn)的選擇器,我找不到實(shí)現(xiàn)的對(duì)象
- 既然你
找不到實(shí)現(xiàn)的對(duì)象,那我就給你個(gè)實(shí)現(xiàn)這個(gè)方法的對(duì)象
案例:

在第一次resolveInstanceMethod之后,成功替換了實(shí)現(xiàn)方法,并攔截了崩潰。
法器二:慢速轉(zhuǎn)發(fā)methodSignatureForSelector


意思是:返回一個(gè)NSMethodSignature函數(shù)簽名對(duì)象,Discussion中說(shuō)了需要實(shí)現(xiàn)協(xié)議。搭配forwardinvocation函數(shù)使用。

點(diǎn)擊進(jìn)入NSInvocation查看格式:

我們打印invocation看看

invocation就是一個(gè)漂流瓶,只要methodSignatureForSelector返回不為nil,且實(shí)現(xiàn)了forwardinvocation函數(shù),就一定不會(huì)崩潰。系統(tǒng)會(huì)把這個(gè)invocatio當(dāng)做一個(gè)漂流瓶,拋棄它了。誰(shuí)想處理誰(shuí)處理,沒人處理我也不管了invocation可以修改target接收對(duì)象和替換selector??刹僮餍钥臻g更大。invocation需要調(diào)用invoke對(duì)象方法執(zhí)行([invocation invoke])必要時(shí)可以保存
invocation,修改完參數(shù)再調(diào)用invoke執(zhí)行。
例如:
- 在
HTStudent實(shí)現(xiàn)一個(gè)sayNB,然后修改anInvocation的target和selector。- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { NSLog(@"%s %@", __func__ ,NSStringFromSelector(aSelector)); return [NSMethodSignature signatureWithObjCTypes:"v@:"]; } -(void)forwardInvocation:(NSInvocation *)anInvocation { anInvocation.target = [HTStudent alloc]; anInvocation.selector = @selector(sayNB); [anInvocation invoke]; }
4. 異常消息處理流程圖

