前言:最近一直在研讀《Effective Objective-C 2.0》這篇文章,覺得受益匪淺。自己將書上的代碼進行實現,再結合作者的解釋進行思路擴展之后,對oc語言機制有了更深的理解;
這篇文章結合了自身的理解,盡量更清楚明白的闡述消息轉發機制的實現過程和原理,如有闡述不當的地方,歡迎指導改正。
什么是消息轉發?
oc中方法調用就是一個消息傳遞的過程。例如:
[self testMethodWithInfo:info];
本例中,self被稱為“接收者”,testMethodWithInfo:被稱為選擇器,info為參數,選擇器和參數合起來稱為“消息”。
但是,在編譯期像對象發送了其無法解讀的消息之后,編譯器并不會報錯,只會給出一個方法名未知的警告:
這是因為在運行期可以繼續向類中添加方法,所以編譯器在編譯時還無法確定類中到底會不會有某個方法實現。當對象接收到無法解讀的消息后,就會啟動“消息轉發”機制,我們也可以經由此過程老告訴對象應該如何處理未知消息。
消息轉發機制的實現過程:
消息轉發分為兩大階段。第一個階段先征詢接收者所屬的類,看其是否能動態添加方法,以處理當前這個“未知的選擇器”,這叫做“動態方法解析”。第二個階段涉及“完整的消息轉發機制”。如果運行期征詢結果接收者沒能動態添加方法以響應包含該選擇器的消息,此時系統會請求接收者以其他手段來處理與消息相關的方法調用。首先,請接收者看看有沒有其他對象能處理這條消息,如果有,系統會把消息轉發給那個對象,消息轉發過程結束。如果沒有“備援的接受者”,則啟動完整的消息轉發機制,系統會把與消息相關的全部細節都封裝在NSInvocation對象中,再給接收者最后一次機會,令其設法解決當前還未處理的這條消息。
動態方法解析:
對象在收到無法解讀的消息后,首先會調用所屬類的下列類方法:
+ (BOOL)resolveInstanceMethod:(SEL)sel
參數sel就是未知的選擇器,返回值BOOL類型,表示這個類能否新增一個實例方法以處理此選擇器。假如將要實現的不是實例方法而是類方法,系統則會調用另外一個方法:
+ (BOOL)resolveClassMethod:(SEL)sel
使用這種辦法的前提是:相關方法的實現代碼已經寫好(否則識別不到新添加的方法名),只等著運行的時候動態插入到類里面就可以了。
演示代碼:
先實現替代方法:
#import "StudentModel.h"
void nullMethodSubstite(id self, SEL _cmd){
NSLog(@"message has transpond");
};
然后在+ (BOOL)resolveInstanceMethod:(SEL)sel方法中進行處理:
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSString *str = NSStringFromSelector(sel);
if ([str containsString:@"nullMethod"]) {
/**
通過給定名稱向類中添加新方法
@param self 指定類
@param sel 待處理的選擇器
@param IMP 方法名
@return 用來描述方法參數類型的字符集
*/
class_addMethod(self, sel, (IMP)nullMethodSubstite, "@@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
1、獲取選擇器;2、檢測選擇器是否表示nullMethod方法;3、向類中添加該方法
查看運行結果:
備援接收者:
當前接收者還有第二次機會能處理未知的選擇器,在該步驟中,系統會征詢接收者能不能把這條消息轉給其他接收者來處理。對應的處理方法如下:
- (id)forwardingTargetForSelector:(SEL)aSelector
實現代碼:
@interface ModelPrintViewController ()
{
//新建Student類
StudentModel *_model;
}
@end
- (id)forwardingTargetForSelector:(SEL)aSelector{
if ([NSStringFromSelector(aSelector) containsString:@"nullMethod"]) {
return _model;
}
return [super forwardingTargetForSelector:aSelector];
}
在student類中實現代碼:
- (void)nullMethod{
NSLog(@"This is Student nullMethod");
}
1、判斷是否為nullMethod方法;2、設置備援接收者;3、在備援接收者中實現該代碼
查看運行結果:
注:我們無法操作經由這一步轉發的消息,只能設置備援接收者替代當前對象來接收這一消息,如果想在發送給備援接收者之前先修改消息內容,那就得通過完整的消息轉發機制來做了。
完整的消息轉發:
如果轉發算法已經來到這一步的話,那么唯一能做的就是啟用完整的消息轉發機制了。首先創建NSInvocation對象,把與尚未處理的那條消息相關的全部細節都封于其中。此對象包含選擇器、目標(target)、及參數。在觸發NSInvocation對象時,“消息派發系統將親自出馬,把消息指派給目標對象”。
此步驟會調用下列方法來轉發消息:
- (void)forwardInvocation:(NSInvocation *)anInvocation
這個方法的實現可以很簡單:只需改變調用目標,使消息在新目標上得以調用即可。然而這樣實現出來的方法與“備援接收者“方案所實現的方法等效,所以很少有人采用這么簡單的實現方式。比較有用的實現方式為:在觸發消息前,先以某種方式改變消息內容,比如追加另外一個參數,或者改換選擇器,等等。
實現此方法,若發現某調用操作不應由本類處理,則需調用超累的同名方法,這樣的話,繼承體系中的每個類都有機會處理此調用請求,直至NSObject。如果最后調用了NSObject類的方法,那么該方法還會調用”doesNotRecognizeSelector:“,以拋出異常,表明選擇器最終未能得到處理。如圖:
消息轉發全流程:
上圖描述了消息轉發機制處理消息的各個步驟。
以完整的例子演示動態方法解析:
@dynamic: 使用@dynamic關鍵字聲明屬性,可以讓編譯器默認不去自動創建實現屬性所用的實例變量,也不會為其創建存取方法,并且編譯器不會報錯。
為了說明消息轉發機制的意義,下面示范如何以動態方法解析來實現@dynamic屬性。
創建student對象,并且將其屬性用@dynamic關鍵字進行聲明后,在不創建實例變量和存取方法的情況下,通過消息轉發機制實現其存取方法。示例如下:
#import <Foundation/Foundation.h>
@interface StudentModel : NSObject
@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *city;
@property (nonatomic, copy) NSString *country;
@end
#import "StudentModel.h"
@interface StudentModel ()
//muDict用來傳值
@property (nonatomic, strong) NSMutableDictionary *muDict;
@end
@implementation StudentModel
@dynamic name```,city,country;
進行此操作后,編譯器不會自動創建實例變量和存取方法,此時用點語法進行屬性的存取值時,運行期系統找不到對應的選擇器,此時消息轉發機制啟動,系統會調用所屬類的+ (BOOL)resolveInstanceMethod:(SEL)sel方法,我們可以在此方法中對選擇器進行處理:
首先實現替代方法:
id autoStudentGetter(id self, SEL _cmd){
StudentModel *model = (StudentModel *)self;
NSMutableDictionary *dict = model.muDict;
NSString *key = NSStringFromSelector(_cmd);
NSString *str = [[key substringFromIndex:3]lowercaseString];
return [dict objectForKey:str];
}
void autoStudentSetter(id self, SEL _cmd, id value){
StudentModel *model = (StudentModel *)self;
NSMutableDictionary *dict = model.muDict;
NSString *str = NSStringFromSelector(_cmd);
NSMutableString *key = [str mutableCopy];
[key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
[key deleteCharactersInRange:NSMakeRange(0, 3)];
NSString *lowerCharStr = [key lowercaseString];
if (value && ([value isKindOfClass:[NSString class]] || [value isKindOfClass:[NSArray class]] || [value isKindOfClass:[NSNumber class]])) {
[dict setObject:value forKey:lowerCharStr];
}else{
[dict removeObjectForKey:lowerCharStr];
}
}
然后在+ (BOOL)resolveInstanceMethod:(SEL)sel方法中進行邏輯判斷處理:
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSString *str = NSStringFromSelector(sel);
if ([str containsString:@"set"]) {
class_addMethod(self, sel, (IMP)autoStudentSetter, "v@:@");
}else{
class_addMethod(self, sel, (IMP)autoStudentGetter, "@@:");
}
return YES;
}
使用點語法給對象進行賦值,然后進行打印:
- (void)setData{
_model = [[StudentModel alloc]init];
_model.name = @"Samson";
_model.city = @"FuYang";
_model.country = @"China";
NSLog(@"model.name:%@,model.city:%@,model.country:%@",_model.name,_model.city,_model.country);
}
查看打印結果: