1. Objective-C 的消息派發(fā)
Objective-C 是動態(tài)語言,所有的消息都是在 Runtime 進(jìn)行派發(fā)的
1.1. objc_msgSend
?最底層的轉(zhuǎn)發(fā)函數(shù)為objc_msgSend
胃珍,它的定義如下
OBJC_EXPORT id objc_msgSend(id self, SEL op, ...)
從以上的定義我們可以得出一個消息轉(zhuǎn)發(fā)包含了幾大要素:target概页、selector催跪、arguments、return value,objc_msgSend
是 C 函數(shù)硬纤,蘋果不提倡我們直接使用該函數(shù)來向?qū)ο笙ⅰ?/p>
1.2. performSelector
想必大家都知道使用 performSelector
給對象發(fā)送消息膳灶,但是其有幾個短板
- 在 ARC 場景下 performSelector 可能會造成內(nèi)存泄漏
-
performSelector
至多接收 2 個參數(shù)咱士,如果參數(shù)多余 2 個,我們就無法使用performSelector
來向?qū)ο蟀l(fā)送消息了轧钓。 - performSelector 限制參數(shù)類型為 id序厉,以標(biāo)量數(shù)據(jù)(int double NSInteger 等)為參數(shù)的方法使用 performSelector 調(diào)用會出現(xiàn)各種各樣詭異的問題
1.3. NSInvocation
NSInvocation 是蘋果工程師們提供的一個高層的消息轉(zhuǎn)發(fā)系統(tǒng)。它是一個命令對象毕箍,可以給任何 Objective-C 對象類型發(fā)送消息弛房,接下來將介紹 NSInvocation 的?用法。
2. NSInvocation 的使用
2.1. 初始化
必須使用工廠方法 invocationWithMethodSignature:
來創(chuàng)建一個 NSInvocation
實(shí)例而柑。工廠方法的參數(shù)是一個 NSMethodSignature
對象文捶。一般使用 NSObject
的實(shí)例方法 methodSignatureForSelector:
或者類方法 instanceMethodSignatureForSelector:
來創(chuàng)建對應(yīng) selector 的 NSMethodSignature 對象。
例:創(chuàng)建類方法的簽名與實(shí)例方法簽名
- (void)createClassMethodSignature:(SEL)selector {
NSMethodSignature *methodSignature = [[self class] methodSignatureForSelector:selector];
}
- (void)createInstanceMethodSignature:(SEL)selector {
NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector];
}
2.2. 接受對象以及選擇子
需要注意的是 NSMethodSignature 對象僅僅表示了方法的簽名:方法的請求媒咳、返回數(shù)據(jù)的編碼粹排。所以在使用 NSMethodSignature 來創(chuàng)建 NSInvocation 對象之后仍需指定消息的接收對象和選擇子。
NSMethodSignature *methodSignature = [[self class] methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setTarget:[self class]];
[invocation setSelector:selector];
原則是接收對象的對應(yīng)選擇子需要跟 NSMethodSignature 相匹配涩澡。但是根據(jù)實(shí)踐來說顽耳,只要不造成 NSInvocation setArgument:atIndex 越界的異常,都是可以成功轉(zhuǎn)發(fā)消息的妙同,并且轉(zhuǎn)發(fā)成功之后射富,未賦值的參數(shù)都將被賦值為 nil。
例如:
- (void)greetingWithInvocation {
NSMethodSignature *methodSignature = [self methodSignatureForSelector:@selector(greetingWithName:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setSelector:@selector(greetingWithAge:name:)];
// NSString *name = @"Tom";
// [invocation setArgument:&name atIndex:3];
NSUInteger age = 10;
[invocation setArgument:&age atIndex:2];
[invocation invokeWithTarget:self];
}
- (void)greetingWithName:(NSString *)name {
NSLog(@"Hello World %@!",name);
}
- (void)greetingWithAge:(NSUInteger)age name:(NSString *)name {
NSLog(@"Hello %@ %ld!", name, (long)age);
}
執(zhí)行結(jié)果:
2017-05-03 16:16:29.815 NSInvocationDemo[50214:49610519] Hello (null) 10!
2.3. 參數(shù)傳遞
- (void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
- (void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
以上為 NSInvocation 類中定義針對參數(shù)的操作粥帚。 argumentLocation 參數(shù)為 void *
類型辉浦,表示需要傳遞指針地址給它。idx 參數(shù)是從 2 開始的茎辐,0 和 1 分別代表 target 和 selector宪郊,雖然可以?直接使用 getArgument:atIndex 來獲取 target 和 selector掂恕,但是不如 NSInvocation 的 target 以及 selector 屬性來的方便
。需要注意的是當(dāng) idx 超過對應(yīng) NSMethodSignature 的參數(shù)個數(shù)的時候獲取參數(shù)和設(shè)置參數(shù)的方法都會拋出 NSInvalidArgumentException 異常弛槐。
例如:給 greetingWithName: 方法傳參
- (void)sendMsgWithInvocation {
NSString *name = @"Tom";
SEL selector = @selector(greetingWithName:);
NSMethodSignature *methodSignature = [self methodSignatureForSelector:selector];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setTarget:self];
[invocation setSelector:selector];
[invocation setArgument:&name atIndex:2];
[invocation invoke];
}
- (void)greetingWithName:(NSString *)name {
NSLog(@"Hello %@!", name);
}
需要特別注意 setArgument:atIndex: 默認(rèn)不會強(qiáng)引用它的 argument懊亡,如果 argument 在 NSInvocation 執(zhí)行的時候之前被釋放就會造成野指針異常(EXC_BAD_ACCESS)。
如上圖所示乎串, invocation 未?強(qiáng)引用它的 target店枣,在控制器彈出之后,target ?被釋放叹誉,然后再 invoke 這個 invocation 會造成野指針異常鸯两。調(diào)用 retainArguments
方法來強(qiáng)引用參數(shù)(包括 target 以及 selector)。
2.4. 返回數(shù)據(jù)
NSInvocation 類中的返回數(shù)據(jù)的方法如下
- (void)getReturnValue:(void *)retLoc;
- (void)setReturnValue:(void *)retLoc;
可以看到返回數(shù)據(jù)仍然是通過傳入指針來進(jìn)傳值的长豁。例:
- (void)plusWithInvocation {
NSMethodSignature *methodSignature = [self methodSignatureForSelector:@selector(plusWithA:B:)];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation retainArguments];
[invocation setTarget:self];
[invocation setSelector:@selector(plusWithA:B:)];
int a = 10;
[invocation setArgument:&a atIndex:2];
int b = 5;
[invocation setArgument:&b atIndex:3];
[invocation invoke];
int result;
[invocation getReturnValue:&result];
NSLog(@"%ld", (long)result);
}
- (int)plusWithA:(int)a B:(int)b {
return a + b;
}
輸出結(jié)果為:
2017-05-03 17:13:31.884 NSInvocationDemo[50948:49713408] 15
需要注意的是:考慮到 getReturnValue 方法僅僅是將返回數(shù)據(jù)拷貝到提供的緩存區(qū)(retLoc)內(nèi)钧唐,并不會考慮到此處的內(nèi)存管理
,所以如果返回數(shù)據(jù)是對象類型的匠襟,實(shí)際上獲取到的返回數(shù)據(jù)是 __unsafe_unretained
類型的钝侠,上層函數(shù)再?把它作為返回數(shù)據(jù)返回的時候就會造成野指針異常。通常的解決方法有2種:
第一種:新建一個相同類型的對象并指向它酸舍,這樣做 result 就會強(qiáng)引用 tempResult帅韧,當(dāng)做返回數(shù)據(jù)返回之后會自動添加 autorelease 關(guān)鍵字,也就不會造成野指針異常啃勉。
NSNumber __unsafe_unretained *tempResult;
[invocation getReturnValue:&tempResult];
NSNumber *result = tempResult;
return result;
第二種:?使用 __bridge 將緩存區(qū)轉(zhuǎn)換為 Objective-C 類型忽舟,這種做法其實(shí)跟第一種相似,但是我們更建議使用這種方式來解決以上問題淮阐,因?yàn)?getReturnValue ?本來就是給緩存區(qū)寫入數(shù)據(jù)叮阅,緩存區(qū)聲明為 void* 類型更為合理,然后通過 __bridge 方式轉(zhuǎn)換為 Objective-C 類型并?且將該內(nèi)存區(qū)的內(nèi)存管理交給 ARC枝嘶。
void *tempResult = NULL;
[invocation getReturnValue:&tempResult];
NSNumber *result = (__bridge NSNumber *)tempResult;
return result;