OC方法的本質(zhì)
首先了解OC方法的本質(zhì)到底是什么:
OC方法由兩個(gè)部分組成:
SEL: 方法編號(hào)(一本書的目錄編號(hào))
IMP: 方法實(shí)現(xiàn),是函數(shù)指針,指向函數(shù)(一本書的目錄頁碼,頁碼指向?qū)?yīng)頁的內(nèi)容)
動(dòng)態(tài)綁定
簡(jiǎn)單舉個(gè)例子:一個(gè)Person類的.m文件中不實(shí)現(xiàn)-(void)eat:(NSString *)
通過運(yùn)行時(shí)來動(dòng)態(tài)實(shí)現(xiàn)這個(gè)eat方法,這個(gè)過程叫做 動(dòng)態(tài)綁定 :
#import <objc/message.h>
+(BOOL)resolveInstanceMethod:(SEL)sel{
// 給類添加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(@”我來了%@”, objc);
}
OC方法調(diào)用 會(huì)傳遞兩個(gè) 隱式參數(shù) self, _cmd,self 是方法的調(diào)用者,_cmd 是方法編號(hào);
OC的方法調(diào)用其實(shí)是 消息發(fā)送 (通過終端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ì)象的方法簽名在頭文件中暴漏出來,而在.m文件中忘記實(shí)現(xiàn),一般程序會(huì)報(bào)運(yùn)行時(shí)錯(cuò)誤不識(shí)別的選擇器,通過消息轉(zhuǎn)發(fā)可以改變這行為。
消息轉(zhuǎn)發(fā): 當(dāng)對(duì)象接收到與其 方法集 不匹配的消息時(shí),通過消息轉(zhuǎn)發(fā)機(jī)制可以使對(duì)象執(zhí)行用戶預(yù)先定義的邏輯,如:將消息發(fā)送給能夠做出響應(yīng)的其他接收器(對(duì)象),或者將所有無法識(shí)別的消息都發(fā)送給同一個(gè)
接收器 再或者 默默的吞下消息(既不執(zhí)行處理過程也不使程序拋出運(yùn)行時(shí)錯(cuò)誤)。
還是上面的例子,在Dog類中實(shí)現(xiàn)了eat方法,在Person類中可以通過 消息轉(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];
}
}
重寫methodSignatureForSelector:和forwardInvocation:方法,將方法簽名,轉(zhuǎn)發(fā)給真正實(shí)現(xiàn)了該方法的目標(biāo)對(duì)象,讓其去調(diào)用已實(shí)現(xiàn)的方法。
methodSignatureForSelector:的作用在于為另一個(gè)類實(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ì)象的返回,也就是說消息只能被轉(zhuǎn)發(fā)給一個(gè)對(duì)象。比如轉(zhuǎn)發(fā)給一個(gè)專門用于處理未識(shí)別的方法的處理類。
forwardInvocation 可以將消息同時(shí)轉(zhuǎn)發(fā)給任意多個(gè)對(duì)象,如果你想執(zhí)行其他邏輯(如記錄日志并吞下該消息),可以考慮用 forwardInvocation
? 關(guān)于 signatureWithObjCTypes: 中的objcTypes,是OC的類型編碼 Type Encodings【相關(guān)文檔鏈接】
? 語法參照?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為:
"返回值類型 第一參數(shù) 第二參數(shù) [第三參數(shù)...]"
如果沒有返回值用v,如果有用@代替id類型,
第一二參數(shù)是必須存在的,即 id 類型的 self,和 SEL 類型的 _cmd,第三參數(shù)是用戶自定義的參數(shù),可有可無例如: "v@:@" : void id類型的self SEL類型的_cmd 自定義參數(shù);
"@@:" :id類型的返回值 id類型的self SEL類型的_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)建方法沒有檢測(cè)url為空的情況,
很容易出現(xiàn)難以定位的bug(Swift中有可選類型,可以避免這問題)。
*/
NSURL *url = [NSURL URLWithString:@”http://www.baidu.com/中文”];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
解決:①用 category 創(chuàng)建分類 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;
}