iOS消息轉(zhuǎn)發(fā)機制-編寫高質(zhì)量iOS和OS X代碼的52個有效方法記錄

若想令類能夠理解某條消息,我們必須實現(xiàn)出對應的方法才行鲸湃。但是子寓,在編譯器向類發(fā)送其無法解讀的消息時并不會報錯,因為在運行期可以繼續(xù)向類中添加方法炸裆,所以編譯器在編譯時還無法確定類中到底會不會有某個方法的實現(xiàn)鲜屏。當對象接收到無法解讀的消息時,就會啟動“消息轉(zhuǎn)發(fā)”機制惯殊,我們可以經(jīng)由此過程告訴對象應該如何處理未知消息也殖。

可能遇到的經(jīng)由消息轉(zhuǎn)發(fā)機制所處理的消息,控制臺出現(xiàn)下面這種提示信息己儒,那就說明你曾向某個對象發(fā)送過一條其無法解讀的消息捆毫,就會啟動消息轉(zhuǎn)發(fā)機制绩卤,并將此消息轉(zhuǎn)發(fā)給了NSObject的默認實現(xiàn)

-[__NSCFNumber lowercaseString] :unrecognized selector send to instance 0x87

消息轉(zhuǎn)發(fā)分為兩個階段江醇。第一階段先征詢接收者所屬的類何暇,看其是否能動態(tài)添加方法,已處理當前這個“未知的選擇子”,這叫做“動態(tài)方法解析”遏插。第二階段涉及“完整的消息轉(zhuǎn)發(fā)機制”纠修。如果運行期系統(tǒng)已經(jīng)把第一階段執(zhí)行完了,那么接收者自己就無法再以動態(tài)新增方法的手段來響應包含該選擇子的消息了了牛。此時運行期系統(tǒng)就會請求接收者以其他手段來處理與消息相關(guān)的方法調(diào)用辰妙。細分為兩步:首先,讓接收者看看有沒有其他對象能處理這條消息蛙婴。如果有尔破,則運行期系統(tǒng)會把消息轉(zhuǎn)給那個接收者懒构,于是消息轉(zhuǎn)發(fā)結(jié)束。如果沒有這個“備援接收者”胆剧,則啟動完整的消息轉(zhuǎn)發(fā)機制秩霍,運行期系統(tǒng)會把與消息有關(guān)的全部細節(jié)封裝到NSInvocation對象中,再給接收者最后一次機會辕近,令其設(shè)法解決當前還未處理的這條消息匿垄。

動態(tài)方法解析

對象在收到無法解讀的消息后归粉,首先將調(diào)用其所屬類的下列類方法:

+(BOOL)resolveInstanceMethod:(SEL)sel ;

sel 就是那個未知的選擇子糠悼,該方法返回值為BOOL類型浅乔,表示這個類是否能增一個實例方法來處理此選擇子。在繼續(xù)往下執(zhí)行轉(zhuǎn)發(fā)機制之前席噩,本類有機會新增一個處理此選擇子的方法贤壁。
假如尚未實現(xiàn)的方法是類方法,則調(diào)用

+(BOOL)resolveClassMethod:(SEL)sel ;

此方案常用來實現(xiàn)@dynamic屬性馒索,文章后面會寫出完整例子

備援接收者

當前接收者還有第二次機會處理未知的選擇子名船,在這一步中渠驼,運行期系統(tǒng)會詢問是否能將該消息轉(zhuǎn)發(fā)給其他的接收者處理。

- (id)forwardingTargetForSelector:(SEL)aSelector

方法參數(shù)代表未知的選擇子疯趟,若當前接收者能找到備援對象谋梭,則將其返回,找不到就返回nil盹舞。通過此方案隘庄,我們可以用“組合”來模擬出“多重繼承”的某些特性。在一個對象內(nèi)部获印,可能還有一系列其他對象街州,該對象可以經(jīng)由此方法將能夠處理某選擇子的相關(guān)內(nèi)部對象返回,這樣的話鳍征, 在外界看來好像是該對象親自處理了這些消息艳丛。
請注意,我們無法操作經(jīng)由這一步所轉(zhuǎn)發(fā)的消息碰酝,若是想在發(fā)送給備援接收者之前先修改消息內(nèi)容眶蕉,那就得通過完整的消息轉(zhuǎn)發(fā)機制來做了唧躲。

