在前面的兩篇博客iOS原理探索08--objc_msgSend慢速查找流程分析和iOS原理探索07--objc_msgSend快速查找流程分析中大脉,我們知道方法的調(diào)用查找流程:先在緩存中進(jìn)行快速查找
朋譬,如果快速查找沒有找到
,那么會進(jìn)入慢速查找流
实抡,在方法的列表中進(jìn)行查找
肮帐,在這慢速查找流程結(jié)束后
纷责,沒有找到
的時候,會執(zhí)行一次動態(tài)決議方法
臣樱,如果動態(tài)決議
還沒有
找到靶擦,會進(jìn)行消息轉(zhuǎn)發(fā)
。如果消息轉(zhuǎn)發(fā)也沒有那么就會來到我們平時開發(fā)中的unrecognized selector sent to instance
報錯提示雇毫!
日常開發(fā)中沒有方法實(shí)現(xiàn)的報錯分析
- 條件設(shè)置:創(chuàng)建一個
LGPerson
類玄捕,添加一個+(void)sayNB
方法,但是并沒有實(shí)現(xiàn)該方法棚放;在main函數(shù)
中通過初始化一個LGPerson
類對象枚粘,調(diào)用sayNB
方法,運(yùn)行程序席吴。
可以看到赌结,這里已經(jīng)報錯了。下面可以跟進(jìn)源碼來看一下報錯的源代碼實(shí)現(xiàn)孝冒。
- 源代碼實(shí)現(xiàn)
根據(jù)慢速查找
發(fā)現(xiàn)柬姚,報錯都走到了根據(jù)慢速查找的源碼,我們發(fā)現(xiàn)庄涡,其報錯最后都是走到__objc_msgForward_impcache方法
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]
TailCallFunctionPointer x17
END_ENTRY __objc_msgForward
- 匯編實(shí)現(xiàn)中查找
__objc_forward_handler
量承,并沒有找到該方法的實(shí)現(xiàn),那么在源碼中去掉一個下劃線進(jìn)行全局搜索_objc_forward_handler
,最后發(fā)現(xiàn)默認(rèn)執(zhí)行的是objc_defaultForwardHandler
方法撕捍。下面是源碼實(shí)現(xiàn)拿穴。
// Default forward handler halts the process.
__attribute__((noreturn, cold)) 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;
打印輸出的就是如上方法為找到的錯誤提示。
動態(tài)決議
- 蘋果建議在慢速查找沒有找到方法實(shí)現(xiàn)的時候忧风,使用動態(tài)決議方法默色,可以算是補(bǔ)救崩潰的一個機(jī)會吧。
- 這個補(bǔ)救崩潰的機(jī)會就在
lookUpImpOrForward
方法中
//如果沒有找到方法實(shí)現(xiàn)狮腿,嘗試方法解析
if (slowpath(behavior & LOOKUP_RESOLVER)) {
//動態(tài)方法決議的控制條件腿宰,表示流程只走一次
behavior ^= LOOKUP_RESOLVER;
return resolveMethod_locked(inst, sel, cls, behavior);
}
-
resolveMethod_locked
源碼實(shí)現(xiàn)
static NEVER_INLINE IMP
resolveMethod_locked(id inst, SEL sel, Class cls, int behavior)
{
runtimeLock.assertLocked();
ASSERT(cls->isRealized());
runtimeLock.unlock();
//對象 -- 類
if (! cls->isMetaClass()) { //類不是元類,調(diào)用對象的解析方法
// try [cls resolveInstanceMethod:sel]
resolveInstanceMethod(inst, sel, cls);
}
else {//如果是元類缘厢,調(diào)用類的解析方法吃度, 類 -- 元類
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
resolveClassMethod(inst, sel, cls);
//為什么要有這行代碼? -- 類方法在元類中是對象方法贴硫,所以還是需要查詢元類中對象方法的動態(tài)方法決議
if (!lookUpImpOrNil(inst, sel, cls)) { //如果沒有找到或者為空椿每,在元類的對象方法解析方法中查找
resolveInstanceMethod(inst, sel, cls);
}
}
// chances are that calling the resolver have populated the cache
// so attempt using it
//如果方法解析中將其實(shí)現(xiàn)指向其他方法,則繼續(xù)走方法查找流程
return lookUpImpOrForward(inst, sel, cls, behavior | LOOKUP_CACHE);
}
所以如果沒有實(shí)現(xiàn)的方法英遭,我們可以通過resolveInstanceMethod
進(jìn)行一次補(bǔ)救间护,
- 主要流程分為以下幾個步驟
類不是元類,調(diào)用對象的解析方法贪绘,執(zhí)行的是
-(void) resolveInstanceMethod(id inst, SEL sel, Class cls)
方法兑牡;
如果是元類央碟,調(diào)用類的解析方法+ (BOOL)resolveClassMethod:(SEL)sel
税灌, 即類 -- 元類
-
分析流程圖
- 實(shí)例方法的動態(tài)決議
static void resolveInstanceMethod(id inst, SEL sel, Class cls)
{
runtimeLock.assertUnlocked();
ASSERT(cls->isRealized());
SEL resolve_sel = @selector(resolveInstanceMethod:);
// look的是 resolveInstanceMethod --相當(dāng)于是發(fā)送消息前的容錯處理
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); //發(fā)送resolve_sel消息
// Cache the result (good or bad) so the resolver doesn't fire next time.
// +resolveInstanceMethod adds to self a.k.a. cls
//查找say666
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));
}
}
}
主要分為以下幾個步驟:
在發(fā)送
resolveInstanceMethod
消息前,需要查找cls類
中是否有該方法的實(shí)現(xiàn)
亿虽,即通過lookUpImpOrNil方法
又會進(jìn)入lookUpImpOrForward慢速查找流程
查找resolveInstanceMethod方法
如果
沒有
菱涤,則直接返回
如果
有
,則發(fā)送resolveInstanceMethod消息
再次慢速查找
實(shí)例方法的實(shí)現(xiàn)洛勉,即通過lookUpImpOrNil
方法又會進(jìn)入lookUpImpOrForward
慢速查找流程查找實(shí)例方法實(shí)例方法示例:
say666
未實(shí)現(xiàn)的方法粘秆,通過resolveInstanceMethod
動態(tài)決議后由sayMaster
進(jìn)行實(shí)現(xiàn)。
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 來了", NSStringFromSelector(sel));
//獲取sayMaster方法的imp
IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
//獲取sayMaster的實(shí)例方法
Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
//獲取sayMaster的豐富簽名
const char *type = method_getTypeEncoding(sayMethod);
//將sel的實(shí)現(xiàn)指向sayMaster
return class_addMethod(self, sel, imp, type);
}
return [super resolveInstanceMethod:sel];
}
- 類方法的動態(tài)決議
解決未實(shí)現(xiàn)類方法調(diào)用崩潰問題收毫,比如上面的+(void)sayNB;
方法添加如下代碼
+ (BOOL)resolveClassMethod:(SEL)sel{
NSLog(@"%@ 來了",NSStringFromSelector(sel));
if (sel == @selector(sayNB)) {
IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
Method sayMMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
const char *type = method_getTypeEncoding(sayMMethod);
return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
}
return [super resolveClassMethod:sel];
}
LGPerson
打印了lgClassMethod
方法。注意:resolveClassMethod
類方法的重寫需要注意一點(diǎn)此再,傳入的cls不再是類
昔搂,而是元類
,可以通過objc_getMetaClass方法
獲取類的元類输拇,原因是因為類方法在元類中是實(shí)例方法
摘符。
動態(tài)決議相關(guān)優(yōu)化
我們通過繼承鏈可以知道,實(shí)例方法:類 -- 父類 -- 根類 -- nil
,類方法元類 -- 根元類 -- 根類 -- nil
逛裤。
那么我們可以將動態(tài)決議方法瘩绒,放在
NSObject的分類
里面,統(tǒng)一實(shí)現(xiàn)未實(shí)現(xiàn)的方法带族。當(dāng)然我們需要注意的是锁荔,這樣會攔截系統(tǒng)的方法,我們可以按照項目模塊針對自定義類中方法統(tǒng)一方法名的前綴
判斷蝙砌。
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(say666)) {
NSLog(@"%@ 來了", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(self, @selector(sayMaster));
Method sayMethod = class_getInstanceMethod(self, @selector(sayMaster));
const char *type = method_getTypeEncoding(sayMethod);
return class_addMethod(self, sel, imp, type);
}else if (sel == @selector(sayNB)) {
NSLog(@"%@ 來了", NSStringFromSelector(sel));
IMP imp = class_getMethodImplementation(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
Method lgClassMethod = class_getInstanceMethod(objc_getMetaClass("LGPerson"), @selector(lgClassMethod));
const char *type = method_getTypeEncoding(lgClassMethod);
return class_addMethod(objc_getMetaClass("LGPerson"), sel, imp, type);
}
return NO;
}
消息轉(zhuǎn)發(fā)
如果方法的快速查找堕战、慢速查找以及動態(tài)決議都找不到的情況下,就會進(jìn)行一個消息轉(zhuǎn)發(fā)拍霜,我們可以利用消息轉(zhuǎn)發(fā)來做一些操作避免出現(xiàn)崩潰嘱丢,這同樣是蘋果給我們的一個避免發(fā)生錯誤的機(jī)會。
- 通過
instrumentObjcMessageSends
的方式查看發(fā)送消息的日志
通過
lookUpImpOrForward --> log_and_fill_cache --> logMessageSend
,在logMessageSend
源碼下方找到instrumentObjcMessageSends
的源碼實(shí)現(xiàn)祠饺,所以越驻,在main
中調(diào)用instrumentObjcMessageSends
打印方法調(diào)用的日志信息,有以下兩點(diǎn)準(zhǔn)備工作
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 {
LGPerson *person = [LGPerson alloc];
instrumentObjcMessageSends(YES);
[person sayHello];
instrumentObjcMessageSends(NO);
NSLog(@"Hello, World!");
}
return 0;
}
可以根據(jù)源碼提供的路徑/tmp/msgSends
查看日志
bool logMessageSend(bool isClassMethod,
const char *objectsClass,
const char *implementingClass,
SEL selector)
{
char buf[ 1024 ];
// Create/open the log file
if (objcMsgLogFD == (-1))
{
snprintf (buf, sizeof(buf), "/tmp/msgSends-%d", (int) getpid ());
objcMsgLogFD = secure_open (buf, O_WRONLY | O_CREAT, geteuid());
if (objcMsgLogFD < 0) {
// no log file - disable logging
objcMsgLogEnabled = false;
objcMsgLogFD = -1;
return true;
}
}
// Make the log entry
snprintf(buf, sizeof(buf), "%c %s %s %s\n",
isClassMethod ? '+' : '-',
objectsClass,
implementingClass,
sel_getName(selector));
objcMsgLogLock.lock();
write (objcMsgLogFD, buf, strlen(buf));
objcMsgLogLock.unlock();
// Tell caller to not cache the method
return false;
}
運(yùn)行程序崩潰并巍,按照路徑可以找到如下文件
-
崩潰日志路徑
-
崩潰日志內(nèi)容
打開日志我們可以看到
- 兩次動態(tài)方法決議:
resolveInstanceMethod方法
- 兩次消息快速轉(zhuǎn)發(fā):
forwardingTargetForSelector方法
- 兩次消息慢速轉(zhuǎn)發(fā):
methodSignatureForSelector + resolveInstanceMethod
- 使用
bt
查看堆棧信息 - 發(fā)現(xiàn)
___forwarding___來自CoreFoundation
-
image list
讀取整個鏡像文件,然后搜索CoreFoundation
,查看其可執(zhí)行文件的路徑如下圖所示 - 根據(jù)
CoreFoundation
路徑找到該文件 - 通過反匯編工具
hopper
查看
搜索
__forwarding_prep_0___
方法换途,通過跳轉(zhuǎn)____forwarding___
懊渡,我們可以看到該方法的偽代碼了,如下所示
通過操作我們可以先判斷是否實(shí)現(xiàn)了forwardingTargetForSelector
方法军拟,如果沒有響應(yīng)剃执,跳轉(zhuǎn)至loc_64a67
也就是如果快速轉(zhuǎn)發(fā)沒有響應(yīng),則進(jìn)入慢速轉(zhuǎn)發(fā)流程懈息,查看是否實(shí)現(xiàn)methodSignatureForSelector
方法肾档,如果沒有響應(yīng),跳轉(zhuǎn)至loc_64e3
辫继,則直接報錯怒见,如果獲取methodSignatureForSelector
的方法簽名為nil
,也是直接報錯
如果methodSignatureForSelector
返回值不為空
姑宽,則在forwardInvocation
方法中對invocation
進(jìn)行處理
-
消息轉(zhuǎn)發(fā)機(jī)制流程圖
消息轉(zhuǎn)發(fā)的處理主要分為兩部分:
【快速轉(zhuǎn)發(fā)】當(dāng)慢速查找遣耍,以及動態(tài)方法決議均沒有找到實(shí)現(xiàn)時,進(jìn)行消息轉(zhuǎn)發(fā)低千,首先是進(jìn)行快速消息轉(zhuǎn)發(fā)配阵,即走forwardingTargetForSelector方法
如果返回消息接收者馏颂,在消息接收者中
還是沒有找到
,則進(jìn)入另一個
方法的查找流程
如果返回nil
棋傍,則進(jìn)入慢速消息轉(zhuǎn)發(fā)
救拉。
【慢速轉(zhuǎn)發(fā)】執(zhí)行到methodSignatureForSelector方法
,如果返回的方法簽名為nil
,則直接崩潰報錯
如果返回的方法簽名
不為nil
瘫拣,走到forwardInvocation
方法中亿絮,對invocation事務(wù)
進(jìn)行處理,如果不處理也不會報錯
總結(jié):消息轉(zhuǎn)發(fā)有三種
-
forwardingTargetForSelector
快速轉(zhuǎn)發(fā)
如果動態(tài)決議沒有找到方法麸拄,則需要在LGPerson中
重寫forwardingTargetForSelector
方法派昧,將LGPerson的實(shí)例方法
的接收者
指定為LGStudent的對象
,代碼如下
- (id)forwardingTargetForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
// runtime + aSelector + addMethod + imp
//將消息的接收者指定為LGStudent拢切,在LGStudent中查找say666的實(shí)現(xiàn)
return [LGStudent alloc];
}
輸出結(jié)果如下所示-
methodSignatureForSelector
和forwardInvocation
慢速轉(zhuǎn)發(fā)
如果快速轉(zhuǎn)發(fā)沒有找到方法的實(shí)現(xiàn)蒂萎,就會進(jìn)行慢速轉(zhuǎn)發(fā)流程,代碼如下
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"%s - %@",__func__,NSStringFromSelector(aSelector));
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"%s - %@",__func__,anInvocation);
// GM sayHello - anInvocation - 漂流瓶 - anInvocation
anInvocation.target = [LGStudent alloc];
// anInvocation 保存 - 方法
[anInvocation invoke];
}
輸出結(jié)果如下所示淮椰,并且發(fā)現(xiàn)forwardInvocation方法中
不對invocation
進(jìn)行處理五慈,也不會
崩潰報錯
。