消息轉發是Objective-C的消息機制的一個特性而已,實際工程中使用到的場景其實很少,不過了解其機制也是頗有必要的。 OC的消息機制,容許用戶在未實現某個消息(SEL)的具體方法(IMP)時,依然有機會可以響應該消息。能夠理解爲是發送消息的一個補充,專用於處理未找到消息的狀況。 因此,普通的發送消息,若是沒有在實例對象的類(或者類對象的元類)的方法列表中找到對應的方法,則會進入消息轉發的流程,包含如下幾個步驟。bash
這兩個方法,容許開發者動態添加方法的具體實現。app
static void sayInstanceName(id self, SEL cmd, id value) {
NSLog(@"resolveInstanceMethod %@", value);
}
static void sayClassName(id self, SEL cmd, id value) {
NSLog(@"resolveClassMethod %@", value);
}
複製代碼
@implementation Person
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
NSLog(@"resolveInstanceMethod");
NSString *methodName = NSStringFromSelector(sel);
if ([methodName isEqualToString:@"sayInstanceName:"]) {
class_addMethod([self class], sel, (IMP)sayInstanceName, "v@:@");
return YES;
} else if ([methodName isEqualToString:@"dynamicName"]) {
class_addMethod(self, sel, (IMP)myDynamicName, "@@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
// 注意:
// self爲實例對象時,[self class]與object_getClass(self)都返回類(前者調用後者),object_getClass([self class])返回元類。
// self爲類對象時,[self class]返回自身,而object_getClass(self)返回元類,等價於object_getClass([self class])。
+ (BOOL)resolveClassMethod:(SEL)sel
{
NSString *methodName = NSStringFromSelector(sel);
if ([methodName isEqualToString:@"sayClassName:"]) {
class_addMethod(object_getClass(self), sel, (IMP)sayClassName, "v@:@");
return YES;
}
return [super resolveClassMethod:sel];
}
@end
複製代碼
能夠設置實例方法和類方法。優化
注意,這裏會涉及到方法實現的Type Encoding,咱們暫時先了解簡單的對應關係便可。ui
[返回值][target][action][參數]
複製代碼
// 如 v@:@, 即this
// v void
// @ id
// : SEL
// @ id
複製代碼
消息轉發,能夠將消息轉發給能夠響應該消息的一個對象,這個對象能夠是實例對象的一個屬性,也能夠是絕不相關的另一個對象。atom
// 僅支持將消息轉發給一個對象
- (id)forwardingTargetForSelector:(SEL)aSelector
{
NSLog(@"forwardingTargetForSelector");
// 此處能夠詢問對象的全部屬性,看有誰能夠響應消息,即將其return
if ([NSStringFromSelector(aSelector) isEqualToString:@"sayInstanceName:"]) {
if ([self.helper respondsToSelector:aSelector]) {
return self.helper;
}
// 並不會執行,因此forwardingTargetForSelector僅支持將消息轉發給一個對象
if ([self.anotherHelper respondsToSelector:aSelector]) {
return self.anotherHelper;
}
}
return [super forwardingTargetForSelector:aSelector];
}
複製代碼
注意:這裏只能將消息轉發給 一個對象 。spa
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"methodSignatureForSelector");
if ([self.helper respondsToSelector:aSelector]) {
return [self.helper methodSignatureForSelector:aSelector];
}
if ([self.anotherHelper respondsToSelector:aSelector]) {
return [self.anotherHelper methodSignatureForSelector:aSelector];
}
return [super methodSignatureForSelector:aSelector];
}
// 支持將消息轉發給任意多個對象,因此多繼承也只能採用forwardInvocation:的方式
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"forwardInvocation");
SEL sel = anInvocation.selector;
if ([self.helper respondsToSelector:sel]) {
[anInvocation invokeWithTarget:self.helper];
}
if ([self.anotherHelper respondsToSelector:sel]) {
[anInvocation invokeWithTarget:self.anotherHelper];
}
}
複製代碼
這裏,對方法的簽名進行嚴格匹配,而後再執行對應的消息轉發。3d
在以上步驟所有執行過,依然沒能完成發送消息,則會調用以下方法:指針
// unrecognized selector sent to instance
- (void)doesNotRecognizeSelector:(SEL)aSelector
{
NSLog(@"doesNotRecognizeSelector");
}
複製代碼
NSObject對該方法的默認實現即拋出一個異常,如:code
[Person sayClassName:]: unrecognized selector sent to class 0x109344950
複製代碼
一般狀況下,這兩個階段的區別以下:
方法簽名包含了方法的名稱、參數、返回值。iOS中使用Type Encoding的方式來表示。
例以下邊的方法:
- (void)myVoid:(NSDictionary *)params {
NSLog(@"myFunc %@", params);
}
- (BOOL)myBool:(NSDictionary *)params {
NSLog(@"myFunc %@", params);
return YES;
}
- (NSInteger)myNSInteger:(NSDictionary *)params {
NSLog(@"myFunc %@", params);
return 1;
}
- (CGFloat)myCGFloat:(NSDictionary *)params {
NSLog(@"myFunc %@", params);
return CGFLOAT_MAX;
}
- (id)myFunc:(NSDictionary *)params {
NSLog(@"myFunc %@", params);
return [params copy];
}
複製代碼
- (void)testInvocations {
SEL myVoid = @selector(myVoid:);
SEL myBool = @selector(myBool:);
SEL myNSInteger = @selector(myNSInteger:);
SEL myCGFloat = @selector(myCGFloat:);
SEL myFunc = @selector(myFunc:);
NSDictionary *params = @{@"name": @"name"};
NSMethodSignature *myVoidSig = [self methodSignatureForSelector:myVoid]; // v@:@
NSMethodSignature *sigMyBool = [self methodSignatureForSelector:myBool]; // B@:@
NSMethodSignature *sigMyNSInteger = [self methodSignatureForSelector:myNSInteger]; // q@:@
NSMethodSignature *sigMyCGFloat = [self methodSignatureForSelector:myCGFloat]; // d@:@
NSMethodSignature *sigMyFunc = [self methodSignatureForSelector:myFunc]; // @@:@
NSLog(@"testInvocations");
}
複製代碼
重複一次,Type Encoding的格式爲:
[返回值][target][action][參數]
複製代碼
各個類型對應的表示分別爲 void-v,Bool-B,NSInteger-q,CGFlat-d,id-@ 。
以myFunc:爲例,表示爲 @@:@ ,即返回值爲id,接收參數也爲NSObject類型(NSDictonary)。
NSInvocation能夠給任意OC對象發送消息,其使用方式有固定的步驟:
- (void)testInvocation {
SEL sel = @selector(myFunc:);
NSDictionary *params = @{@"name": @"name"};
NSMethodSignature *sig = [self methodSignatureForSelector:sel];
if (!sig) {
return;
}
const char *retType = [sig methodReturnType];
if (strcmp(retType, @encode(void)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
invocation.target = self;
invocation.selector = sel;
[invocation setArgument:¶ms atIndex:2];
[invocation invoke];
NSLog(@"void ret");
/// 0是target,1是action。參數是從2開始的。
void *target;
SEL action;
[invocation getArgument:&target atIndex:0];
[invocation getArgument:&action atIndex:1];
/// target-action : <AppDelegate: 0x600003d69080>-myFunc:
NSLog(@"target-action : %@-%@", target, NSStringFromSelector(action));
} else if (strcmp(retType, @encode(NSInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
invocation.target = self;
invocation.selector = sel;
[invocation setArgument:¶ms atIndex:2];
[invocation invoke];
NSInteger ret = 0;
[invocation getReturnValue:&ret];
NSLog(@"NSInteger ret %ld", (long)ret);
} else if (strcmp(retType, @encode(NSUInteger)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
invocation.target = self;
invocation.selector = sel;
[invocation setArgument:¶ms atIndex:2];
[invocation invoke];
NSUInteger ret = 0;
[invocation getReturnValue:&ret];
NSLog(@"NSUInteger ret %ld", (long)ret);
} else if (strcmp(retType, @encode(BOOL)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
invocation.target = self;
invocation.selector = sel;
[invocation setArgument:¶ms atIndex:2];
[invocation invoke];
BOOL ret = false;
[invocation getReturnValue:&ret];
NSLog(@"BOOL ret %ld", (long)ret);
} else if (strcmp(retType, @encode(CGFloat)) == 0) {
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
invocation.target = self;
invocation.selector = sel;
[invocation setArgument:¶ms atIndex:2];
[invocation invoke];
CGFloat ret = 0;
[invocation getReturnValue:&ret];
NSLog(@"CGFloat ret %ld", (long)ret);
} else {
/// performSelector比較嚴格,若是返回值不匹配,則極可能會致使crash。
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
id ret = [self performSelector:sel withObject:params];
#pragma clang diagnostic pop
NSLog(@"id ret %@", ret);
}
}
複製代碼
注意,這裏NSInvocation默認不會強引用其各個參數,因此若參數在NSInvocation執行前就被釋放則會形成野指針異常(EXC_BAD_ACCESS)。
若是有須要強引用參數的場景,如延遲執行invoke方法,則須要對參數進行強持有操做。調用retainArguments便可,同時有一個屬性argumentsRetained能夠用來判斷。
請看開發者文檔的詳細描述,注意其中的細節:對象類型是retain操做,而C-string和blocks則是copy操做。
Instance Method
retainArguments
If the receiver hasn’t already done so, retains the target and all object arguments of the receiver and copies all of its C-string arguments and blocks. If a returnvalue has been set, this is also retained or copied.
Declaration
- (void)retainArguments;
Discussion
Before this method is invoked, argumentsRetained returns NO; after, it returns YES.
For efficiency, newly created NSInvocation objects don’t retain or copy their arguments, nor do they retain their targets, copy C strings, or copy any associated blocks. You should instruct an NSInvocation object to retain its arguments if you intend to cache it, because the arguments may otherwise be released before the invocation is invoked. NSTimer objects always instruct their invocations to retain their arguments, for example, because there’s usually a delay before a timer fires.
複製代碼
getReturnValue方法,僅僅將返回數據拷貝到指定內存區域,並不考慮內存管理。若返回對象類型,則爲__unsafe_unretained。優化辦法有:
手動添加一個強持有,則返回對象會自動添加autorelease關鍵字,不會出現野指針異常。
NSObject __unsafe_unretained *tmpRet;
[invoke getReturnValue:&tmpRet];
NSObject *ret = tmpRet;
return ret;
複製代碼
或者,使用__bridge進行類型轉換,這種作法更爲推薦。
void *tmpRet = NULL;
[invoke getReturnValue:&tmpRet];
NSObject *ret = (__bridge NSObject *)tmpRet;
return ret;
複製代碼
若是對屬性使用了@dynamic關鍵字,則編譯器不會自動爲其生成getter/setter方法,而是經過動態查找的方法。
因此,若是使用了@dynamic關鍵字,而沒有手動添加getter/setter方法,則使用時會出錯。
@property (nonatomic, copy) NSString *dynamicName;
...
@implementation Person
@dynamic dynamicName;
...
@end
複製代碼
能夠經過resolveInstanceMethod:方法動態添加getter/setter便可。
OC的面向對象是單一繼承的關係。若是有個別場景須要用到多繼承,能夠是要forwardInvocation:來實現。不過不到萬不得已,最好不要這樣作。
// 用於描述被轉發的消息
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
{
NSLog(@"methodSignatureForSelector");
id p1 = [[NSClassFromString(@"BasePerson1") alloc] init];
if ([p1 respondsToSelector:aSelector]) {
return [p1 methodSignatureForSelector:aSelector];
}
id p2 = [[NSClassFromString(@"BasePerson2") alloc] init];
if ([p2 respondsToSelector:aSelector]) {
return [p2 methodSignatureForSelector:aSelector];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation
{
NSLog(@"forwardInvocation");
SEL sel = anInvocation.selector;
id p1 = [[NSClassFromString(@"BasePerson1") alloc] init];
if ([p1 respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p1];
}
id p2 = [[NSClassFromString(@"BasePerson2") alloc] init];
if ([p2 respondsToSelector:sel]) {
[anInvocation invokeWithTarget:p2];
}
}
複製代碼