完整的消息轉(zhuǎn)發(fā)

如果轉(zhuǎn)發(fā)已經(jīng)到了這一步,那么唯一能做的就是啟動完整的消息轉(zhuǎn)發(fā)機制了饭入。首先創(chuàng)建NSIvocation對象肛真,把尚未處理的那條消息有關(guān)的細節(jié)全部封到其中蚓让。此對象包含選擇子、目標窄瘟、參數(shù)趟卸。在出發(fā)NSIvocation對象時,”消息派發(fā)系統(tǒng)“將親自出馬图云,把消息指派給目標對象邻邮。
此步驟會調(diào)用下列方法來轉(zhuǎn)發(fā)消息:

-(void)forwardInvocation:(NSInvocation *)anInvocation

這個方法的實現(xiàn)可以寫的很簡單筒严,只需要改變調(diào)用目標鸠补,使消息在新目標上得以調(diào)用即可嘀掸。然而這樣實現(xiàn)出來的方法與”備援接收者“反感所實現(xiàn)的方法等效睬塌,所以很少有人采用這么簡單的實現(xiàn)方式。比較有用的實現(xiàn)方式為:在觸發(fā)消息前勋陪,先以某種方式改變消息內(nèi)容硫兰,比如追加另外一個參數(shù),或者是改換選擇子等等违孝。實現(xiàn)此方法時若發(fā)現(xiàn)不應該由本類處理泳赋,則需要調(diào)用超類的同名方法祖今。這樣的話,集成體系中的每個類都有機會處理此調(diào)用請求耍目,直至NSObject徐绑。如果最后調(diào)用了NSObject類的方法,那么該方法還會繼而調(diào)用”doesNotRecognizeSelector:“以拋出異常耕捞,此異常表明選擇子最終未能得到處理俺抽。

消息轉(zhuǎn)發(fā)全流程

消息機制轉(zhuǎn)發(fā)全流程-下載于簡書作者Maximuum相似文章中.png

接收者在每一步中均有機會處理消息较曼。步驟越往后,處理消息的代價就會越大弛饭。最好能在第一步就完成侣颂,這樣的話,運行期系統(tǒng)就可以將此方法緩存起來藻肄。如果這個類的實例后面還收到同名的選擇子拒担,那么根本就無須啟動消息轉(zhuǎn)發(fā)流程。若想在第三部把消息轉(zhuǎn)發(fā)給備援接收者州弟,還不如把轉(zhuǎn)發(fā)操作提前到第二部婆翔。以為第三部只是修改了調(diào)用目標毁兆,這項改動放在第二部執(zhí)行的話會更加簡單阴挣,不然還得創(chuàng)建并處理完整的NSIvocation。

完整例子演示動態(tài)方法解析

為了說明消息轉(zhuǎn)發(fā)機制的意義茎芭,下面示范如何以動態(tài)方法解析來實現(xiàn)@dynamic屬性梅桩。假設(shè)要編寫一個類似于”字典的對象“拜隧,它里面可以容納其他對象,只不過開發(fā)者要直接通過屬性來存取其中的數(shù)據(jù)垦页。這個類的設(shè)計思路是:由開發(fā)者來添加數(shù)據(jù)定義痊焊,并將其聲明為@dynamic,而類則會自動處理相關(guān)屬性值得存放與獲取操作薄啥。

新建一個繼承于NSObject的類EOCAutoDictionary

#import <Foundation/Foundation.h>

@interface EOCAutoDictionary : NSObject
@property (nonatomic, strong) NSString *string;
@property (nonatomic, strong) NSNumber *number;
@property (nonatomic, strong) NSDate *date;
@property (nonatomic, strong) id opaqueObject;
@end

