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
我們先來看下整體流程圖:
下面舉例說明,在 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 的所有信息。
小結
本片文章主要了解下消息發送機制,通過它我們可以為程序動態增加很多行為,例如消息轉發中的第二步和第三步,這兩個方法都允許一個對象與其它對象建立關系,以處理某些消息,我們可以以此來模擬多重繼承,讓對象可以“繼承”其它對象的特性來處理一些事情,當然最好不要這么做,如果可以,我們應該用常規方式解決問題。