在介紹動(dòng)態(tài)方法調(diào)用之前,我們先搞清楚方法調(diào)用的本質(zhì)是什么。了解runtime的朋友都知道,OC語言會(huì)在編譯期轉(zhuǎn)換成C語言,所有調(diào)用方法的代碼實(shí)際上主要會(huì)轉(zhuǎn)換成objc_msgSend函數(shù),所以O(shè)C的方法調(diào)用更準(zhǔn)確的說應(yīng)該叫發(fā)送消息。
OC代碼
/* 方法調(diào)用 */
[target doSomeThing:action];
C語言代碼:
/**
objc_msgSend函數(shù)的具體調(diào)用形式
@param id 發(fā)送消息的對(duì)象(target)的類型
@param SEL 方法(@selector(doSomeThing:) )選擇器
@param id 參數(shù)(action)的類型
*/
((id (*)(id,SEL,id))objc_msgSend)(target, @selector(doSomeThing:) ,action);
如果我們直接通過objc_msgSend來調(diào)用方法,那么只需要通過接口傳入方法調(diào)用者、方法選擇器、參數(shù),即可以實(shí)現(xiàn)方法的調(diào)用。那是不是就能實(shí)現(xiàn)動(dòng)態(tài)方法調(diào)用呢?
我們來實(shí)現(xiàn)這個(gè)功能:
+ (id)performTarget:(id)taget selector:(SEL)selector param:(id)param {
((id (*)(id,SEL,id))objc_msgSend)(taget, selector ,param);
}
這是當(dāng)只有一個(gè)參數(shù)的時(shí)候的方法,如果有兩個(gè)參數(shù),那么就要再加一個(gè)方法
+ (id)performTarget:(id)taget selector:(SEL)selector param1:(id)param1 param2:(id)param2 {
((id (*)(id,SEL,id,id))objc_msgSend)(taget, selector ,param1 ,param2);
}
那么假如更多個(gè)參數(shù)呢,每多一個(gè)參數(shù)都需要多加一個(gè)方法。直接調(diào)用objc_msgSend的缺點(diǎn)就顯現(xiàn)出來了,它并不能動(dòng)態(tài)的調(diào)用不同數(shù)量參數(shù)的函數(shù)。
其實(shí)OC的api就自帶了這么一個(gè)方法調(diào)用的機(jī)制:
- (id)performSelector:(SEL)aSelector;
- (id)performSelector:(SEL)aSelector withObject:(id)object;
- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
api默認(rèn)給了3個(gè)方法,不帶參數(shù)的、帶一個(gè)參數(shù)的、帶兩個(gè)參數(shù)的,如果有需要我們可以寫帶更多參數(shù)的方法。但是這不是我們想要的,我們希望不管有多少個(gè)參數(shù),都只需要調(diào)用一個(gè)方法即可實(shí)現(xiàn)。這樣做的好處是顯而易見的,比如我們可以通過后臺(tái)接口返回的數(shù)據(jù)來決定調(diào)用哪個(gè)方法,以及傳什么參數(shù)。
NSInvocation
既然直接發(fā)送消息不能實(shí)現(xiàn)動(dòng)態(tài)方法調(diào)用,那么有沒有辦法能實(shí)現(xiàn)呢。繼續(xù)從runtime尋找答案,runtime機(jī)制中除了發(fā)送消息,還有消息轉(zhuǎn)發(fā)。調(diào)用方法時(shí),如果對(duì)象找不到對(duì)應(yīng)方法名的方法,就會(huì)進(jìn)行消息轉(zhuǎn)發(fā),我們通常會(huì)在項(xiàng)目中交換系統(tǒng)的轉(zhuǎn)發(fā)方法,動(dòng)態(tài)添加新方法,以避免因?yàn)檎{(diào)用為定義的方法而產(chǎn)生的崩潰。而在消息轉(zhuǎn)發(fā)的方法中有一個(gè)NSInvocation類很可疑,我們?nèi)ス俜轿臋n看看它的介紹。

大致的意思是
NSInvocation使用于在對(duì)象和應(yīng)用程序之間存儲(chǔ)和轉(zhuǎn)發(fā)消息,一個(gè)NSInvocation對(duì)象包含了Object-C的所有元素:一個(gè)target、一個(gè)方法選擇器、參數(shù)和返回值
NSInvocation對(duì)象可以被反復(fù)的分配給不同的target,參數(shù)可以在NSInvocation對(duì)象執(zhí)行之前修改,選擇器可以在不改變方法簽名的情況下修改成不同的值,這使得invocation非常靈活并且適合處理有多個(gè)參數(shù)的情況。
NSInvocation不支持調(diào)用其他的類方法來創(chuàng)建對(duì)象,不管這個(gè)方法是有一個(gè)或者多個(gè)參數(shù)。只能使用invocationWithMethodSignature:方法來創(chuàng)建一個(gè)NSInvocation對(duì)象,而不能使用alloc init方法創(chuàng)建對(duì)象。
invocation默認(rèn)不對(duì)內(nèi)部的參數(shù)進(jìn)項(xiàng)強(qiáng)引用,如果這些參數(shù)在你創(chuàng)建或者使用invocation對(duì)象的時(shí)候會(huì)消失,你應(yīng)該手動(dòng)的去強(qiáng)引用這些參數(shù)或者調(diào)用invocation的retainArguments方法讓invocation來強(qiáng)引用這些參數(shù)。
從官方文檔我們可以了解到,NSInvocation非常適用于調(diào)用多個(gè)參數(shù)的情況,具體代碼如下:
/**
多參數(shù)的方法調(diào)用
@param target 發(fā)送消息的對(duì)象
@param action 方法名
@param arguments 參數(shù)數(shù)組
*/
+ (id)performTarget:(id)target action:(NSString *)action arguments:(NSArray *)arguments {
SEL sel = NSSelectorFromString(action);
if (!sel || ![target respondsToSelector:sel]) {
return nil;
}
NSMethodSignature *signature = [target methodSignatureForSelector:sel];
NSUInteger number = signature.numberOfArguments;
if (number < 2 || number > arguments.count + 2) {
return nil;
}
/* NSInvocation不能通過alloc初始化,只能使用invocationWithMethodSignature:方法創(chuàng)建NSInvocation對(duì)象 */
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
invocation.target = target;
invocation.selector = sel;
/* invocation的第一個(gè)參數(shù)是target,第二個(gè)參數(shù)是selector,所以方法的參數(shù)的下標(biāo)要從2開始 */
for (int i = 0; i < arguments.count; i++) {
int index = i + 2;
id object = arguments[i];
void * argument = &object;
const char *type = [signature getArgumentTypeAtIndex:index];
/* 因?yàn)閍rgument傳的是地址,所以對(duì)非對(duì)象類型進(jìn)行轉(zhuǎn)換,這里以NSInteger為例 */
if (strcmp(type, @encode(NSInteger)) == 0) {
NSInteger num = [object unsignedIntegerValue];
argument = #
}
[invocation setArgument:argument atIndex:index];
}
/* 調(diào)用方法 */
[invocation invoke];
void *returnValue;
/* 獲取返回值 */
[invocation getReturnValue:&returnValue];
id result = (__bridge id)returnValue;
return result;
}
這樣不管參數(shù)有多少都可以通過這一個(gè)方法進(jìn)行調(diào)用了。