在類EOCAutoDictionary的內(nèi)部垄惧,每個屬性的值還會存放在字典里到逊,所以在類中編寫如下代碼,并將屬性聲明為@dynamic枷踏,這樣的話編譯器就不會為其自動生成實例變量和存取方法了

#import "EOCAutoDictionary.h"
#import <objc/runtime.h>

@interface EOCAutoDictionary ()
@property (nonatomic, strong) NSMutableDictionary *backingStore;
@end

@implementation EOCAutoDictionary

@dynamic string,number,date,opaqueObject; // 通過dynamic修飾之后編譯器不會為其自動生成實例變量及存取方法
- (id)init {

    if (self = [super init]) {
        _backingStore = [NSMutableDictionary new];
    }
    return self;
}

當對象收到無法解讀的消息后掰曾,調(diào)用下面所屬類方法

// 處理實例方法 
+(BOOL)resolveInstanceMethod:(SEL)sel {

    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString hasPrefix:@"set"]) {
        /**  class_addMethod函數(shù)解讀如下:
         class_addMethod(Class cls, SEL name,IMP imp,const char *types) 
        
       1旷坦、  Class cls:這里需要一個類 [XXX class] 在這里用self,相當于[EOCAutoDictionary class]
      2、   SEL name: 這里的方法命名隨意旗芬,就是添加的方法在本類里叫做的名字捆蜀,但是方法的格式要和你添加的方法的格式一樣辆它,比如有無參數(shù), 這里直接拿sel即可呢蔫,相當于幾個屬性的setter和getter方法名
         
       3飒筑、  IMP imp:函數(shù)指針协屡,指向添加的方法,
         需要實現(xiàn)下面這個方法联予,也就是runtime的方法,獲取對應的函數(shù)的指針季眷,也就是IMP
         // OBJC_EXPORT IMP 函數(shù)返回值是 IMP指針
         // class_getMethodImplementation 函數(shù)名
         括號里各個參數(shù)意義:
          (1)Class cls 一個class
          (2)SEL name 方法名
         OBJC_EXPORT IMP class_getMethodImplementation(Class cls, SEL name)
         OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
         
       4子刮、  const char *types: 'v@:' ==> void methodName(Class cls, SEL name){}
                            'i@:' ==> int methodName(Class cls, SEL name){}
                            'i@:@' ==> int methodName(Class cls, SEL name,parameter){}
                            '@@:' ==> id methodName(Class cls, SEL name){}
         以:為界窑睁,左邊代表函數(shù)返回值類型,右邊表示參數(shù)個數(shù)和類型
         */
        // 向類中動態(tài)的添加方法
        class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
        
    }else {
        class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
    }
    return YES;
}

通過函數(shù)class_addMethod動態(tài)添加的函數(shù)如下:
getter函數(shù)如下:

id autoDictionaryGetter (id self,SEL _cmd) {

    // Get the backing store from the Object
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary *)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;
    
    // The key is simply the selector name
    NSString *key = NSStringFromSelector(_cmd);
    
    // return the value
    return [backingStore objectForKey:key];
}

setter函數(shù)如下:


void autoDictionarySetter(id self,SEL _cmd, id value) {
    // get the backing store from the object
    EOCAutoDictionary *typedSelf = (EOCAutoDictionary*)self;
    NSMutableDictionary *backingStore = typedSelf.backingStore;
    /**
     The seletor will be for example "setOpaqueObject:".
     We need to remove the "set",":",and lowercase the first letter of the remiander
     
     方法類似 "setOpaqueObject:" 我們需要刪除前面的 'set' 和后面的 ':' 并且讓第一個字母小寫
     */
    
    NSString *selectorString = NSStringFromSelector(_cmd);
    NSMutableString *key = [selectorString mutableCopy];
    
    // remove the ":" at the end
    
    [key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
    
    // remove the 'set' prefix
    
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    
    // lowercase the first character
    NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
    
    if (value) {
        [backingStore setObject:value forKey:key];
    } else {
        [backingStore removeObjectForKey:key];
    }
}

EOCAutoDictionary的用法如下

#import "ViewController.h"
#import "EOCAutoDictionary.h"
@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    EOCAutoDictionary *dict = [[EOCAutoDictionary alloc] init];
    dict.date = [NSDate dateWithTimeIntervalSince1970:475372800];
    dict.string = @"編寫iOS OS X代碼的52個高效有效方法";
    NSLog(@"dict.date = %@",dict.date);
    NSLog(@"dict.string = %@",dict.string);
    
}


- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
    // Dispose of any resources that can be recreated.
}


@end
要點:

1箫津、若對象無法響應某個選擇子苏遥,則進入消息轉(zhuǎn)發(fā)流程
2、通過運行期的動態(tài)方法解析功能师抄,我們可以在需要用到某個方法時再將其加入類中
3教硫、對象可以把其無法解讀的某些選擇子轉(zhuǎn)交給其他對象來處理
4、經(jīng)過上述幾步之后挤安,如果還是無法處理選擇子丧鸯,那就啟動完整的消息轉(zhuǎn)發(fā)機制

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末丛肢,一起剝皮案震驚了整個濱河市剿干,隨后出現(xiàn)的幾起案子置尔,更是在濱河造成了極大的恐慌,老刑警劉巖幽歼,帶你破解...
    沈念sama閱讀 222,627評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件甸私,死亡現(xiàn)場離奇詭異,居然都是意外死亡诬烹,警方通過查閱死者的電腦和手機弃鸦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評論 3 399
  • 文/潘曉璐 我一進店門唬格,熙熙樓的掌柜王于貴愁眉苦臉地迎上來西轩,“玉大人,你說我怎么就攤上這事马僻∽⒎” “怎么了?”我有些...
    開封第一講書人閱讀 169,346評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長辜御。 經(jīng)常有香客問我擒权,道長,這世上最難降的妖魔是什么愉老? 我笑而不...
    開封第一講書人閱讀 60,097評論 1 300
  • 正文 為了忘掉前任嫉入,我火速辦了婚禮,結(jié)果婚禮上姨谷,老公的妹妹穿的比我還像新娘映九。我一直安慰自己,他們只是感情好捌议,可當我...
    茶點故事閱讀 69,100評論 6 398
  • 文/花漫 我一把揭開白布瓣颅。 她就那樣靜靜地躺著譬正,像睡著了一般曾我。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上贫贝,一...
    開封第一講書人閱讀 52,696評論 1 312
  • 那天蛉谜,我揣著相機與錄音型诚,去河邊找鬼。 笑死也搓,一個胖子當著我的面吹牛暮现,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 41,165評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼塘幅,長吁一口氣:“原來是場噩夢啊……” “哼电媳!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起捞稿,我...
    開封第一講書人閱讀 40,108評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎拼缝,沒想到半個月后娱局,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,646評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡咧七,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,709評論 3 342
  • 正文 我和宋清朗相戀三年衰齐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片继阻。...
    茶點故事閱讀 40,861評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡耻涛,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瘟檩,到底是詐尸還是另有隱情抹缕,我是刑警寧澤墨辛,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布歉嗓,位于F島的核電站,受9級特大地震影響背蟆,放射性物質(zhì)發(fā)生泄漏鉴分。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,196評論 3 336
  • 文/蒙蒙 一带膀、第九天 我趴在偏房一處隱蔽的房頂上張望志珍。 院中可真熱鬧,春花似錦垛叨、人聲如沸伦糯。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽敛纲。三九已至,卻和暖如春剂癌,著一層夾襖步出監(jiān)牢的瞬間淤翔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評論 1 274
  • 我被黑心中介騙來泰國打工佩谷, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留旁壮,地道東北人监嗜。 一個月前我還...
    沈念sama閱讀 49,287評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像抡谐,于是被迫代替她去往敵國和親裁奇。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,860評論 2 361

推薦閱讀更多精彩內(nèi)容