iOS runtime 消息機制及消息轉發

Sending Messages

在 Objective-C 中,如果向某對象傳遞消息,那就會使用動態綁定機制來決定需要調用的方法。在底層,所有方法都是普通的 C 語言函數,然而對象收到消息之后,究竟該調用哪個方法則完全于運行期決定,甚至可以在程序運行時改變,這些特性使得 Objective-C 成為一門真正的動態語言。

我們看下定義:

id objc_msgSend(id self, SEL op, ...);

其中參數解釋:

  • self: 消息接收者
  • op: 消息的selector,一個C的字符串用來定位
  • ...: 方法參數數組

從定義可以看出消息由接受者,選擇器及參數構成。

給對象發送消息可以這么寫:

id returnValue = [someObject messageName:parameter]; 

編譯器會將消息轉換成如下函數:

id returnValue = objc_msgSend(someObject,  
                              @selector(messageName:),  
                              parameter); 

objc_msgSend 函數會依據接收者與選擇子的類型來調用適當的方法。為了完成此操作,該方法需要在接收者所屬的類中搜尋其“方法列表”(list of methods),如果能找到與選擇子名稱相符的方法,就跳至其實現代碼。若是找不到,那就沿著繼承體系繼續向上查找,等找到合適的方法之后再跳轉。如果最終還是找不到相符的方法,那就執行“消息轉發”(message forwarding)操作。

下面解釋下上面這段話:

objc_msgSend 如何找到該方法?

1、我們先來看下對象定義:

struct objc_object {
    Class isa  OBJC_ISA_AVAILABILITY;
};

其中 isa 指向對象所屬的類,也就是上面例子中 someObject 由 isa 找到其所屬的類。

2、我們看下類的定義:

struct objc_class {
    Class isa  OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
/* Use `Class` instead of `struct objc_class *` */

我們看到這一行

struct objc_method_list **methodLists 

很明顯類會從這個方法列表中找到與選擇器名稱相同的方法。

3、我們看下方法的定義:

struct objc_method {
    SEL method_name                                          OBJC2_UNAVAILABLE;
    char *method_types                                       OBJC2_UNAVAILABLE;
    IMP method_imp                                           OBJC2_UNAVAILABLE;
}

其中參數:

  • method_name:方法名字
  • method_types:方法類型
  • method_imp:方法具體實現

我們可以看到該結構體中包含一個 SEL 和 IMP,實際上相當于在 SEL 和 IMP 之間作了一個映射。有了 SEL,我們便可以找到對應的IMP,IMP 實際上是一個函數指針,指向方法實現的地址,其定義如下:

/// A pointer to the function of a method implementation. 
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ ); 
#else
typedef id (*IMP)(id, SEL, ...); 
#endif

在這里執行具體的代碼,并返回值給調用者。

寫個例子,再梳理一遍:

NSString * helloWorld =  [obj returnMeHelloWorld];

其傳遞消息流程:

  • 編譯成
    id objc_msgSend(self,@selector(returnMeHelloWorld:));
  • 在 self 中沿著 isa 找到 CustomObject 的類對象
  • 類對象查找自己的方法 list,找到對應的方法執行體 Method
  • 把參數傳遞給 IMP 實際指向的執行代碼
  • 代碼執行返回結果給 helloWorld

以上是實例方法的處理,那么類方法是如何處理的呢?

在類的定義中,可以看到第一行:

Class isa  OBJC_ISA_AVAILABILITY;

這個isa指向的一個Class類型,就是保存了類方法的地方,這個Class類型的東西就是類元對象。類方法的調用先從類元對象中找到對應的方法,后面就和上面舉的例子中實例方法的調用流程相同了,這里不再贅述。

消息轉發 message forwarding

當發送一條消息時,如果接受對象沒有找到對應的方法,會沿著其繼承體系繼續向上找,如果最終還是沒有找到,就會走消息轉發。

消息轉發主要涉及到的方法,在 NSObject.h 中:

+ (BOOL)resolveClassMethod:(SEL)sel
+ (BOOL)resolveInstanceMethod:(SEL)sel

- (id)forwardingTargetForSelector:(SEL)aSelector

- (void)forwardInvocation:(NSInvocation *)anInvocation

我們先來看下整體流程圖:

image

下面舉例說明,在 ViewController.m 中:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self performSelector:@selector(method1)];
    
}

我們調用方法 method1,但并沒有實現它,那么程序就會崩潰,控制臺輸出如下:

 -[ViewController method1]: unrecognized selector sent to instance 0x151d0f2b0
 *** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[ViewController method1]: unrecognized selector sent to instance 0x151d0f2b0'

這段異常信息實際上是由 NSObject 的doesNotRecognizeSelector方法拋出的。不過,我們可以采取一些措施,讓我們的程序執行特定的邏輯,而避免程序的崩潰。

