
本文主要講解消息轉(zhuǎn)發(fā),需要對 Class 的結(jié)構(gòu) selector,IMP,元類等概念有一點的了解,如果之前沒了解,最好先暫停去了解一下
我們在 iOS 開發(fā)過程中應(yīng)該都有碰到過這樣的錯:unrecognized selector sent to instance **,是因為我們調(diào)用了一個不存在的方法,用 OC 消息機制來說,消息的接收者的 methods 列表中找不到 selector 對應(yīng)的方法實現(xiàn),這樣就啟動消息轉(zhuǎn)發(fā)機制。但是 Objective-C 提供了3次機會讓我們?nèi)パa救 動態(tài)添加方法實現(xiàn),轉(zhuǎn)發(fā)方法 和 完整的消息轉(zhuǎn)發(fā)
首先寫一個 unrecognized selector sent to instance ** 例子,然后通過這三種方法分別一一解決:
// Person.h
@interface Person : NSObject
- (void)run;
@end
// Person.m
@implementation Person
@end
// main.m
int main(int argc, const char * argv[]) {
Person *p = [[Person alloc] init];
[p run];
return 0;
}
動態(tài)添加方法實現(xiàn)
對象在收到 unrecognized selector sent to instance ** 錯誤的時候,首先會調(diào)用 + (BOOL)resolveInstanceMethod:(SEL)sel 或則 + (BOOL)resolveClassMethod:(SEL)sel 詢問是否有動態(tài)添加的方法來處理異常
void dynamicRun(id self, SEL _cmd)
{
NSLog(@"這是c語言的run函數(shù) -- %@", self);
}
void dynamicBattle(id self, SEL _cmd)
{
NSLog(@"這是c語言的battle函數(shù) -- %@", self);
}
- (void)wd_run {
NSLog(@"running");
}
+ (void)wd_battle {
NSLog(@"Battle");
}
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel_isEqual(sel, NSSelectorFromString(@"run"))) {
// 添加OC方法
Method method = class_getInstanceMethod(self, @selector(wd_run));
IMP imp = method_getImplementation(method);
class_addMethod([self class], sel, imp, method_getTypeEncoding(method));
// 添加c函數(shù)
class_addMethod([self class], sel, (IMP)dynamicRun, "v@:");
return YES;
}
return NO;
}
+ (BOOL)resolveClassMethod:(SEL)sel {
if (sel_isEqual(sel, NSSelectorFromString(@"battle"))) {
// 獲取元類
Class metaClass = object_getClass(self);
// 添加OC方法
Method method = class_getClassMethod(self, @selector(wd_battle));
IMP imp = method_getImplementation(method);
class_addMethod(metaClass, sel, imp, method_getTypeEncoding(method));
// 添加c函數(shù)
class_addMethod(metaClass, sel, (IMP)dynamicBattle, "v@:");
return YES;
}
return NO;
}
當(dāng) Person 收到 未知選擇子 run 的時候,如果是實例方法,則會調(diào)用上文的 resolveInstanceMethod: 方法;如果是類方法,則會調(diào)用 resolveClassMethod: 方法。在方法內(nèi)部,我們可以通過 class_addMethod 方法動態(tài)添加一個 run 的實現(xiàn)方法來解決,并返回 YES,如果這一步不解決,則返回 NO,然后會進入第二步的 轉(zhuǎn)發(fā)
這里最值得注意的是,
resolveClassMethod內(nèi)部通過class_addMethod的時候是添加到 Person 的元類上的
轉(zhuǎn)發(fā)
如果在第一步的動態(tài)添加方法也沒解決的選擇子,會進入到這一步中,嘗試轉(zhuǎn)發(fā)給其他對象或類處理:
// Dog.h
@interface Dog : NSObject
- (void)run;
@end
// Dog.m
@implementation Dog
- (void)run {
NSLog(@"Dog running");
}
@end
// Person.m
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (sel_isEqual(aSelector, @selector(run))) {
return [[Dog alloc] init];
}
}
可能有些人有疑問了,為什么只有 forwardingTargetForSelector: 只有實例方法,沒有對應(yīng)的類方法呢?Objective-C 中的方法調(diào)用都是消息發(fā)送,并不沒有明確表示調(diào)用的是實例方法還是類方法,只和接收者有關(guān)。也就是說消息消息接收者是實例對象,那么調(diào)用的就是實例方法;如果消息接收者是類對象,那么調(diào)用的就是類方法。下面可以試試轉(zhuǎn)發(fā)類方法:
// Dog.h
+ (void)battle;
// Dog.m
+ (void)battle {
NSLog(@"Dog Battle");
}
// Person.m
+ (id)forwardingTargetForSelector:(SEL)aSelector {
if (sel_isEqual(aSelector, @selector(battle))) {
return [Dog class];
}
return [super forwardingTargetForSelector:aSelector];
}
沒報錯,控制臺也成功輸出了
Dog Battle
完整的消息轉(zhuǎn)發(fā)
如果第二步的 forwardingTargetForSelector 方法返回 nil,那么就會進入消息轉(zhuǎn)發(fā)的最后階段——完整的消息轉(zhuǎn)發(fā),這一步驟比較麻煩,需要實現(xiàn)2個方法: methodSignatureForSelector: 返回未知選擇子 run 的簽名; forwardInvocation: 拿到對應(yīng)的信息進行處理
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (sel_isEqual(aSelector, @selector(run))) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (sel_isEqual(anInvocation.selector, @selector(run))) {
Dog *d = [[Dog alloc] init];
[anInvocation invokeWithTarget:d];
}
}
這時控制臺會打印
Dog running
對應(yīng)的類方法的完整消息轉(zhuǎn)發(fā),和第二步一樣的,只需要將實例方法 - 改成類方法 + 就行了
+ (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (sel_isEqual(aSelector, @selector(battle))) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
+ (void)forwardInvocation:(NSInvocation *)anInvocation {
if (sel_isEqual(anInvocation.selector, @selector(battle))) {
[anInvocation invokeWithTarget:[Dog class]];
}
}
如果這三個步驟都沒實現(xiàn)的話,那么還是會調(diào)用 - (void)doesNotRecognizeSelector:(SEL)aSelector 這個方法報錯。當(dāng)然,線上環(huán)境肯定是不能出現(xiàn)此類問題的
最后附上用 Sketch 畫的流程圖:

其實之前有寫過消息轉(zhuǎn)發(fā)的文章,但是表達有點問題,就拖到了現(xiàn)在??