利用Runtime實現方法交換
分析場景 ? ??在我們的項目開發中,會遇到這樣的問題:如果字符串鏈接里面帶有文字,那它返回的url是空的, “null” ,后面我們再進行網絡請求一個空的url結果..... 。這也就說明NSURL 在創建出來后,系統不會檢測是否為nil。怎么解決?
開始的想法:我可以為NSURL擴展一個方法,在方法實現里判斷 返回的url是否為空,再做一些其他處理,最后再把方法名替換下就行啦。 ? 擴展的方法如下;
+(instancetype)HZ_urlWtihString:(NSString *)str{
NSURL *url = [NSURL URLWithString:str];
if (url == nil) {
NSLog(@"url 空了");
}
return url;
}
要是方法調用次數少,簡單替換下方法名 。但是如果類似于這種情況 并且替換的地方很多呢?就要一個一個找替換 很麻煩。 ?
這時有一個一勞永逸的方法:HOOK(面向切面編程)鉤子思想!!?勾住某個方法的調用,改變方法調用的順序。
繼續說,我們可以不改變系統方法的調用,還是調用URLWithString,但是調用后讓它走到我們自定義方法的實現里。 方法的調用就是發送消息 , 那么調用URLWithString方法 被編譯后C++代碼 就是這樣: ??objc_msgSend(<#id self#>, @selector(URLWithString)) ,只要他發送這消息,我就攔截到 ?,接下來就需要我們做些改變 ??! Runtime 提供了改變方法調用順序的功能 ?,那改變的是什么? ?@selector(URLWithString)這是被編譯后的我們無法改變(改變相當于改變源碼),在@selector之后還有一個機會改變方法的調用?,改變的是方法的IMP
提示方法的組成:SEL方法編號+IMP方法實現(實質:函數指針,它指向一塊內存區域,內存里面的二進制指令)),SEL和IMP是一一對應的。 也可以這樣理解:SEL(書的目錄)--IMP(書的頁碼)-- ?代碼(書里的內容);我們發送消息SEL 它就會找到IMP(IMP不是代碼 它是函數指針)
Runtime可以這么做,它可以改變目錄上面的對應關系,它可以做交換
那什么時候交換?要在一切調用該方法之前做好,main函數里一般不寫,我們可以在分類的.m源文件里,源文件再加載的時候有個+(void)load方法,
//App裝載進內存的時候 就會執行這個指令;(app安裝在手機里都是二進制,點開App第一件事就是將硬盤里的二進制裝進內存里,就會讀load里面的內容,也就是先執行load里面的指令,再啟動APP 執行UIApplicationmain, load比main函數靠前)
+(void)load{
//下鉤子
}
我們在load里面“下鉤子”。我們會用到這個方法來交換:
method_exchangeImplementations(<#Method m1#>, <#Method m2#>)//改變方法實現
我們通過函數獲取m1 m2
+(void)load{
//下鉤子
//? ? class_getInstanceMethod;//獲取實例方法
//獲取類方法,返回Method結構體
Method urlWithString = class_getClassMethod(self, @selector(URLWithString:));
Method HZ_UrlWithString = class_getClassMethod(self, @selector(HZ_urlWtihString:));
//交換方法的實現(相當于交換:“書的頁碼”)
method_exchangeImplementations(urlWithString, HZ_UrlWithString);
}
到現在,方法交換就實現啦,原來的代碼不用動。但是還有一地方沒改,否則會造成循環調用 ,棧溢出:URLWithString 改為HZ_urlWtihString
//HZ_urlWtihString 與 urlWithString 的imp已經交換
+(instancetype)HZ_urlWtihString:(NSString *)str{
NSURL *url = [NSURL HZ_urlWtihString:str];
if (url == nil) {
NSLog(@"url 空了");
}
return url;
}
所謂HOOK,是一種思想,是專門的一種鉤子,他會在內存中找到某一個方法的調用(一塊兒內存區域),直到調用這個方法時,就勾住他,對它的方法行為進行改變,所以Runtime 的方法交換 某種意義上說也是一種鉤子。
動態添加方法
當一個類被調用了沒有被實現的方法,那么他會走這兩種方法的一種
//當類被調用沒有實現的類方法時,調用該方法
+(BOOL)resolveClassMethod:(SEL)sel{
}
//當這個類被調用沒有實現的對象方法時,調用該方法
+(BOOL)resolveInstanceMethod:(SEL)sel{
}
比如在控制器里,一個類Person被調用了一個沒有實現的無參數方法[p performSelector:@selector(eat)];,會走到resolveInstanceMethod:里來,那么我們就可以在該方法中給Person類動態添加方法。使用Runtime添加方法,就會用到這個函數class_addMethod,代碼實例
//當這個類被調用沒有實現的對象方法時,調用該方法
+(BOOL)resolveInstanceMethod:(SEL)sel{
NSLog(@"%@",NSStringFromSelector(sel));
//動態添加方法(方法組成:SEL (方法編號) + IMP(函數指針) + 函數體)
/*
1.cls 目標類
2.sel 方法編號
3.imp 方法實現(函數指針)
4.返回值類型 (無返回值無參的函數 為 "v@:")//v代表viod,@代表一個id對象,:代表方法編號
*/
class_addMethod(self, sel, (IMP)haha, "v@:");
//不知道返回什么,父類返回什么 我這就返回什么
return [super resolveInstanceMethod:sel];
}
//寫一個函數,haha 就是函數指針,這里需要加(IMP)來強制轉化,不然會報警告
void haha(){
NSLog(@"來了!!");
}
以上時添加的無返回值無參數的實例方法,如果調用了一個沒有實現的帶參數的方法,
[p performSelector:@selector(eat) withObject:@"哈哈"];
這就要改變class_addMethod的第四個參數-返回值類型,和haha函數體里的參數。返回值類型改為“v@:”,更改函數體里的參數,有兩個 方法調用的隱式參數?id self,SEL _cmd,
/*
1.方法的調用者
2.方法的編號
(OC 里 每個方法調用,它的函數體里有倆個隱式參數,是默認的)
*/
void eat(id self,SEL _cmd,NSString *str){
NSLog(@"%@",str);
}
為什么會有隱式參數,可能隱約感覺的到,方法調用就是消息發送:
// [p performSelector:@selector(eat) withObject:@"哈哈"];
//調用OC方法的時候,會給IMP傳入兩個參數:方法的調用者 ?id self 、方法編號SEL _cmd (行參名字可以改)
? ? objc_msgSend(p, @selector(eat:),@"哈哈");
這里面的p 和?@selector(eat:) 就傳到?void eat(id self,SEL _cmd,NSString *str)這里來啦。
*另外swift調用方法 和oc ?調用方法的底層是不一樣的。