OC,objc_msgSend()
簡介:用一句話簡介消息傳遞那就是:用一個 C語言函數(shù)欢搜,向一個實例傳遞一個字符串封豪,實例拿到字符串后與自己的method_list中的SEL比較,遇到一樣的就找到對應(yīng)IMP執(zhí)行炒瘟。SEL就是一個字符串吹埠,IMP就是一個函數(shù)指針,在一個Method結(jié)構(gòu)體中封裝著這兩者疮装,因此他倆一一對應(yīng)缘琅。
- 在向一個實例發(fā)送消息時[obj doSomthing];實際上是調(diào)用的這個函數(shù)objc_msgSend(obj,@selector(doSomething));(該函數(shù)在<objc/message.g>中)
- 接下來將從頭到尾表述在這個函數(shù)中究竟干了多少事情。
- 階段1(對于能獨(dú)立解決的問題):
- 尋找類:根據(jù)上一篇的OC是如何使C語言變得面向?qū)ο蟮睦疲酪粋€objc_object結(jié)構(gòu)體中就只有isa(一個指向objc_class的指針)刷袍,因此需要從他的objc_class結(jié)構(gòu)體中查找函數(shù),于是順著isa指針找到了這個objc_class樊展。
- 訪問cache:objc_cache結(jié)構(gòu)體中裝著一個method結(jié)構(gòu)體的數(shù)組呻纹,先在這個數(shù)組中遍歷method,對于每一個method专缠,要對照method中的SEL與上面?zhèn)鬟f進(jìn)來的@selector(doSomething)是不是一樣雷酪,如果一樣就確認(rèn)是需要調(diào)用它的IMP。
- 訪問method_list:如果在步驟2中沒有找到一樣的SEL藤肢,就需要遍歷所有的函數(shù)了太闺,與遍歷cache時一樣,對照SEL嘁圈,選取IMP省骂。如果在這個步驟中仍然不能找到對應(yīng)的SEL蟀淮,那么就會進(jìn)入階段2(不能獨(dú)立解決的問題)。
- 假如通過上述1-3過程找到了method钞澳,之后執(zhí)行對應(yīng)的IMP就可以了怠惶。
- 階段2(對于不能獨(dú)立解決的問題):
- 尋找父類:在階段1中,當(dāng)在cache和method_list中不能找到一致的SEL時轧粟,msgSend函數(shù)會繼續(xù)往祖墳上刨策治,它根據(jù)objc_class中的super指針(指向父類的objc_class結(jié)構(gòu)體的指針)找到該類的父類,然后在父類的cache和method_list中執(zhí)行階段1的2兰吟、3步一樣的操作通惫。
- 尋找父類的父類:如果在這個時候還是不能找到對應(yīng)的SEL,就會繼續(xù)根據(jù)super指針繼續(xù)尋找父類混蔼,直到super指針是nil履腋,說明沒有一個人可以響應(yīng)這條消息。這個時候如果沒有一些階段3的防護(hù)措施就會報錯了惭嚣,通常是unrecognized selector的錯誤遵湖。
- 階段3(所有子類和父類都不能解決的問題):
- 對于我們沒有預(yù)測到的一些錯誤調(diào)用(使用performSelector傳入一個隨便的selector)或者在實現(xiàn)上的一些失誤(比如你答應(yīng)了某個協(xié)議去實現(xiàn)他的一些方法,然后在implement中沒有寫對應(yīng)的實現(xiàn)晚吞,而協(xié)議的另一端正在調(diào)用協(xié)議中的方法)延旧。有三種補(bǔ)救方式允許我們避免程序的崩潰:在resolve(Instance/Class)Method中做檢查、提供另外一個可以供轉(zhuǎn)發(fā)的對象槽地、使用NSInvocation重新調(diào)用迁沫。
- 在resolveMethod中做檢查(這里均以實例方法為例):首先進(jìn)入resolveInstanceMethod函數(shù),注意這個方法不會影響階段1和階段2的所有過程捌蚊,也就是說弯洗,執(zhí)行了這個方法之后,如果仍然找不到對應(yīng)的SEL逢勾,依然是會報錯的牡整。因此我們可以在這個方法中,動態(tài)地添加其實現(xiàn)溺拱。創(chuàng)建TestObj類如下逃贝,啥屬性啥方法都沒有,卻能向他發(fā)送任何實例消息迫摔。原因就是沐扳,在resolveInstanceMethod方法中接收到一個找不到的SEL,不論這個SEL叫什么句占,我們都給它一個默認(rèn)的實現(xiàn)defaultDealMethod沪摄。因此,TestObj永遠(yuǎn)都不會引起崩潰。
#import <Foundation/Foundation.h>
@interface TestObj : NSObject
@end
#import "TestObj.h"
#import <objc/runtime.h>
@implementation TestObj
+ (BOOL)resolveInstanceMethod:(SEL)sel{
NSString *str = NSStringFromSelector(sel);
NSLog(@"TestObj沒有找到方法:%@杨拐,來到了resolveMethod中",str);
class_addMethod(self, sel, (IMP)defaultDealMethod, "v@:");
return YES;
}
void defaultDealMethod(id self, SEL sel){
NSString *str = NSStringFromSelector(sel);
NSLog(@"DefaultDealMethod:%@",str);
}
@end
- 第二個保障祈餐,就是為這個不能處理的對象找一個可以求助的實例。為了告知這個實例是誰哄陶,需要重寫方法forwardingTargetForSelector:(SEL)sel方法帆阳。既然剛剛有一個無所不能的TestObj方法,那就讓他來擔(dān)當(dāng)這個求助對象吧屋吨,雖然他沒做什么實質(zhì)工作蜒谤,但能保證不崩潰。我們在測試的ViewController中這樣寫至扰,雖然隨便調(diào)用了兩個沒有實現(xiàn)的方法鳍徽,但是并沒有崩潰,因為他成功向TestObj求助了敢课,而TestObj為了幫助它旬盯,在自己的方法列表中加入了兩個新的方法,這樣就可以查找到SEL并成功執(zhí)行翎猛。
- (void)viewDidLoad {
[super viewDidLoad];
objc_msgSend(self, @selector(asdasdasdadsas));
objc_msgSend(self, @selector(asdakjsdajdjka));
}
- (id)forwardingTargetForSelector:(SEL)aSelector{
return [[TestObj alloc] init];
}
3.第三個保障:使用Invocation調(diào)用(個人不大明白這樣做的必要性),Invocation類似設(shè)計模式中的命令模式接剩,就是將一個命令或者是調(diào)用封裝為一個Obj切厘,Invocation中大體包含如下屬性:一個MethodSignature(格式)、target(目標(biāo)懊缺,也就是消息發(fā)送的對象)疫稿、selector(方法名);一個MethodSignature中又大體包含如下屬性:參數(shù)個數(shù)鹃两、方法長度遗座、返回值類型、返回值長度俊扳。由此可見途蒋,一個Invocation就是對一次調(diào)用函數(shù)各方面格式、目標(biāo)的封裝馋记。因此我們需要做的就是在最后一條保障中号坡,規(guī)定一個Invocation,并使用這個Invocation完成調(diào)用梯醒。
- (id)forwardingTargetForSelector:(SEL)aSelector{
//return [[TestObj alloc] init];
return nil;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation{
[anInvocation invokeWithTarget:[[TestObj alloc] init]];
}
- Signature是Invocation的組成元素宽堆,因此在得到Invocation前需獲取到這些格式信息,對于這個格式:第一位是返回值茸习,可以是c(char)畜隶、i(int)、s(short)、l(long)等等籽慢。第二位是接收的第一個參數(shù)self浸遗,因此是"@",第三位是selector用":"表示嗡综,之后的位置就都是自定義的參數(shù)了乙帮,這些類型在官網(wǎng)上可以查到,后面給出一些常用的极景。舉個例子察净,這樣一個函數(shù):
- (instancetype)initWithName:(NSString *)name height:(float)height weight:(float)weight father:(id)father mother:(id)mother;
- 它的格式就可以這樣表示"@@:@ff@@";
- 每一位分別對應(yīng) instance返回值、默認(rèn)參數(shù)self盼樟、默認(rèn)參數(shù)SEL氢卡、參數(shù)name、參數(shù)height晨缴、參數(shù)weight译秦、參數(shù)father、參數(shù)mother击碗。
char:c
int:i
short:s
long:l
longlong:q
//上面這些都大寫就代表是 unsigned的,如 unsigned int:I
float:f
double:d
bool:B
void:v
id:@
SEL: :
如果我們在methodSignature中返回了一個非nil的signature筑悴,系統(tǒng)就會為我們創(chuàng)建一個invocation并調(diào)用forwardingInvocation方法,在該方法中我們會得到之前預(yù)定好的格式的invocation稍途,我們就可以invoke這個invocation了阁吝。
當(dāng)然,如果最后第三個保障也沒有好好利用的話就只有崩潰了械拍。