第一種 動態方法解析,類自己處理,動態添加方法

對象在接收到未知的消息時,我們可以動態添加方法,首先會調用以下方法:

+resolveInstanceMethod:(實例方法)

//或者
+resolveClassMethod:(類方法)

在這個方法中,我們可以動態的為該未知消息新增一個處理方法,只需要在運行時通過 class_addMethod 函數動,動態添加到類里面就可以了。如下代碼所示:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self performSelector:@selector(method1)];
    
}

void dynamicMethodIMP(id self, SEL _cmd){
    NSLog(@"implementation method1");
}

+ (BOOL) resolveInstanceMethod:(SEL)aSEL{
    
    if (aSEL == @selector(method1)){
        class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:aSEL];
}

控制臺輸出:

implementation method1

這里用到了 runtime 中 class_addMethod 方法,其定義如下:

BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types);

其中參數:

  • cls: 要添加方法的類。
  • name: 方法名。
  • imp: 具體方法的實現。
  • types: 方法參數的編碼,詳見文檔

第二種 備用接受者,交給其它對象處理

如果在上一步無法處理消息,則Runtime會繼續調以下方法,重定向給其它對象:

- (id)forwardingTargetForSelector:(SEL)aSelector

在這里我們創建一個新的類 TestObject,并實現 method1 方法:

TestObject.h 文件:

@interface TestObject : NSObject

-(void)method1;

@end

TestObject.m 文件:

@implementation TestObject

-(void)method1{
    NSLog(@"TestObject method1");
}

@end

然后回到 ViewController.m 文件,代碼如下:

@interface ViewController (){
    TestObject *_testObj;
}

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    _testObj = [[TestObject alloc]init];
    
    [self performSelector:@selector(method1)];
    
}

- (id)forwardingTargetForSelector:(SEL)aSelector{
    
    if (aSelector == @selector(method1)) {
        return _testObj;
    }
    return [super forwardingTargetForSelector:aSelector];
}

控制臺輸出:

TestObject method1

我們可以看到在這個方法中,我們并不能操作參數與返回值,也就說我們發一條消息帶有參數和返回值,則本方法無法使用。

最后 完整消息轉發,重定向消息。

對于完整轉發,NSObject提供了以下方法來處理:

- (void)forwardInvocation:(NSInvocation *)anInvocation

當前面兩步都無法處理消息時,運行時系統便會給接收者最后一個機會,將其轉發給其它代理對象來處理。這主要是通過創建一個表示消息的 NSInvocation 對象并將這個對象當作參數傳遞給 forwardInvocation: 方法。我們在 forwardInvocation: 方法中可以選擇將消息轉發給其它對象。

還有重要的一點,我們必須重寫以下方法:

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

看一下官方原文:

Important

To respond to methods that your object does not itself recognize, you must override methodSignatureForSelector: in addition to forwardInvocation:. The mechanism for forwarding messages uses information obtained from methodSignatureForSelector: to create the NSInvocation object to be forwarded. Your overriding method must provide an appropriate method signature for the given selector, either by pre formulating one or by asking another object for one.

我們必須重寫methodSignatureForSelector:forwardInvocation:。轉發消息的機制使用從methodSignatureForSelector獲得的信息來創建要轉發的 NSInvocation 對象。重寫方法必須為給定的選擇器提供適當的方法簽名,方法可以是預先構造一個方法簽名,也可以是向另一個對象請求一個方法簽名。

所以我們下面重寫一下這兩個方法:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self performSelector:@selector(method1)];
}

- (NSMethodSignature*)methodSignatureForSelector:(SEL)aSelector{
    //判斷selector是否為需要轉發的,如果是則手動生成方法簽名并返回。
    if (aSelector == @selector(method1)){
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super forwardingTargetForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation{
    //判斷待處理的anInvocation是否為我們要處理的
    if (anInvocation.selector == @selector(method1)){
        NSLog(@"執行 method1");
    }else{
        [super forwardInvocation:anInvocation];
    }
}

控制臺輸出:

執行 method1

其中 anInvocation 保存著我們調用一個 method 的所有信息。

小結

本片文章主要了解下消息發送機制,通過它我們可以為程序動態增加很多行為,例如消息轉發中的第二步和第三步,這兩個方法都允許一個對象與其它對象建立關系,以處理某些消息,我們可以以此來模擬多重繼承,讓對象可以“繼承”其它對象的特性來處理一些事情,當然最好不要這么做,如果可以,我們應該用常規方式解決問題。

參考資料:

runtime 源碼

Objective-C Runtime Reference

http://book.51cto.com/art/201403/432144.htm

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,461評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,538評論 3 417
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,423評論 0 375
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,991評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,761評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,207評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,268評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,419評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,959評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,782評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,528評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,222評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,653評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,901評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,678評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,978評論 2 374

推薦閱讀更多精彩內容