代碼中我們難免會遇到未實現(xiàn)方法的情況(比如動使用方法名稱字符串冬天調(diào)用方法的情況),能不能避免unrecognized selector這種crash呢。答案當(dāng)然是肯定的,蘋果給我們提供了消息的動態(tài)轉(zhuǎn)發(fā)機(jī)制。下面我們來探究消息的轉(zhuǎn)發(fā)。
首先我們回到前面的lookUpImpOrForward方法。在方法查找過程中如果沒有找到方法的實現(xiàn),就會走到下面的一段代碼:
// No implementation found. Try method resolver once.
if (resolver && !triedResolver) {
runtimeLock.unlock();
_class_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.
triedResolver = YES;
goto retry;
}
其中調(diào)用了_class_resolveMethod方法,繼續(xù)跟蹤:
void _class_resolveMethod(Class cls, SEL sel, id inst)
{
if (! cls->isMetaClass()) {
// try [cls resolveInstanceMethod:sel]
_class_resolveInstanceMethod(cls, sel, inst);
}
else {
// try [nonMetaClass resolveClassMethod:sel]
// and [cls resolveInstanceMethod:sel]
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
_class_resolveInstanceMethod(cls, sel, inst);
}
}
}
如果不是元類,那么就是實例方法,否則就是類方法。
我們先進(jìn)入實例方法的流程查看:
static void _class_resolveInstanceMethod(Class cls, SEL sel, id inst)
{
if (! lookUpImpOrNil(cls->ISA(), SEL_resolveInstanceMethod, cls,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// Resolver not implemented.
return;
}
BOOL (*msg)(Class, SEL, SEL) = (typeof(msg))objc_msgSend;
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
IMP imp = lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/);
if (resolved && PrintResolving) {...}
}
我們看到以上代碼中調(diào)用了兩次lookUpImpOrNil。
第一次是針對resolveInstanceMethod,如果沒有實現(xiàn)就直接返回了,所以O(shè)C底層肯定給我們實現(xiàn)了該方法。我們在源碼中搜索到了該方法的默認(rèn)實現(xiàn):
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO;
}
然后下面給對象發(fā)送了resolveInstanceMethod消息,實際上此時resolveInstanceMethod方法已經(jīng)可以在緩存中查到了。
第二次是針對我們自己未實現(xiàn)的方法。此時又開始查找目標(biāo)方法的實現(xiàn)。 所以我們可以感覺到前面的resolveInstanceMethod方法應(yīng)該是為我們提供了實現(xiàn)方法的機(jī)會。我們只需要在自己的對象中去實現(xiàn)resolveInstanceMethod方法,然后在這個方法中動態(tài)的添加目標(biāo)方法的實現(xiàn)。這樣的話在下面的lookUpImpOrNil方法查找中就能找到我們動態(tài)添加的方法實現(xiàn)了。
我們在我們的對象中添加resolveInstanceMethod的實現(xiàn):
- (void)sayHello{
NSLog(@"%s",__func__);
}
+ (BOOL)resolveInstanceMethod:(SEL)sel{
if (sel == @selector(saySomething)) {
IMP sayHIMP = class_getMethodImplementation(self, @selector(sayHello));
Method sayHMethod = class_getInstanceMethod(self, @selector(sayHello));
const char *sayHType = method_getTypeEncoding(sayHMethod);
return class_addMethod(self, sel, sayHIMP, sayHType);
}
return [super resolveInstanceMethod:sel];
}
這樣處理后,假設(shè)我們未實現(xiàn)saySomething方法,然后調(diào)用了saySomething方法。就會調(diào)用sayHello方法,而不會直接unrecognized selector崩潰了。
上面我們講的是實例方法的動態(tài)決議,下面我們再看下類方法的動態(tài)決議。
在_class_resolveMethod方法中,如果是類方法的話,就會走下面的代碼:
_class_resolveClassMethod(cls, sel, inst);
if (!lookUpImpOrNil(cls, sel, inst,
NO/*initialize*/, YES/*cache*/, NO/*resolver*/))
{
// 對象方法 決議
_class_resolveInstanceMethod(cls, sel, inst);
}
其中_class_resolveClassMethod對應(yīng)著實例方法中的_class_resolveInstanceMethod。但是下面還可能會調(diào)用_class_resolveInstanceMethod。為什么類方法動態(tài)解析還會調(diào)用_class_resolveInstanceMethod呢?我們可以聯(lián)想下ISA的走位圖,類方法存在元類中,那么類方法的查找順序就:
元類---父元類---根元類---NSObject。所以不管是實例方法還是類方法,最后都會找到NSObject。系統(tǒng)可能是為了容錯處理,如果我們沒有在_class_resolveClassMethod方法中動態(tài)添加未實現(xiàn)的類方法,就會繼續(xù)走一遍_class_resolveInstanceMethod。
既然所有的方法(不管是實例方法還是類方法),如果未實現(xiàn)的話,都會走_class_resolveInstanceMethod方法。那么我們就可以在NSObject的該方法中做統(tǒng)一的處理。例如可以在此處做一個統(tǒng)一的容錯處理。比如我們可以在這里根據(jù)不同類別的方法(可以用方法前綴來區(qū)分),做出不同的容錯處理(可以跳轉(zhuǎn)到不同容錯界面)。但是在這里統(tǒng)一做容錯處理也有些弊端:
1、所以的處理都是在NSObject中統(tǒng)一處理,耦合度比較高;
2、如果具體的類實現(xiàn)了該方法,那么就會別攔截,走不到NSObject的這個方法了。
消息轉(zhuǎn)發(fā)
如果方法的動態(tài)解析沒有處理的話,就會進(jìn)入消息的轉(zhuǎn)發(fā)階段,消息轉(zhuǎn)發(fā)又分為快速轉(zhuǎn)發(fā)階段和慢速轉(zhuǎn)發(fā)階段。
1、快速轉(zhuǎn)發(fā):轉(zhuǎn)給另外一個對象去處理;
- (id)forwardingTargetForSelector:(SEL)aSelector{
if (aSelector == @selector(saySomething)) {
return [OtherObjc alloc];
}
return [super forwardingTargetForSelector:aSelector];
}
如果沒有實現(xiàn)方法的動態(tài)解析,接下來就會走到上面這個方法。這個方法需要返回一個對象,由該對象去實現(xiàn)該方法。這樣就會轉(zhuǎn)發(fā)給OtherObjc對象,去調(diào)用OtherObjc對象的saySomething方法。
@interface OtherObjc : NSObject
@end
@implementation OtherObjc
- (void)saySomething{
NSLog(@"%s",__func__);
}
@end
2、慢速轉(zhuǎn)發(fā):將所有未實現(xiàn)的方法封裝成NSInvocation,放在統(tǒng)一的一個地方,你可以選擇進(jìn)行處理,也可以選擇不處理。
首先我們需要實現(xiàn)methodSignatureForSelector,只有實現(xiàn)了該方法,并且在該方法中返回對應(yīng)的簽名,后面才會走forwardInvocation方法。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
if (aSelector == @selector(saySomething)) { // v @ :
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
上面的方法返回了v@:的方法簽名,該簽名表示方法返回值為void,并且有一個參數(shù)。
然后就會調(diào)用下面的方法
- (void)forwardInvocation:(NSInvocation *)anInvocation{
// 系統(tǒng)本質(zhì)
SEL aSelector = [anInvocation selector];
if ([[LGTeacher alloc] respondsToSelector:aSelector])
[anInvocation invokeWithTarget:[LGTeacher alloc]];
else
[super forwardInvocation:anInvocation];
}
我們通過Invocation可以獲取到方法的相關(guān)信息,然后針對不同方法進(jìn)行相應(yīng)的處理??梢詫⒉煌悇e的方法交于不同的對象去處理。如果在這里我們只是實現(xiàn)了forwardInvocation方法,但是沒有針對未實現(xiàn)的Selector做出相應(yīng)的處理。調(diào)用未實現(xiàn)的方法的時候,也是不會報錯“unRecognize selector”的。
疑問一
我們調(diào)用一個未實現(xiàn)的OC實例方法saySomething,可以通過在resolveInstanceMethod方法中添加實現(xiàn),從而實現(xiàn)OC方法的動態(tài)解析。該方法只會調(diào)用一次。
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"===== %s - %@",__func__,NSStringFromSelector(sel));
if (sel == @selector(saySomething)) {
NSLog(@"說話了");
IMP sayHIMP = class_getMethodImplementation(self, @selector(sayHello));
Method sayHMethod = class_getInstanceMethod(self, @selector(sayHello));
const char *sayHType = method_getTypeEncoding(sayHMethod);
return class_addMethod(self, sel, sayHIMP, sayHType);
}
return [super resolveInstanceMethod:sel];
}
但是如果實現(xiàn)了resolveInstanceMethod:方法。但是沒有在該方法中添加方法saySomething的實現(xiàn),就會調(diào)用兩次resolveInstanceMethod:方法,為什么呢?
resolveInstanceMethod:實現(xiàn)內(nèi)容如下
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"==== %s - %@",__func__,NSStringFromSelector(sel));
return [super resolveInstanceMethod:sel];
}
我們可以推理,如果在resolveInstanceMethod:方法中添加了方法saySomething實現(xiàn),然后就會返回YES,此時resolveInstanceMethod:調(diào)用了一次。而如果未添加方法saySomething實現(xiàn),就會第二次調(diào)用resolveInstanceMethod:,那么第二次的調(diào)動肯定是在后面的流程。后面的流程是什么呢?
首先下面調(diào)用了[super resolveInstanceMethod:sel];,我們可以跟蹤該方法的調(diào)用是在第二次調(diào)用的前面還是后面,經(jīng)過追蹤,是在第二次調(diào)用的前面。然后我們繼續(xù)追蹤第二次調(diào)用的時機(jī)。
下面的流程就是消息的動態(tài)轉(zhuǎn)發(fā)流程,
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"=====%s - %@",__func__,NSStringFromSelector(aSelector));
return [super forwardingTargetForSelector:aSelector];
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSLog(@"=====%s - %@",__func__,NSStringFromSelector(aSelector));
if (aSelector == @selector(saySomething)) { // v @ :
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSLog(@"=====%s - %@",__func__);
[super forwardInvocation:anInvocation];
}
使用以上代碼流程繼續(xù)跟蹤,發(fā)現(xiàn)第二次調(diào)用的時機(jī)位于methodSignatureForSelector和forwardInvocation之間。
然后再怎么追蹤第二次方法的調(diào)用呢?只能通過匯編了。我們運(yùn)行項目,此時會crash,因為我們此時我們沒有在動態(tài)解析中添加方法實現(xiàn)。crash后我們可以看到崩潰前的調(diào)用堆棧,類似下圖

然后我們就可以去右邊的匯編代碼中查找具體的匯編流程了。但是最后還是沒有找到有用的信息,只能猜測是因為沒有開源,所以編譯器隱藏了這部分實現(xiàn)。