[深入淺出Cocoa]之消息(二)-詳解動態方法決議(Dynamic Method Resolution)
羅朝輝 (http://www.cnblogs.com/kesalin/)
序言
如果我們在 Objective C 中向一個對象發送它無法處理的消息,會出現什么情況呢?根據前文《深入淺出Cocoa之消息》的介紹,我們知道發送消息是通過 objc_send(id, SEL, ...) 來實現的,它會首先在對象的類對象的 cache,method list 以及父類對象的 cache, method list 中依次查找 SEL 對應的 IMP;如果沒有找到且實現了動態方法決議機制就會進行決議,如果沒有實現動態方法決議機制或決議失敗且實現了消息轉發機制就會進入消息轉發流程,否則程序 crash。也就是說如果同時提供了動態方法決議和消息轉發,那么動態方法決議先于消息轉發,只有當動態方法決議依然無法正確決議 selector 的實現,才會嘗試進行消息轉發。在前文中,我并沒有詳細講解動態方法決議,因此本文將詳細介紹之。
本文代碼下載:點此下載
一,向一個對象發送該對象無法處理的消息
如下代碼:
@interface Foo : NSObject
-(void)Bar;
@end
@implementation Foo
-(void)Bar
{
NSLog(@" >> Bar() in Foo");
}
@end
/////////////////////////////////////////////////
#import "Foo.h"
int main (int argc, const char * argv[])
{
@autoreleasepool {
Foo * foo = [[Foo alloc] init];
[foo Bar];
[foo MissMethod];
[foo release];
}
return 0;
}
在編譯時,XCode 會提示警告:
Instance method '-MissMethod' not found (return type defaults to 'id')
如果,我們忽視該警告運行之,一定會 crash:
>> Bar() in Foo
-[Foo MissMethod]: unrecognized selector sent to instance 0x10010c840
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Foo MissMethod]: unrecognized selector sent to instance 0x10010c840'
*** Call stack at first throw:
......
terminate called after throwing an instance of 'NSException'
下劃線部分就是造成 crash 的原因:對象無法處理 MissMethod 對應的 selector,也就是沒有相應的實現。
二,動態方法決議
Objective C 提供了一種名為動態方法決議的手段,使得我們可以在運行時動態地為一個 selector 提供實現。我們只要實現 +resolveInstanceMethod: 和/或 +resolveClassMethod: 方法,并在其中為指定的 selector 提供實現即可(通過調用運行時函數 class_addMethod 來添加)。這兩個方法都是 NSObject 中的類方法,其原型為:
+ (BOOL)resolveClassMethod:(SEL)name;
+ (BOOL)resolveInstanceMethod:(SEL)name;
參數 name 是需要被動態決議的 selector;返回值文檔中說是表示動態決議成功與否。但在上面的例子中(不涉及消息轉發的情況下),如果在該函數內為指定的 selector 提供實現,無論返回 YES 還是 NO,編譯運行都是正確的;但如果在該函數內并不真正為 selector 提供實現,無論返回 YES 還是 NO,運行都會 crash,道理很簡單,selector 并沒有對應的實現,而又沒有實現消息轉發。resolveInstanceMethod 是為對象方法進行決議,而 resolveClassMethod 是為類方法進行決議。
下面我們用動態方法決議手段來修改上面的代碼:
//
// Foo.m
// DeepIntoMethod
//
// Created by 飄飄白云 on 12-11-13.
// Copyright (c) 2012年 kesalin@gmail.com All rights reserved.
//
#import "Foo.h"
#include <objc/runtime.h>
void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@" >> dynamicMethodIMP");
}
@implementation Foo
-(void)Bar
{
NSLog(@" >> Bar() in Foo");
}
+ (BOOL)resolveInstanceMethod:(SEL)name
{
NSLog(@" >> Instance resolving %@", NSStringFromSelector(name));
if (name == @selector(MissMethod)) {
class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:name];
}
+ (BOOL)resolveClassMethod:(SEL)name
{
NSLog(@" >> Class resolving %@", NSStringFromSelector(name));
return [super resolveClassMethod:name];
}
@end
在前文《深入淺出Cocoa之消息》中已經介紹過 Objective C 中的方法其實就是至少帶有兩個參數(self 和 _cmd)的普通 C 函數,因此在上面的代碼中提供這樣一個 C 函數 dynamicMethodIMP,讓它來充當對象方法 MissMethod 這個 selector 的動態實現。因為 MissMethod 是被對象所調用,所以它被認為是一個對象方法,因而應該在 resolveInstanceMethod 方法中為其提供實現。通過調用
class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");
就能在運行期動態地為 name 這個 selector 添加實現:dynamicMethodIMP。class_addMethod 是運行時函數,所以需要導入頭文件:objc/runtime.h。
再次編譯運行前面的測試代碼,輸出如下:
>> Bar() in Foo.
>> Instance resolving MissMethod
>> dynamicMethodIMP called.
>> Instance resolving _doZombieMe
dynamicMethodIMP 被調用了,crash 沒有了!萬事大吉!
注意:這里兩次調用了 resolveInstanceMethod,而且兩次決議的 selector 在不同的系統下是不同的,上面演示的是 10.7 系統下第一個決議 MissMethod,第二個決議 _doZombieMe;在 10.6 系統下兩次都是決議 MissMethod。
下面我把 resolveInstanceMethod 方法中為 selector 添加實現的那一行屏蔽了,消息轉發就應該會進行:
//class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");
再次編譯運行,此時輸出:
>> Bar() in Foo.
>> Instance resolving MissMethod
+[Foo resolveInstanceMethod:MissMethod] returned YES, but no new implementation of -[Foo MissMethod] was found
>> Instance resolving _doZombieMe
objc[1223]: +[Foo resolveInstanceMethod:MissMethod] returned YES, but no new implementation of -[Foo MissMethod] was found
-[Foo MissMethod]: unrecognized selector sent to instance 0x10010c880
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '-[Foo MissMethod]: unrecognized selector sent to instance 0x10010c880'
*** Call stack at first throw:
......
在這里,resolveInstanceMethod 使詐了,它聲稱成功(返回 YES )決議了 selector,但是并沒有真正提供實現,被編譯器發覺而提示相應的錯誤信息。那它的返回值到底有什么作用呢,在它沒有提供真正的實現,并且提供了消息轉發機制的情況下,YES 表示不進行后續的消息轉發,返回 NO 則表示要進行后續的消息轉發。
三,源碼剖析
讓我們來看看運行時系統是如何進行動態方法決議的,下面的代碼來自蘋果官方公開的源碼 objc-class.mm,我在其中添加了中文注釋:
1,首先是判斷是不是要進行類方法決議,如果不是或決議失敗,則進行實例方法決議(請參考:《深入淺出Cocoa之類與對象》):
/***********************************************************************
* _class_resolveMethod
* Call +resolveClassMethod or +resolveInstanceMethod and return
* the method added or NULL.
* Assumes the method doesn't exist already.
**********************************************************************/
__private_extern__ Method _class_resolveMethod(Class cls, SEL sel)
{
Method meth = NULL;
if (_class_isMetaClass(cls)) {
meth = _class_resolveClassMethod(cls, sel);
}
if (!meth) {
meth = _class_resolveInstanceMethod(cls, sel);
}
if (PrintResolving && meth) {
_objc_inform("RESOLVE: method %c[%s %s] dynamically resolved to %p",
class_isMetaClass(cls) ? '+' : '-',
class_getName(cls), sel_getName(sel),
method_getImplementation(meth));
}
return meth;
}
2,類方法決議與實例方法決議大體相似,在這里就只看實例方法決議部分了:
/***********************************************************************
* _class_resolveInstanceMethod
* Call +resolveInstanceMethod and return the method added or NULL.
* cls should be a non-meta class.
* Assumes the method doesn't exist already.
**********************************************************************/
static Method _class_resolveInstanceMethod(Class cls, SEL sel)
{
BOOL resolved;
Method meth = NULL;
// 是否實現了 resolveInstanceMethod,如果沒有返回 NULL
if (!look_up_method(((id)cls)->isa, SEL_resolveInstanceMethod,
YES /*cache*/, NO /*resolver*/))
{
return NULL;
}
// 調用 resolveInstanceMethod,并獲取返回值
resolved = ((BOOL(*)(id, SEL, SEL))objc_msgSend)((id)cls, SEL_resolveInstanceMethod, sel);
if (resolved) {
// 返回值為 YES,表示 resolveInstanceMethod 聲稱它已經成功添加實現,則再次查找 method list
// +resolveClassMethod adds to self
meth = look_up_method(cls, sel, YES/*cache*/, NO/*resolver*/);
if (!meth) {
// resolveInstanceMethod 使詐了,它聲稱成功添加實現了,但實際沒有,給出警告信息,并返回 NULL
// Method resolver didn't add anything?
_objc_inform("+[%s resolveInstanceMethod:%s] returned YES, but "
"no new implementation of %c[%s %s] was found",
class_getName(cls),
sel_getName(sel),
class_isMetaClass(cls) ? '+' : '-',
class_getName(cls),
sel_getName(sel));
return NULL;
}
}
// 其他情況下返回 NULL
return meth;
}
這段代碼很容易理解:
1,首先判斷是否實現了 resolveInstanceMethod,如果沒有實現,返回 NULL,進入下一步處理;
2,如果實現了,調用 resolveInstanceMethod,獲取返回值;
3,如果返回值為 YES,表示 resolveInstanceMethod 聲稱它已經提供了 selector 的實現,因此再次查找 method list,如果依然找到對應的 IMP,則返回該實現,否則提示警告信息,返回 NULL,進入下一步處理;
4,如果返回值為 NO,返回 NULL,進入下一步處理;
四,加入消息轉發
在前文《深入淺出Cocoa之消息》一文中,我演示了一個消息轉發的示例,下面我把動態方法決議部分去除,把消息轉發部分添加進來:
// Proxy
@interface Proxy : NSObject
-(void)MissMethod;
@end
@implementation Proxy
-(void)MissMethod
{
NSLog(@" >> MissMethod() called in Proxy.");
}
@end
// Foo
@interface Foo : NSObject
-(void)Bar;
@end
@implementation Foo
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL name = [anInvocation selector];
NSLog(@" >> forwardInvocation for selector %@", NSStringFromSelector(name));
Proxy * proxy = [[[Proxy alloc] init] autorelease];
if ([proxy respondsToSelector:name]) {
[anInvocation invokeWithTarget:proxy];
}
else {
[super forwardInvocation:anInvocation];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [Proxy instanceMethodSignatureForSelector:aSelector];
}
-(void)Bar
{
NSLog(@" >> Bar() in Foo.");
}
@end
運行測試代碼,輸出如下:
>> Bar() in Foo.
>> forwardInvocation for selector MissMethod
>> MissMethod() called in Proxy.
如果我把動態方法決議部分代碼也加入進來輸出又是怎樣呢?下面只列出了 Foo 的實現代碼,其他代碼不變動。
@implementation Foo
+(BOOL)resolveInstanceMethod:(SEL)name
{
NSLog(@" >> Instance resolving %@", NSStringFromSelector(name));
if (name == @selector(MissMethod)) {
class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:name];
}
+(BOOL)resolveClassMethod:(SEL)name
{
NSLog(@" >> Class resolving %@", NSStringFromSelector(name));
return [super resolveClassMethod:name];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
SEL name = [anInvocation selector];
NSLog(@" >> forwardInvocation for selector %@", NSStringFromSelector(name));
Proxy * proxy = [[[Proxy alloc] init] autorelease];
if ([proxy respondsToSelector:name]) {
[anInvocation invokeWithTarget:proxy];
}
else {
[super forwardInvocation:anInvocation];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
return [Proxy instanceMethodSignatureForSelector:aSelector];
}
-(void)Bar
{
NSLog(@" >> Bar() in Foo.");
}
@end
此時,輸出為:
>> Bar() in Foo.
>> Instance resolving MissMethod
>> dynamicMethodIMP called.
>> Instance resolving _doZombieMe
注意到了沒,消息轉發沒有進行!在前文中說過,消息轉發只有在對象無法正常處理消息時才會調用,而在這里我在動態方法決議中為 selector 提供了實現,使得對象可以處理該消息,所以消息轉發不會繼續了。官方文檔中說:
If you implement resolveInstanceMethod:
but want particular selectors to actually be forwarded via the forwarding mechanism, you return NO
for those selectors.
文檔里的說法其實并不準確,只有在 resolveInstanceMethod 的實現中沒有真正為 selector 提供實現,并返回 NO 的情況下才會進入消息轉發流程;否則絕不會進入消息轉發流程,程序要么調用正確的動態方法,要么 crash。這也與前面的源碼不太一致,我猜測在比上面源碼的更高層次的地方,再次查找了 method list,如果提供了實現就能夠找到該實現。
下面我把 resolveInstanceMethod 方法中為 selector 添加實現的那一行屏蔽了,消息轉發就應該會進行:
//class_addMethod([self class], name, (IMP)dynamicMethodIMP, "v@:");
再次編譯運行,此時輸出正如前面所推斷的那樣:
>> Bar() in Foo.
>> Instance resolving MissMethod
objc[1618]: +[Foo resolveInstanceMethod:MissMethod] returned YES, but no new implementation of -[Foo MissMethod] was found
>> forwardInvocation for selector MissMethod
>> MissMethod() called in Proxy.
>> Instance resolving _doZombieMe
進行了消息轉發!而且編譯器很善意地提示(見前面源碼剖析):哎呀,你不能欺騙我嘛,你說添加了實現(返回YES),其實還是沒有呀!然后編譯器就無奈地去看能不能消息轉發了。當然如果把返回值修改為 NO 就不會有該警告出現,其他的輸出不變。
五,總結
從上面的示例演示可以看出,動態方法決議是先于消息轉發的。
如果向一個 Objective C 對象對象發送它無法處理的消息(selector),那么編譯器會按照如下次序進行處理:
1,首先看是否為該 selector 提供了動態方法決議機制,如果提供了則轉到 2;如果沒有提供則轉到 3;
2,如果動態方法決議真正為該 selector 提供了實現,那么就調用該實現,完成消息發送流程,消息轉發就不會進行了;如果沒有提供,則轉到 3;
3,其次看是否為該 selector 提供了消息轉發機制,如果提供了消息了則進行消息轉發,此時,無論消息轉發是怎樣實現的,程序均不會 crash。(因為消息調用的控制權完全交給消息轉發機制處理,即使消息轉發并沒有做任何事情,運行也不會有錯誤,編譯器更不會有錯誤提示。);如果沒提供消息轉發機制,則轉到 4;
4,運行報錯:無法識別的 selector,程序 crash;
六,引用
官方運行時源代碼:http://www.opensource.apple.com/source/objc4/objc4-532/runtime/
Objective-C Runtime Programming Guide
深入淺出Cocoa之消息
深入淺出Cocoa之類與對象