最近在看消息轉發的資料,發現大部分都是理論知識,很少有完整的代碼。現在以代碼的形式形象的解釋一下:
用Xcode創建一個工程
1.正常方法調用
創建一個類Person 代碼如下
Person.h代碼如下:
#import <Foundation/Foundation.h>@interface Person : NSObject- (instancetype)init NS_UNAVAILABLE; - (instancetype)initWithUserName:(NSString *)userName;- (void)logUserName;@end
?
Person.m代碼如下:
#import "Person.h"@interface Person()@property (nonatomic, copy) NSString *userName;@end@implementation Person- (instancetype)initWithUserName:(NSString *)userName {self = [super init];if (self) {_userName = [userName copy];}return self; }- (void)logUserName {NSLog(@"userName = %@", self.userName); }@end
?
ViewController.m代碼如下:
#import "ViewController.h" #import "Person.h"@interface ViewController ()@end@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// Do any additional setup after loading the view, typically from a nib.Person *person = [[Person alloc] initWithUserName:@"小王"];[person logUserName]; }- (void)didReceiveMemoryWarning {[super didReceiveMemoryWarning];// Dispose of any resources that can be recreated. }@end
運行工程結果為:
2017-03-01 22:47:07.296 RunTimeDemo[24364:1776826] userName = 小王
結果正常
2. unrecognized selector 情況
把Person.m 的logUserName方法刪除,此時Person.m 的代碼如下:
#import "Person.h"@interface Person()@property (nonatomic, copy) NSString *userName;@end@implementation Person- (instancetype)initWithUserName:(NSString *)userName {self = [super init];if (self) {_userName = [userName copy];}return self; }@end
運行工程會出現 如下結果
2017-03-01 23:06:25.788 RunTimeDemo[24729:1788071] -[Person logUserName]: unrecognized selector sent to instance 0x608000018e00 2017-03-01 23:06:25.796 RunTimeDemo[24729:1788071] *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Person logUserName]: unrecognized selector sent to instance 0x608000018e00'
假如不在Person類中實現logUsrName這個方法,也可以讓工程正常運行,此時就涉及到消息轉發,Objective-C運行時會給出三次拯救程序崩潰的機會。
如下:
(1).Method resolution
objc運行時會調用+resolveInstanceMethod:或者 +resolveClassMethod:,讓你有機會提供一個函數實現。如果你添加了函數,那運行時系統就會重新啟動一次消息發送的過程,否則 ,運行時就會移到下一步,消息轉發(Message Forwarding)。
(2).Fast forwarding
如果目標對象實現了-forwardingTargetForSelector:,Runtime 這時就會調用這個方法,給你把這個消息轉發給其他對象的機會。 只要這個方法返回的不是nil和self,整個消息發送的過程就會被重啟,當然發送的對象會變成你返回的那個對象。否則,就會繼續Normal Fowarding。 這里叫Fast,只是為了區別下一步的轉發機制。因為這一步不會創建任何新的對象,但下一步轉發會創建一個NSInvocation對象,所以相對更快點。
(3).Normal forwarding
這一步是Runtime最后一次給你挽救的機會。首先它會發送-methodSignatureForSelector:消息獲得函數的參數和返回值類型。如果-methodSignatureForSelector:返回nil,Runtime則會發出-doesNotRecognizeSelector:消息,程序這時也就掛掉了。如果返回了一個函數簽名,Runtime就會創建一個NSInvocation對象并發送-forwardInvocation:消息給目標對象。
?
下面就驗證一下上面所對應的函數的執行順序:
Person.m的代碼如下:
#import "Person.h"#define LOG_ClassAndFunctionName NSLog(@"%@ %s", [self class], __func__)@interface Person()@property (nonatomic, copy) NSString *userName;@end@implementation Person- (instancetype)initWithUserName:(NSString *)userName {self = [super init];if (self) {_userName = [userName copy];}return self; }+ (BOOL)resolveInstanceMethod:(SEL)sel {LOG_ClassAndFunctionName;return [super resolveInstanceMethod:sel]; }- (id)forwardingTargetForSelector:(SEL)aSelector {LOG_ClassAndFunctionName;return [super forwardingTargetForSelector:aSelector]; }- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {LOG_ClassAndFunctionName;return [super methodSignatureForSelector:aSelector]; }- (void)forwardInvocation:(NSInvocation *)anInvocation {LOG_ClassAndFunctionName;[super forwardInvocation:anInvocation];}- (void)doesNotRecognizeSelector:(SEL)aSelector {LOG_ClassAndFunctionName;[super doesNotRecognizeSelector:aSelector]; }@end
?
運行結果為:
2017-03-02 10:16:37.855 RunTimeDemo[35098:2456660] Person +[Person resolveInstanceMethod:] 2017-03-02 10:16:37.856 RunTimeDemo[35098:2456660] Person -[Person forwardingTargetForSelector:] 2017-03-02 10:16:37.856 RunTimeDemo[35098:2456660] Person -[Person methodSignatureForSelector:] 2017-03-02 10:16:37.857 RunTimeDemo[35098:2456660] Person +[Person resolveInstanceMethod:] 2017-03-02 10:16:37.858 RunTimeDemo[35098:2456660] Person -[Person doesNotRecognizeSelector:] 2017-03-02 10:16:37.859 RunTimeDemo[35098:2456660] -[Person logUserName]: unrecognized selector sent to instance 0x600000003d40
?
運行結果正好驗證了上述說明, 最后找不到實現會調用doesNotRecognizeSelector方法
那如何做才能不讓其Crash呢?
做法是重寫剛才說的那幾個方法,那我們就倒著重寫上面的方法。
3.重寫方法來拯救Crash
1.在methodSignatureForSelector方法攔截 新建一個類ForwardClass,代碼如下,具體看注釋
#import "Person.h"#define LOG_ClassAndFunctionName NSLog(@"%@ %s", [self class], __func__)@interface ForwardClass : NSObject- (void)logUserName;@end@implementation ForwardClass- (void)forwardLogUserName:(NSString *)userName; {LOG_ClassAndFunctionName;NSLog(@"userName = %@",userName); }@end@interface Person()@property (nonatomic, copy) NSString *userName; @property (nonatomic, strong) ForwardClass *forwardClass;@end@implementation Person- (instancetype)initWithUserName:(NSString *)userName {self = [super init];if (self) {_userName = [userName copy];_forwardClass = [[ForwardClass alloc] init];}return self; }+ (BOOL)resolveInstanceMethod:(SEL)sel {LOG_ClassAndFunctionName;return [super resolveInstanceMethod:sel]; }- (id)forwardingTargetForSelector:(SEL)aSelector {LOG_ClassAndFunctionName;return [super forwardingTargetForSelector:aSelector]; }- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {LOG_ClassAndFunctionName;NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];if (!signature) {/// 方法簽名為nil,就生成一個forwardClass的方法簽名并制定selectorsignature = [self.forwardClass methodSignatureForSelector:@selector(forwardLogUserName:)];}return signature; }- (void)forwardInvocation:(NSInvocation *)anInvocation {LOG_ClassAndFunctionName;/// 重定向方法 [anInvocation setSelector:@selector(forwardLogUserName:)];/// 假如調用的函數需要參數的話,在這里調用anInvocation的setArgument:atIndex:方法來進行設置; 指定參數,以指針方式,并且第一個參數的起始index是2,因為index為1,2的分別是self和selectorNSString *userName = self.userName;[anInvocation setArgument:&userName atIndex:2];/// ForwardClass 接受轉發的消息 [anInvocation invokeWithTarget:[[ForwardClass alloc] init]]; }- (void)doesNotRecognizeSelector:(SEL)aSelector {LOG_ClassAndFunctionName; }@end
?
運行結果:
2017-03-02 10:47:14.229 RunTimeDemo[36473:2479172] Person +[Person resolveInstanceMethod:] 2017-03-02 10:47:14.229 RunTimeDemo[36473:2479172] Person -[Person forwardingTargetForSelector:] 2017-03-02 10:47:14.229 RunTimeDemo[36473:2479172] Person -[Person methodSignatureForSelector:] 2017-03-02 10:47:14.230 RunTimeDemo[36473:2479172] Person +[Person resolveInstanceMethod:] 2017-03-02 10:47:14.230 RunTimeDemo[36473:2479172] Person -[Person forwardInvocation:] 2017-03-02 10:47:14.230 RunTimeDemo[36473:2479172] ForwardClass -[ForwardClass forwardLogUserName:] 2017-03-02 10:47:14.230 RunTimeDemo[36473:2479172] userName = 小王
此時不Crash了,我們讓其執行了forwardClass 中的forwardLogUserName方法。
2.在forwardingTargetForSelector處攔截,轉發給其他的類,執行對應的方法。代碼如下
#import "Person.h"#define LOG_ClassAndFunctionName NSLog(@"%@ %s", [self class], __func__)@interface ForwardClass : NSObject- (void)logUserName;@end@implementation ForwardClass- (void)forwardLogUserName:(NSString *)userName; {LOG_ClassAndFunctionName;NSLog(@"userName = %@",userName); }- (void)logUserName {LOG_ClassAndFunctionName; }@end@interface Person()@property (nonatomic, copy) NSString *userName; @property (nonatomic, strong) ForwardClass *forwardClass;@end@implementation Person- (instancetype)initWithUserName:(NSString *)userName {self = [super init];if (self) {_userName = [userName copy];_forwardClass = [[ForwardClass alloc] init];}return self; }+ (BOOL)resolveInstanceMethod:(SEL)sel {LOG_ClassAndFunctionName;return [super resolveInstanceMethod:sel]; }- (id)forwardingTargetForSelector:(SEL)aSelector {LOG_ClassAndFunctionName;id fowarding = [super forwardingTargetForSelector:aSelector];if (!fowarding) {/// 進行攔截,讓ForwardClass的示例進行執行aSelector方法fowarding = [[ForwardClass alloc] init];}return fowarding; }- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {LOG_ClassAndFunctionName;NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];if (!signature) {/// 方法簽名為nil,就生成一個forwardClass的方法簽名并制定selectorsignature = [self.forwardClass methodSignatureForSelector:@selector(forwardLogUserName:)];}return signature; }- (void)forwardInvocation:(NSInvocation *)anInvocation {LOG_ClassAndFunctionName;/// 重定向方法 [anInvocation setSelector:@selector(forwardLogUserName:)];/// 假如調用的函數需要參數的話,在這里調用anInvocation的setArgument:atIndex:方法來進行設置; 指定參數,以指針方式,并且第一個參數的起始index是2,因為index為1,2的分別是self和selectorNSString *userName = self.userName;[anInvocation setArgument:&userName atIndex:2];/// ForwardClass 接受轉發的消息 [anInvocation invokeWithTarget:[[ForwardClass alloc] init]]; }- (void)doesNotRecognizeSelector:(SEL)aSelector {LOG_ClassAndFunctionName; }@end
?
運行結果為:
2017-03-02 12:35:02.033 RunTimeDemo[41433:2552894] Person +[Person resolveInstanceMethod:] 2017-03-02 12:35:02.033 RunTimeDemo[41433:2552894] Person -[Person forwardingTargetForSelector:] 2017-03-02 12:35:02.034 RunTimeDemo[41433:2552894] ForwardClass -[ForwardClass logUserName]
3.在resolveInstanceMethod處攔截,動態的為類添加相應的方法。代碼如下:
#import "Person.h" #import <objc/runtime.h>#define LOG_ClassAndFunctionName NSLog(@"%@ %s", [self class], __func__)@interface ForwardClass : NSObject- (void)logUserName;@end@implementation ForwardClass- (void)forwardLogUserName:(NSString *)userName; {LOG_ClassAndFunctionName;NSLog(@"userName = %@",userName); }- (void)logUserName {LOG_ClassAndFunctionName; }@end@interface Person()@property (nonatomic, copy) NSString *userName; @property (nonatomic, strong) ForwardClass *forwardClass;@end@implementation Personvoid dynamicMethodIMP(id self, SEL _cmd) {LOG_ClassAndFunctionName; }- (instancetype)initWithUserName:(NSString *)userName {self = [super init];if (self) {_userName = [userName copy];_forwardClass = [[ForwardClass alloc] init];}return self; }+ (BOOL)resolveInstanceMethod:(SEL)sel {LOG_ClassAndFunctionName;if (sel == @selector(logUserName)) {/// 動態的為這個類去添加方法class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");return YES;}return [super resolveInstanceMethod:sel]; }- (id)forwardingTargetForSelector:(SEL)aSelector {LOG_ClassAndFunctionName;id fowarding = [super forwardingTargetForSelector:aSelector];if (!fowarding) {/// 進行攔截,讓ForwardClass的示例進行執行aSelector方法fowarding = [[ForwardClass alloc] init];}return fowarding; }- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {LOG_ClassAndFunctionName;NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];if (!signature) {/// 方法簽名為nil,就生成一個forwardClass的方法簽名并制定selectorsignature = [self.forwardClass methodSignatureForSelector:@selector(forwardLogUserName:)];}return signature; }- (void)forwardInvocation:(NSInvocation *)anInvocation {LOG_ClassAndFunctionName;/// 重定向方法 [anInvocation setSelector:@selector(forwardLogUserName:)];/// 假如調用的函數需要參數的話,在這里調用anInvocation的setArgument:atIndex:方法來進行設置; 指定參數,以指針方式,并且第一個參數的起始index是2,因為index為1,2的分別是self和selectorNSString *userName = self.userName;[anInvocation setArgument:&userName atIndex:2];/// ForwardClass 接受轉發的消息 [anInvocation invokeWithTarget:[[ForwardClass alloc] init]]; }- (void)doesNotRecognizeSelector:(SEL)aSelector {LOG_ClassAndFunctionName; }@end
運行結果為:
2017-03-02 12:44:24.529 RunTimeDemo[41577:2561853] Person +[Person resolveInstanceMethod:] 2017-03-02 12:44:24.530 RunTimeDemo[41577:2561853] Person dynamicMethodIMP
總結:上面三次攔截更加形象的說明消息轉發進行的次序。若某個類的實例調用一個沒有實現的方法后。首先會調用
resolveInstanceMethod/resolveClassMethod方法,若沒有添加相應的方法,其次就會調用forwardingTargetForSelector方法,若沒有轉發給其他對象,最后就調用methodSignatureForSelector方法,若方法簽名為nil,Runtime則會發出-doesNotRecognizeSelector:消息。此時就Crash了。
上面的工程鏈接為:?https://github.com/lidaojian/RunTimeDemo
?
================================================================
若有疑問請加本人QQ:610774281 微信:stephenli225。 一起探討一起進步。。。。