消息機制(Messaging)
不知大家有沒有想過:我們在程序中調用的方法,是怎么執行的,又是怎么通過一個方法名字就能找到其對應的實現的。其實在OC中,我們的方法在運行時,都是作為消息在傳遞的,我們甚至可以把方法叫做消息。
在運行時,消息和方法實現會通過objc_msgSend函數進行動態綁定。編譯器會將方法調用
[receiver message]
轉換為 objc_msgSend(receiver, selector)
函數,該函數自帶兩個參數:receiver(方法的接收者) 和 selector(方法的簽名)。帶參數的方法會被轉換成objc_msgSend(receiver, selector, arg1, arg2, ...)
函數。其中receiver和selector是方法的隱藏參數,這樣寫大家可能會覺得陌生,其實這兩個參數就是大家熟悉的self 和 _cmd,在編譯時,編譯器會將這兩個參數,傳到方法里。
那么objc_msgSend函數是怎么實現動態綁定的呢?其實它做了如下工作:
當我們調用方法[Object message]時,編譯器會將這個方法調用轉換成objc_msgSend函數,該函數會根據Object的isa指針,找到其所屬的類,然后在其類中的方法列表中查找這個message對應的selector,如果沒有找到,objc_msgSend會根據Object的superclass指針,找到該對象所屬類的父類,然后在父類的方法列表中查找message對應的selector,依此,objc_msgSend會在類的繼承結構中一直向上尋找,直到到達NSObject類。一旦定位到selector.函數就會傳入方法的接收者及其他所需要的參數,從而調用這個方法的實現。
獲取方法地址
對于會頻繁調用的方法,OC的消息機制在消息傳遞的過程中,會造成一些不必要的開銷。如果我們對性能要求比較高,會希望避免這些開銷。當然,消息傳遞也不是不可避免,我們可以通過methodForSelector:
獲取方法的地址,然后像調用函數一樣調用方法。
避免消息傳遞的情況,通常會出現在for循環中。舉例:
#import <Foundation/Foundation.h>
#import "Person.h"
void (*setter)(id, SEL, NSString *);
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
setter = (void (*)(id, SEL, NSString *))[p methodForSelector:@selector(setName:)];
for(int i = 0; i < 100; i++){
setter(p,@selector(setName:),@"百客");
}
}
return 0;
}
如果我們用函數的方式來調用方法,方法的隱藏參數self和_cmd必須要顯示的寫出來,所以本例中,函數setter中的前兩個參數id、SEL,是必須要寫的。
動態方法解析(Dynamic Method Resolution)
如果,在方法的尋找過程中,一直到NSObject類,仍然沒有找到方法的實現,會發生什么呢?系統會調用NSObject的 - (void)doesNotRecognizeSelector:(SEL)aSelector
方法報出崩潰信息:unrecognized selector sent to instance。但是在崩潰之前,其實系統會給我們二次處理消息的機會,首先是方法的動態解析
,我們也可以在動態解析時做些處理,使其不會崩潰。
如果直到NSObject類,仍然沒有找到方法的實現,那么系統會去看我們是否是動態實現了該方法,如果是實例方法,會調用對象所在類的+ (BOOL)resolveInstanceMethod:(SEL)sel,如果是類方法,會調用 + (BOOL)resolveClassMethod:(SEL)sel。
代碼示例:
Person.h
#import <Foundation/Foundation.h>
@interface Person : NSObject
@end
Person.m
#import "Person.h"
#import <objc/runtime.h>
void setNameImp(id self, SEL _cmd, NSString *name){
NSLog(@"%@",name);
}
@implementation Person
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"%s",__FUNCTION__);
if(sel == @selector(setName:)){
class_addMethod(self, sel, (IMP)setNameImp, "v@:@");
return YES;
}
return [super resolveInstanceMethod:sel];
}
@end
main.m
#import <Foundation/Foundation.h>
#import "Person.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
[p performSelector:@selector(setName:) withObject:@"百客"];
}
return 0;
}
輸出結果:
+[Person resolveInstanceMethod:]
百客
如果動態方法解析時,沒有處理傳遞的消息,那么系統就會走轉發流程。** 動態解析的兩個方法是在消息轉發前執行的,如果想跳過動態解析過程,直接走消息轉發流程,直接將兩個方法的返回值設為NO**
消息轉發(Message Forwarding)
運行時,如果走到了轉發流程階段,系統會先判斷是否轉移了消息的接收者。
-
轉移消息的接收者
通過- forwardingTargetForSelector:可以轉移消息的接收者,如果有別的對象可以實現該方法,就直接讓那個對象來處理該消息。不管另一對象的方法是公有還是私有,只要實現了就行- (id)forwardingTargetForSelector:(SEL)aSelector{ if(@selector(setName:) == aSelector){ Student *s = [[Student alloc] init]; return s; } return [super forwardingTargetForSelector:aSelector]; }
Student.m
#import "Student.h" @implementation Student - (void)setName:(NSString *)name{ NSLog(@"%@--",name); } @end
Note:該方式只是轉移消息的接受者,不能對傳遞的消息做修改。
- 完整轉發
如果上述方式沒有對轉發的消息做處理,那么系統就會走完整的轉發流程。系統會先通過- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
方法,找到最終響應傳遞的消息的方法的地址,然后再通過- (void)forwardInvocation:(NSInvocation *)anInvocation
來轉發消息。如果通過第一個方法沒有找到最終響應消息的方法的地址,第二個方法是不會執行的。
通過這種方式轉發的消息,我們不僅可以轉換消息的接收者,還可以對轉發的消息做相應的修改。
繼續上面的代碼,我們在Person.m中增加了如下代碼,main.m中的代碼沒有做改動。
Person.m
- (void)forwardInvocation:(NSInvocation *)anInvocation{
NSString *name = @"hello";
int age = 10;
[anInvocation setArgument:&name atIndex:2];
[anInvocation setArgument:&age atIndex:3];
anInvocation.selector = @selector(setName:age:);
Student *s = [[Student alloc] init];
if([s respondsToSelector:[anInvocation selector]]){
[anInvocation invokeWithTarget:s];
}else{
[super forwardInvocation:anInvocation];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
NSMethodSignature *sign = [super methodSignatureForSelector:aSelector];
if(!sign){
Student *s = [[Student alloc] init];
sign = [s methodSignatureForSelector:@selector(setName:age:)];
}
return sign;
}
在代碼中,我們將轉發給Person對象的setName消息,轉發給了Student對象,并修改了消息的內容,最終
輸出結果如下:
hello--10
最終我們總結出消息的傳遞及轉發機制如下: