OC方法的本質(zhì)
首先了解OC方法的本質(zhì)到底是什么:
OC方法由兩個(gè)部分組成:
SEL: 方法編號(hào)(一本書(shū)的目錄編號(hào))
IMP: 方法實(shí)現(xiàn)炼七,是函數(shù)指針饼暑,指向函數(shù)(一本書(shū)的目錄頁(yè)碼期虾,頁(yè)碼指向?qū)?yīng)頁(yè)的內(nèi)容)
動(dòng)態(tài)綁定
簡(jiǎn)單舉個(gè)例子:一個(gè)Person類(lèi)的.m文件中不實(shí)現(xiàn)-(void)eat:(NSString *)
通過(guò)運(yùn)行時(shí)來(lái)動(dòng)態(tài)實(shí)現(xiàn)這個(gè)eat方法驴一,這個(gè)過(guò)程叫做 動(dòng)態(tài)綁定 :
#import <objc/message.h>
+(BOOL)resolveInstanceMethod:(SEL)sel{
// 給類(lèi)添加eat方法慌植,IMP==eat
if ([NSStringFromSelector(sel) isEqualToString:@"eat"]) {
//self:方法調(diào)用者甚牲,sel:方法編號(hào),eat:(IMP函數(shù)指針)方法實(shí)現(xiàn)
class_addMethod(self, sel, eat, “”);
}
return [super resolveInstanceMethod:sel];
}
// 實(shí)現(xiàn)c函數(shù)eat()
void eat(id self, SEL _cmd, NSString *objc){
NSLog(@”我來(lái)了%@”, objc);
}
OC方法調(diào)用 會(huì)傳遞兩個(gè) 隱式參數(shù) self, _cmd蝶柿,self 是方法的調(diào)用者丈钙,_cmd 是方法編號(hào);
OC的方法調(diào)用其實(shí)是 消息發(fā)送 (通過(guò)終端clang –rewrite-objc main.m,生成一個(gè).cpp文件交汤,可以看到.m文件的底層實(shí)現(xiàn)雏赦。)
Person *p = [[Person alloc]init];
//[P eat:@”漢堡”]; 的底層就是objc_msgSend函數(shù)
objc_msgSend(P, @selector(eat:), @”漢堡”);
消息轉(zhuǎn)發(fā)
重定向
當(dāng)對(duì)象的方法簽名在頭文件中暴漏出來(lái),而在.m文件中忘記實(shí)現(xiàn)芙扎,一般程序會(huì)報(bào)運(yùn)行時(shí)錯(cuò)誤不識(shí)別的選擇器星岗,通過(guò)消息轉(zhuǎn)發(fā)可以改變這行為。
消息轉(zhuǎn)發(fā): 當(dāng)對(duì)象接收到與其 方法集 不匹配的消息時(shí)戒洼,通過(guò)消息轉(zhuǎn)發(fā)機(jī)制可以使對(duì)象執(zhí)行用戶預(yù)先定義的邏輯俏橘,如:將消息發(fā)送給能夠做出響應(yīng)的其他接收器(對(duì)象),或者將所有無(wú)法識(shí)別的消息都發(fā)送給同一個(gè)
接收器 再或者 默默的吞下消息(既不執(zhí)行處理過(guò)程也不使程序拋出運(yùn)行時(shí)錯(cuò)誤)圈浇。
還是上面的例子寥掐,在Dog類(lèi)中實(shí)現(xiàn)了eat方法例获,在Person類(lèi)中可以通過(guò) 消息轉(zhuǎn)發(fā) 讓Dog去相應(yīng)eat(我吃不了,dog你幫我吃吧)
//消息重定向
-(id)forwardingTargetForSelector:(SEL)aSelector{
if ([_dog respondsToSelector:aSelector]) {
return _dog; // 相當(dāng)于 [_dog performSelector:aSelector];
}
// 給nil發(fā)消息
return nil;
}
方法簽名
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if(aSelector == @selector(eat:)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:@"]; // v@:@ (type encoding)
}
return nil;
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (anInvocation.selector == @selector(eat)) {
Dog *dog = [[Dog alloc] init];
[anInvocation invokeWithTarget:dog];
}
}
重寫(xiě)methodSignatureForSelector:和forwardInvocation:方法曹仗,將方法簽名,轉(zhuǎn)發(fā)給真正實(shí)現(xiàn)了該方法的目標(biāo)對(duì)象,讓其去調(diào)用已實(shí)現(xiàn)的方法蠕搜。
methodSignatureForSelector:的作用在于為另一個(gè)類(lèi)實(shí)現(xiàn)的消息創(chuàng)建一個(gè)有效的方法簽名怎茫,必須實(shí)現(xiàn),并且返回不為空的methodSignature妓灌,否則會(huì)crash
forwardInvocation:將選擇器轉(zhuǎn)發(fā)給一個(gè)真正實(shí)現(xiàn)了該消息的對(duì)象轨蛤。
1.forwardingTargetForSelector同為消息轉(zhuǎn)發(fā),但在實(shí)踐層面上有什么區(qū)別虫埂?何時(shí)可以考慮把消息下放到forwardInvocation階段轉(zhuǎn)發(fā)祥山?
forwardingTargetForSelector 僅支持一個(gè)對(duì)象的返回,也就是說(shuō)消息只能被轉(zhuǎn)發(fā)給一個(gè)對(duì)象掉伏。比如轉(zhuǎn)發(fā)給一個(gè)專(zhuān)門(mén)用于處理未識(shí)別的方法的處理類(lèi)缝呕。
forwardInvocation 可以將消息同時(shí)轉(zhuǎn)發(fā)給任意多個(gè)對(duì)象,如果你想執(zhí)行其他邏輯(如記錄日志并吞下該消息)斧散,可以考慮用 forwardInvocation
? 關(guān)于 signatureWithObjCTypes: 中的objcTypes供常,是OC的類(lèi)型編碼 Type Encodings【相關(guān)文檔鏈接】
? 語(yǔ)法參照?qǐng)D:
前面提到OC方法調(diào)用 會(huì)傳遞兩個(gè) 隱式參數(shù) self, _cmd,self 是方法的調(diào)用者鸡捐,_cmd 是方法編號(hào)栈暇,指向方法本身。
例如:-(void)eat:(NSString *)food;實(shí)際上有三個(gè)參數(shù):self, _cmd和food箍镜。
將 eat: 轉(zhuǎn)ObjcTypes為:
"返回值類(lèi)型 第一參數(shù) 第二參數(shù) [第三參數(shù)...]"
如果沒(méi)有返回值用v源祈,如果有用@代替id類(lèi)型,
第一二參數(shù)是必須存在的色迂,即 id 類(lèi)型的 self香缺,和 SEL 類(lèi)型的 _cmd,第三參數(shù)是用戶自定義的參數(shù)脚草,可有可無(wú)例如: "v@:@" : void id類(lèi)型的self SEL類(lèi)型的_cmd 自定義參數(shù)赫悄;
"@@:" :id類(lèi)型的返回值 id類(lèi)型的self SEL類(lèi)型的_cmd
因此我們可以調(diào)用[anInvocation getArgument: atIndex:] 獲取指定的參數(shù)值
Runtime應(yīng)用場(chǎng)景—HOOK(鉤子)
HOOK,方法欺騙
直接上例子:
/*當(dāng)url中含有中文時(shí)(需要轉(zhuǎn)碼)馏慨,request還是能創(chuàng)建埂淮,但是此時(shí)
request中的url為空,request的創(chuàng)建方法沒(méi)有檢測(cè)url為空的情況写隶,
很容易出現(xiàn)難以定位的bug(Swift中有可選類(lèi)型倔撞,可以避免這問(wèn)題)。
*/
NSURL *url = [NSURL URLWithString:@”http://www.baidu.com/中文”];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
解決:①用 category 創(chuàng)建分類(lèi) NSURL+HOOK 及XW_URLWithString 方法慕趴,將用到 URLWithString 的地方替換成我們自己的方法 XW_URLWithString 痪蝇, XW_URLWithString 保留了 URLWithString 原本內(nèi)部創(chuàng)建 url 的方式鄙陡,添加 url 是否為空的判斷。
不足:每次都要去導(dǎo)入頭文件躏啰,每次都要去替換項(xiàng)目中原本的urlWithString方法趁矾,比較麻煩;
②利用runtime運(yùn)行時(shí)给僵,改變方法調(diào)用的順序毫捣。
OC發(fā)送 URLWithString 消息會(huì)對(duì)應(yīng)的的去找這個(gè)方法的實(shí)現(xiàn),用運(yùn)行時(shí)可以去改變這種一一對(duì)應(yīng)的關(guān)系帝际,
只要HOOK住 URLWithString 這個(gè)方法的調(diào)用蔓同,當(dāng)發(fā)送 URLWithString(SEL)消息時(shí),讓它去找 XW_URLWithString(IMP)這個(gè)實(shí)現(xiàn)蹲诀。
NSURL+HOOK.m 文件中在 +(void)load 方法中下鉤子HOOK斑粱,以交換方法的IMP實(shí)現(xiàn)。
#import<objc/runtime.h>
+(void)load {
//獲取method
Method URLWithStr = class_getClassMethod(self, @selector(URLWithString:));
Method XWURLWithStr = class_getClassMethod(self, @selector(XW_URLWithString:));
//交換方法的IMP
method_exchangeImplementations(URLWithStr, XWURLWithStr);
}
外界不需要導(dǎo)入 NSURL+HOOK.h 脯爪,也不需要修改 URLWithString 為 XW_URLWithString 就能直接把 URLWithString 方法實(shí)現(xiàn)替換则北。
XW_URLWithString 實(shí)現(xiàn)如下:
+(void) XW_URLWithString:(NSString*)URLString{
// 保留系統(tǒng)原本的實(shí)現(xiàn),實(shí)現(xiàn)交換后這里不能用URLWithString披粟,否則會(huì)遞歸
NSURL *url = [NSURL XW_URLWithString:URLString];
if(url == nil){
NSLog(@"空了");
}
return url;
}