前言
最近在整理博客,發(fā)現(xiàn)自己之前寫的關(guān)于Runtime攔截替換方法的一篇文章《12- Runtime基礎(chǔ)使用場景-攔截替換方法(class_addMethod ,class_replaceMethod和method_exchangeImplementations)》,大家還是很關(guān)注的,文章大家看完依然疑問,但是由于當(dāng)時生產(chǎn)力不足和后續(xù)的種種原因,并沒有補發(fā)文章。最近工作不忙崖媚,正好結(jié)合自己工作中遇到的Runtime使用場景,補上自己這個坑恤浪。
之前留給大家的疑惑主要有兩點畅哑,如下:
-
第一,下邊這三個方法的具體作用:
class_addMethod
class_replaceMethod
method_exchangeImplementations
第二资锰,為什么方法交換需要用到
class_addMethod
和class_replaceMethod
這兩個方法敢课?
要回答這兩個問題,我們有必要了解一下OC中的類對象結(jié)構(gòu)和消息機制绷杜,往下看直秆。
類結(jié)構(gòu)
OC中的類對象結(jié)構(gòu)和消息機制包含的內(nèi)容其實很多,避免篇幅過長鞭盟,這里我只簡單的說一下和本文相關(guān)的部分圾结。
首先,在OC中有實例對象齿诉、類對象筝野、元類對象晌姚,如下:
[Student class]; // 是類對象
Student *stu = [Student new]; // p是實例對象
object_getClass([Student class]) // 元類
類對象
(即我們?nèi)粘=蟹Q為的類
),它是基于實例對象的一種抽象定義歇竟,比如說喵咪小花都屬于貓挥唠,那么貓就是一種抽象的概念,定義了貓的外形焕议、活動特定等等屬性宝磨。我們用代碼的方法可以這么定義:
貓 *小花 = [貓 new];
所以O(shè)C中,類對象也是一個定義了一個實例對象包含了哪些方法盅安、屬性唤锉、父類是誰等信息的抽象對象。OC的底層是c/c++實現(xiàn)别瞭,所以O(shè)C中的類結(jié)構(gòu)是采用c++中的結(jié)構(gòu)體來表示的窿祥。下邊是我寫的一個偽結(jié)構(gòu)體,從偽結(jié)構(gòu)體中我們可以知道蝙寨,類對象中有方法列表
晒衩、父類
這個信息。
struct objc_class {
Class super_class // 當(dāng)前類的父類
struct objc_method_list * * methodLists // 方法列表
//...... 其它信息這里忽略
}
(類對象的真實結(jié)構(gòu)并不是這樣籽慢,這里我們也可以忽略它的真實結(jié)構(gòu)浸遗。即便你日后了解了類的真實結(jié)構(gòu),也不會影響到下邊的結(jié)論箱亿。)
super_class 就是當(dāng)前類的父類。
methodLists實際上是一個數(shù)組弃秆,保存著類有哪些方法届惋。這里可以提到元類了,實例對象是一種結(jié)構(gòu)菠赚,而類對象和元類對象是另外一種結(jié)構(gòu)脑豹。關(guān)于類對象和元類對象的區(qū)別,對于本文只要記住一個:對象方法是保存在類對象的methodLists中衡查,而類方法保存在元類的methodLists中
瘩欺。
methodLists數(shù)組中保存著結(jié)構(gòu)體method_t,這個結(jié)構(gòu)體包含了我們平時寫的方法的信息拌牲。偽結(jié)構(gòu)體如下:
struct method_t {
SEL method_name
IMP method_imp
char * types
}
SEL method_name
sel就是方法的名字
IMP method_imp
imp保存著一個指針俱饿,這個指針指向函數(shù)的具體實現(xiàn)地址。 所以塌忽,方法真正的實現(xiàn)是單獨保存在一個地方的拍埠,它的實現(xiàn)地址交給imp保存。當(dāng)我們執(zhí)行方法的時候土居,實際上是從method_t結(jié)構(gòu)體中找到imp枣购,然后調(diào)用嬉探。
這里有一點很重要,Runtime替換方法實際上就是替換imp棉圈。所以產(chǎn)生了替換了方法之后涩堤,明明你調(diào)用的是methodA,但是執(zhí)行的是methodB的效果分瘾。因為methodA對應(yīng)的method_t結(jié)構(gòu)體中的imp實際保存的是methodB方法的實現(xiàn)地址了胎围。
types
可以簡單的認(rèn)為到能代表這個方法的特定字符,用法我們暫時忽略芹敌。 有一點需要注意痊远,如果你修改了imp為新的imp外,同時修改types改成新方法的types氏捞,這樣才是真正把method_t結(jié)構(gòu)體改成了新的方法碧聪。
消息機制
一般在OC中調(diào)用方法,底層會轉(zhuǎn)成一個objc_msgSend的c++函數(shù)液茎。比如說:[stu instanceMthod]
逞姿,底層實際上是
objc_msgSend([Student class], @Seletor(instanceMthod))
表示我們從Person這個類對象結(jié)構(gòu)體中查找instanceMthod這個方法,找到它并且調(diào)用捆等。查找調(diào)用這個方法的過程滞造,我們可以簡單認(rèn)為就是消息機制。 下邊我們簡單說一下消息機制的流程栋烤,還是用[stu instanceMthod]
來作為例子:
假如這個方法在Student的父類Person中
- 首先谒养,實例對象p在對應(yīng)的類對象Student的方法列表中methodLists查找instanceMthod方法,沒有找到明郭。(如果能找到买窟,那么就直接調(diào)用對應(yīng)method_t結(jié)構(gòu)中的imp執(zhí)行方法,結(jié)束查找)
- 通過類對象Student的superClass找到父類對象Person薯定,在父類的的方法列表中methodLists查找instanceMthod方法始绍,找到了,調(diào)用方法话侄,結(jié)束查找亏推。(如果在父類對象Person、以及Person的父類對象NSObject沒有找到該方法年堆,那么會進入消息轉(zhuǎn)發(fā)的另外兩個階段吞杭,如果這兩個階段還是沒有找到要調(diào)用的方法,那么就會報經(jīng)典錯誤
unrecognized selector sent to instance
)
當(dāng)然這個消息機制的過程是非常簡陋的嘀韧,實際上在進入methodLists查找之前篇亭,會先進入方法緩存cache中查找,有興趣你可以自己多了解一下锄贷。
三個方法的作用
class_addMethod
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
作用:就是動態(tài)在類對象cls中添加一個方法译蒂。參數(shù)SEL
曼月、IMP
、types
我們上邊都提到過柔昼。
注意:如果cls中已經(jīng)有了要添加的方法聲明和方法實現(xiàn)哑芹,那么添加失敗,返回NO捕透。如果沒有聲明和實現(xiàn)方法聪姿,或者只聲明沒有方法實現(xiàn),都可以添加成功乙嘀,返回YES末购。
class_replaceMethod
IMP class_replaceMethod(Class cls, SEL name, IMP newImp, const char *newTypes)
作用:把類對象中的cls的方法的imp替換成newImp,同時還需要替換newTypes虎谢。
method_exchangeImplementations
void method_exchangeImplementations(Method m1, Method m2)
作用:Method這里可以認(rèn)為就是上邊說到的method_t結(jié)構(gòu)體盟榴,交換m1和m2,實際上就是交換兩個method_t結(jié)構(gòu)體中的IMP和types婴噩。
注意:如果imp為nil擎场,交換操作將失敗。
為什么會用到dispatch_once
dispatch_once我們?nèi)粘W铋L用的就是單例几莽。保證在程序運行過程中迅办,其代碼塊內(nèi)的代碼只執(zhí)行一次。Runtime交換方法之所以會用到dispatch_once章蚣,是為了防止load被手動調(diào)用站欺。 load方法的調(diào)用時機是在main函數(shù)被調(diào)用之前,且只被系統(tǒng)調(diào)用一次纤垂。正常情況下镊绪,我們無需再手動調(diào)用load方法,但是為了防止意外洒忧,所以加了dispatch_once,保證替換方
法的Runtime代碼只能執(zhí)行一次够颠,從而避免方法有替換回去熙侍。
method_exchangeImplementations
一般我們交換方法實現(xiàn)的場景比較明確,比如替換蘋果API中的類的某個方法或者第三方框架中的類的某個方法履磨。
例子如下:
@interface LLPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LLPerson
- (void)personInstanceMethod{
NSLog(@"person對象方法, 調(diào)用者:%@ 方法:%s", self,__FUNCTION__);
}
@end
@interface LLPerson (huan)
@end
@implementation LLPerson (huan)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"------// 替換開始 //------");
NSLog(@"%@", self);
Method oriMethod = class_getInstanceMethod(self, @selector(personInstanceMethod));
Method swiMethod = class_getInstanceMethod(self, @selector(new_personInstanceMethod));
method_exchangeImplementations ( oriMethod, swiMethod) ;
NSLog(@"------// 替換完畢 //------");
});
}
- (void)new_personInstanceMethod {
NSLog(@"Person中的新方法, 調(diào)用者:%@蛉抓, 方法:%s",self,__FUNCTION__);
[self new_personInstanceMethod];
}
@end
調(diào)用代碼如下:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"------------");
LLStudent *stu = [LLStudent new];
[stu personInstanceMethod];
}
@end
結(jié)果如下:
2020-01-07 22:19:27.096634+0800 RunTime1[1972:190371] ------// 替換開始 //------
2020-01-07 22:19:27.097367+0800 RunTime1[1972:190371] LLPerson
2020-01-07 22:19:27.097739+0800 RunTime1[1972:190371] ------// 替換完畢 //------
2020-01-07 22:19:27.203340+0800 RunTime1[1972:190371] ------------
2020-01-07 22:19:27.203516+0800 RunTime1[1972:190371] Person中的新方法, 調(diào)用者:<LLStudent: 0x600003520330>, 方法:-[LLPerson(huan) new_personInstanceMethod]
2020-01-07 22:19:27.203679+0800 RunTime1[1972:190371] person對象方法, 調(diào)用者:<LLStudent: 0x600003520330> 方法:-[LLPerson personInstanceMethod]
這種情況非常簡單剃诅,我們明確的知道了LLPerson中的方法聲明和方法實現(xiàn)巷送,只需要在分類中直接交換就可以了。不需要其它的額外代碼矛辕。 下邊介紹一種特殊的情況笑跛,請往下看付魔。
為什么會用到class_addMethod、class_replaceMethod
下邊要講的這種情況是在我開發(fā)過程中遇到的飞蹂,如果只是用method_exchangeImplementations進行方法交換之后几苍,運行會出現(xiàn)crash。
場景:Person聲明了某個方法并且實現(xiàn)了方法陈哑,然后Student繼承Person妻坝,沒有重寫父類的這個方法,依然不影響直接調(diào)用和使用Person中的這個方法惊窖。 如果此時我們在Student的分類中交換父類的這個方法刽宪,會發(fā)生了什么?
代碼如下:
@interface LLPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LLPerson
- (void)personInstanceMethod{
NSLog(@"person對象方法, 調(diào)用者:%@ 方法:%s", self,__FUNCTION__);
}
@end
@interface LLStudent : LLPerson
@end
@implementation LLStudent
@end
@interface LLStudent (huan)
@end
@implementation LLStudent (huan)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"------// 替換開始 //------");
NSLog(@"%@", self);
Method oriMethod = class_getInstanceMethod(self, @selector(personInstanceMethod));
Method swiMethod = class_getInstanceMethod(self, @selector(studentInstanceMethod));
method_exchangeImplementations ( oriMethod, swiMethod) ;
NSLog(@"------// 替換完畢 //------");
});
}
- (void)studentInstanceMethod {
NSLog(@"Student中的新方法, 調(diào)用者:%@界酒, 方法:%s",self,__FUNCTION__);
[self studentInstanceMethod];
}
@end
調(diào)用代碼:
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"------------");
LLStudent *stu = [LLStudent new];
[stu personInstanceMethod];
NSLog(@"------------");
LLPerson *person = [LLPerson new];
[person personInstanceMethod];
}
@end
運行結(jié)果:
2020-01-07 22:41:10.744351+0800 RunTime1[2421:244815] ------// 替換開始 //------
2020-01-07 22:41:10.745134+0800 RunTime1[2421:244815] LLStudent
2020-01-07 22:41:10.745427+0800 RunTime1[2421:244815] ------// 替換完畢 //------
2020-01-07 22:41:10.852287+0800 RunTime1[2421:244815] ------------
2020-01-07 22:41:10.852488+0800 RunTime1[2421:244815] Student中的新方法, 調(diào)用者:<LLStudent: 0x600003964160>圣拄, 方法:-[LLStudent(huan) studentInstanceMethod]
2020-01-07 22:41:10.852652+0800 RunTime1[2421:244815] person對象方法, 調(diào)用者:<LLStudent: 0x600003964160> 方法:-[LLPerson personInstanceMethod]
2020-01-07 22:41:10.852797+0800 RunTime1[2421:244815] ------------
2020-01-07 22:41:10.852948+0800 RunTime1[2421:244815] Student中的新方法, 調(diào)用者:<LLPerson: 0x600003958050>, 方法:-[LLStudent(huan) studentInstanceMethod]
2020-01-07 22:41:10.853127+0800 RunTime1[2421:244815] -[LLPerson studentInstanceMethod]: unrecognized selector sent to instance 0x600003958050
// 崩潰
具體的崩潰位置是LLStudent+huan分類中的[self studentInstanceMethod];
這一行盾计∈鄣#看到這里,可能大家會有點蒙署辉,會有下邊這幾個問題:
- 不是已經(jīng)交換了父類的方法嗎族铆,為什么執(zhí)行[person personInstanceMethod]會crash呢?
- 為什么
[stu personInstanceMethod]
執(zhí)行之后沒有crash哭尝? - 為什么在父類的分類中交換方法就沒有問題呢哥攘?
- 怎么處理這個問題呢?
分析如下:
問題一:為什么執(zhí)行[person personInstanceMethod]會crash呢材鹦?
注意逝淹,上邊的代碼是在子類Student的分類中把父類的方法和子類的方法進行了交換。交換之后如下:
當(dāng)執(zhí)行[person personInstanceMethod]時桶唐,實際上是執(zhí)行子類中的studentInstanceMethod方法栅葡,首先調(diào)用NSLog,輸出結(jié)果尤泽。然后調(diào)用studentInstanceMethod方法中的 [self studentInstanceMethod]欣簇。這里要特別注意,NSLog打印出的當(dāng)前self是<LLPerson: 0x600003958050>坯约,所以 [self studentInstanceMethod]實際上就是[person studentInstanceMethod]熊咽,底層實現(xiàn)代碼為
objc_msgSend([LLPerson class], @Seletor(studentInstanceMethod))
用我們上邊提到的消息機制來還原查找方法studentInstanceMethod的過程,類對象LLPerson中的方法列表methodLists中只有一個方法personInstanceMethod闹丐,且這個方法的IMP指向了studentInstanceMethod的實現(xiàn)地址横殴。但是methodLists中根本沒有studentInstanceMethod這個方法,所以經(jīng)過消息機制的三個階段也找不到該方法卿拴,最終報錯-[LLPerson studentInstanceMethod]: unrecognized selector sent to instance 0x600003958050
衫仑。
問題二:為什么[stu personInstanceMethod]
執(zhí)行之后沒有crash梨与?
[stu personInstanceMethod]底層實現(xiàn)代碼為
底層代碼即
objc_msgSend([Student class], @Seletor(personInstanceMethod))
用消息機制來還原查找方法personInstanceMethod的過程,首先在
- 首先惑畴,在實例對象stu對應(yīng)的類對象Student的方法列表methodLists中查找personInstanceMethod方法蛋欣,沒有找到。
- 然后通過類對象Student的superClass找到父類對象Person如贷,在父類的的方法列表methodLists中查找personInstanceMethod方法陷虎,找到了,調(diào)用方法的IMP杠袱,此時是studentInstanceMethod尚猿。首先執(zhí)行NSLog,打印結(jié)果楣富。注意打印結(jié)果中的self是<LLStudent: 0x600003964160>凿掂,所以接下來調(diào)用
[self studentInstanceMethod]
實際上就是[stu studentInstanceMethod]
,所以底層實現(xiàn)代碼是
objc_msgSend([Student class], @Seletor(studentInstanceMethod))
按照消息機制的查找過程纹蝴,我們在類對象Student的方法列表methodLists中找到studentInstanceMethod庄萎,然后調(diào)用該方法的IMP,此時是personInstanceMethod塘安。
所以我們可以看到糠涛,[stu personInstanceMethod]
這行代碼的運行結(jié)果是:
2020-01-07 22:41:10.852488+0800 RunTime1[2421:244815] Student中的新方法, 調(diào)用者:<LLStudent: 0x600003964160>, 方法:-[LLStudent(huan) studentInstanceMethod]
2020-01-07 22:41:10.852652+0800 RunTime1[2421:244815] person對象方法, 調(diào)用者:<LLStudent: 0x600003964160> 方法:-[LLPerson personInstanceMethod]
問題三:為什么在父類的分類中交換方法就沒有問題呢兼犯?
簡而言之忍捡,就是類對象Person在進行方法交換之前,它的方法列表methodLists中已經(jīng)包含了交換前的方法和交換后的方法切黔,不會存在交換之后砸脊,方法找不到的問題。
問題四:怎么處理這個問題呢纬霞?
首先再次明確我們的目的是為了在Student中將使用的父類方法進行方法交換凌埂。成功的標(biāo)志和直接在父類中的分類中進行方法交換的結(jié)果一樣,如果stu執(zhí)行studentInstanceMethod和personInstanceMethod能夠調(diào)用到對方的實現(xiàn)诗芜,就達到了目的侨舆。 一定要明確這一點。
通過上邊的分析绢陌,我們知道直接使用method_exchangeImplementations
的方法實現(xiàn)不了我們想要的目的。解決的方式熔恢,如問題三中提到的那樣脐湾,先讓stu擁有交換前和交換后的方法,然后再進行交換叙淌。
好了秤掌,先看下代碼和運行結(jié)果愁铺,我們再做具體的分析。
@implementation LLStudent (huan)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"------// 替換開始 //------");
NSLog(@"%@", self);
Method oriMethod = class_getInstanceMethod(self, @selector(personInstanceMethod));
Method swiMethod = class_getInstanceMethod(self, @selector(studentInstanceMethod));
BOOL didAddMethod = class_addMethod(self,
@selector(personInstanceMethod),
method_getImplementation(swiMethod),
method_getTypeEncoding(swiMethod)
);
if (didAddMethod) {
class_replaceMethod(self,
@selector(studentInstanceMethod),
method_getImplementation(oriMethod),
method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations (oriMethod, swiMethod);
}
NSLog(@"------// 替換完畢 //------");
});
}
- (void)studentInstanceMethod {
NSLog(@"Student中的新方法, 調(diào)用者:%@闻鉴, 方法:%s",self,__FUNCTION__);
[self studentInstanceMethod];
}
@end
2020-01-08 00:14:35.333082+0800 RunTime1[3674:509474] ------// 替換開始 //------
2020-01-08 00:14:35.333878+0800 RunTime1[3674:509474] LLStudent
2020-01-08 00:14:35.334058+0800 RunTime1[3674:509474] ------// 替換完畢 //------
2020-01-08 00:14:35.441442+0800 RunTime1[3674:509474] ------------
2020-01-08 00:14:35.441627+0800 RunTime1[3674:509474] Student中的新方法, 調(diào)用者:<LLStudent: 0x6000036185e0>茵乱, 方法:-[LLStudent(huan) studentInstanceMethod]
2020-01-08 00:14:35.441779+0800 RunTime1[3674:509474] person對象方法, 調(diào)用者:<LLStudent: 0x6000036185e0> 方法:-[LLPerson personInstanceMethod]
2020-01-08 00:14:35.441893+0800 RunTime1[3674:509474] ------------
2020-01-08 00:14:35.442032+0800 RunTime1[3674:509474] person對象方法, 調(diào)用者:<LLPerson: 0x600003614160> 方法:-[LLPerson personInstanceMethod]
通過打印結(jié)果,可以看到已經(jīng)實現(xiàn)了我們的目的孟岛,并且父類依然可以調(diào)用到瓶竭,沒有崩潰。我們具體分析下:
首先渠羞,執(zhí)行class_addMethod
方法
BOOL didAddMethod = class_addMethod(self,
@selector(personInstanceMethod),
method_getImplementation(swiMethod),
method_getTypeEncoding(swiMethod)
);
結(jié)果didAddMethod = YES斤贰, 我們動態(tài)給類對象Student新添加了personInstanceMethod方法,并且這個方法的IMP是studentInstanceMethod次询。 此時類對象Student方法列表中就包含了交換前和交換后的方法荧恍,而類對象Person的方法列表我們并沒有進行操作,所以不變屯吊,看圖二送巡。
然后進入if判斷中,執(zhí)行下邊代碼:
class_replaceMethod(self,
@selector(studentInstanceMethod),
method_getImplementation(oriMethod),
method_getTypeEncoding(oriMethod));
類對象Student中的方法studentInstanceMethod的imp和types替換為oriMethod(即personInstanceMethod)的盒卸。此時骗爆,類對象Student中的兩個方法及它們的imp實際如下:
是不是很熟悉?沒錯世落,和圖一中交換后的類對象Person的方法列表中一樣淮腾。這個時候執(zhí)行
[stu personInstanceMethod]
就不會crash且實現(xiàn)方法交換的效果了。小結(jié)一下:如果想要實現(xiàn)方法交換屉佳,那么交換前后的方法必須都在當(dāng)前類對象中有實現(xiàn)才可以谷朝。
所以,AFNetworking和其它一些第三方框架要用到class_addMethod武花、class_replaceMethod兩個方法圆凰,是為了兼顧上邊這種特殊的情況,造成crash体箕。
結(jié)尾
終于把之前的坑補上了专钉。其實在Runtime交換方法的使用過程中還有其它的情況存在,比如說組內(nèi)多個人都對同一個方法進行了交換操作等等累铅,所以我們最好是把這種操作交給一個人或者一個組來統(tǒng)一維護跃须,避免這種情況。
交流
希望能和大家交流技術(shù)
我的博客地址: http://www.lilongcnc.cc/