前言
最近在整理博客,發(fā)現(xiàn)自己之前寫的關(guān)于Runtime攔截替換方法的一篇文章《12- Runtime基礎(chǔ)使用場(chǎng)景-攔截替換方法(class_addMethod ,class_replaceMethod和method_exchangeImplementations)》,大家還是很關(guān)注的,文章大家看完依然疑問,但是由于當(dāng)時(shí)生產(chǎn)力不足和后續(xù)的種種原因,并沒有補(bǔ)發(fā)文章。最近工作不忙,正好結(jié)合自己工作中遇到的Runtime使用場(chǎng)景,補(bǔ)上自己這個(gè)坑。
之前留給大家的疑惑主要有兩點(diǎn),如下:
-
第一,下邊這三個(gè)方法的具體作用:
class_addMethodclass_replaceMethodmethod_exchangeImplementations
第二,為什么方法交換需要用到
class_addMethod和class_replaceMethod這兩個(gè)方法?
要回答這兩個(gè)問題,我們有必要了解一下OC中的類對(duì)象結(jié)構(gòu)和消息機(jī)制,往下看。
類結(jié)構(gòu)
OC中的類對(duì)象結(jié)構(gòu)和消息機(jī)制包含的內(nèi)容其實(shí)很多,避免篇幅過長(zhǎng),這里我只簡(jiǎn)單的說一下和本文相關(guān)的部分。
首先,在OC中有實(shí)例對(duì)象、類對(duì)象、元類對(duì)象,如下:
[Student class]; // 是類對(duì)象
Student *stu = [Student new]; // p是實(shí)例對(duì)象
object_getClass([Student class]) // 元類
類對(duì)象(即我們?nèi)粘=蟹Q為的類),它是基于實(shí)例對(duì)象的一種抽象定義,比如說喵咪小花都屬于貓,那么貓就是一種抽象的概念,定義了貓的外形、活動(dòng)特定等等屬性。我們用代碼的方法可以這么定義:
貓 *小花 = [貓 new];
所以O(shè)C中,類對(duì)象也是一個(gè)定義了一個(gè)實(shí)例對(duì)象包含了哪些方法、屬性、父類是誰等信息的抽象對(duì)象。OC的底層是c/c++實(shí)現(xiàn),所以O(shè)C中的類結(jié)構(gòu)是采用c++中的結(jié)構(gòu)體來表示的。下邊是我寫的一個(gè)偽結(jié)構(gòu)體,從偽結(jié)構(gòu)體中我們可以知道,類對(duì)象中有方法列表、父類這個(gè)信息。
struct objc_class {
Class super_class // 當(dāng)前類的父類
struct objc_method_list * * methodLists // 方法列表
//...... 其它信息這里忽略
}
(類對(duì)象的真實(shí)結(jié)構(gòu)并不是這樣,這里我們也可以忽略它的真實(shí)結(jié)構(gòu)。即便你日后了解了類的真實(shí)結(jié)構(gòu),也不會(huì)影響到下邊的結(jié)論。)
super_class 就是當(dāng)前類的父類。
methodLists實(shí)際上是一個(gè)數(shù)組,保存著類有哪些方法。這里可以提到元類了,實(shí)例對(duì)象是一種結(jié)構(gòu),而類對(duì)象和元類對(duì)象是另外一種結(jié)構(gòu)。關(guān)于類對(duì)象和元類對(duì)象的區(qū)別,對(duì)于本文只要記住一個(gè):對(duì)象方法是保存在類對(duì)象的methodLists中,而類方法保存在元類的methodLists中。
methodLists數(shù)組中保存著結(jié)構(gòu)體method_t,這個(gè)結(jié)構(gòu)體包含了我們平時(shí)寫的方法的信息。偽結(jié)構(gòu)體如下:
struct method_t {
SEL method_name
IMP method_imp
char * types
}
SEL method_name sel就是方法的名字
IMP method_imp imp保存著一個(gè)指針,這個(gè)指針指向函數(shù)的具體實(shí)現(xiàn)地址。 所以,方法真正的實(shí)現(xiàn)是單獨(dú)保存在一個(gè)地方的,它的實(shí)現(xiàn)地址交給imp保存。當(dāng)我們執(zhí)行方法的時(shí)候,實(shí)際上是從method_t結(jié)構(gòu)體中找到imp,然后調(diào)用。
這里有一點(diǎn)很重要,Runtime替換方法實(shí)際上就是替換imp。所以產(chǎn)生了替換了方法之后,明明你調(diào)用的是methodA,但是執(zhí)行的是methodB的效果。因?yàn)閙ethodA對(duì)應(yīng)的method_t結(jié)構(gòu)體中的imp實(shí)際保存的是methodB方法的實(shí)現(xiàn)地址了。
types 可以簡(jiǎn)單的認(rèn)為到能代表這個(gè)方法的特定字符,用法我們暫時(shí)忽略。 有一點(diǎn)需要注意,如果你修改了imp為新的imp外,同時(shí)修改types改成新方法的types,這樣才是真正把method_t結(jié)構(gòu)體改成了新的方法。
消息機(jī)制
一般在OC中調(diào)用方法,底層會(huì)轉(zhuǎn)成一個(gè)objc_msgSend的c++函數(shù)。比如說:[stu instanceMthod],底層實(shí)際上是
objc_msgSend([Student class], @Seletor(instanceMthod))
表示我們從Person這個(gè)類對(duì)象結(jié)構(gòu)體中查找instanceMthod這個(gè)方法,找到它并且調(diào)用。查找調(diào)用這個(gè)方法的過程,我們可以簡(jiǎn)單認(rèn)為就是消息機(jī)制。 下邊我們簡(jiǎn)單說一下消息機(jī)制的流程,還是用[stu instanceMthod]來作為例子:
假如這個(gè)方法在Student的父類Person中
- 首先,實(shí)例對(duì)象p在對(duì)應(yīng)的類對(duì)象Student的方法列表中methodLists查找instanceMthod方法,沒有找到。(如果能找到,那么就直接調(diào)用對(duì)應(yīng)method_t結(jié)構(gòu)中的imp執(zhí)行方法,結(jié)束查找)
- 通過類對(duì)象Student的superClass找到父類對(duì)象Person,在父類的的方法列表中methodLists查找instanceMthod方法,找到了,調(diào)用方法,結(jié)束查找。(如果在父類對(duì)象Person、以及Person的父類對(duì)象NSObject沒有找到該方法,那么會(huì)進(jìn)入消息轉(zhuǎn)發(fā)的另外兩個(gè)階段,如果這兩個(gè)階段還是沒有找到要調(diào)用的方法,那么就會(huì)報(bào)經(jīng)典錯(cuò)誤
unrecognized selector sent to instance)
當(dāng)然這個(gè)消息機(jī)制的過程是非常簡(jiǎn)陋的,實(shí)際上在進(jìn)入methodLists查找之前,會(huì)先進(jìn)入方法緩存cache中查找,有興趣你可以自己多了解一下。
三個(gè)方法的作用
class_addMethod
BOOL class_addMethod(Class cls, SEL name, IMP imp, const char *types)
作用:就是動(dòng)態(tài)在類對(duì)象cls中添加一個(gè)方法。參數(shù)SEL、IMP、types我們上邊都提到過。
注意:如果cls中已經(jīng)有了要添加的方法聲明和方法實(shí)現(xiàn),那么添加失敗,返回NO。如果沒有聲明和實(shí)現(xiàn)方法,或者只聲明沒有方法實(shí)現(xiàn),都可以添加成功,返回YES。
class_replaceMethod
IMP class_replaceMethod(Class cls, SEL name, IMP newImp, const char *newTypes)
作用:把類對(duì)象中的cls的方法的imp替換成newImp,同時(shí)還需要替換newTypes。
method_exchangeImplementations
void method_exchangeImplementations(Method m1, Method m2)
作用:Method這里可以認(rèn)為就是上邊說到的method_t結(jié)構(gòu)體,交換m1和m2,實(shí)際上就是交換兩個(gè)method_t結(jié)構(gòu)體中的IMP和types。
注意:如果imp為nil,交換操作將失敗。
為什么會(huì)用到dispatch_once
dispatch_once我們?nèi)粘W铋L(zhǎng)用的就是單例。保證在程序運(yùn)行過程中,其代碼塊內(nèi)的代碼只執(zhí)行一次。Runtime交換方法之所以會(huì)用到dispatch_once,是為了防止load被手動(dòng)調(diào)用。 load方法的調(diào)用時(shí)機(jī)是在main函數(shù)被調(diào)用之前,且只被系統(tǒng)調(diào)用一次。正常情況下,我們無需再手動(dòng)調(diào)用load方法,但是為了防止意外,所以加了dispatch_once,保證替換方
法的Runtime代碼只能執(zhí)行一次,從而避免方法有替換回去。
method_exchangeImplementations
一般我們交換方法實(shí)現(xiàn)的場(chǎng)景比較明確,比如替換蘋果API中的類的某個(gè)方法或者第三方框架中的類的某個(gè)方法。
例子如下:
@interface LLPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LLPerson
- (void)personInstanceMethod{
NSLog(@"person對(duì)象方法, 調(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對(duì)象方法, 調(diào)用者:<LLStudent: 0x600003520330> 方法:-[LLPerson personInstanceMethod]
這種情況非常簡(jiǎn)單,我們明確的知道了LLPerson中的方法聲明和方法實(shí)現(xiàn),只需要在分類中直接交換就可以了。不需要其它的額外代碼。 下邊介紹一種特殊的情況,請(qǐng)往下看。
為什么會(huì)用到class_addMethod、class_replaceMethod
下邊要講的這種情況是在我開發(fā)過程中遇到的,如果只是用method_exchangeImplementations進(jìn)行方法交換之后,運(yùn)行會(huì)出現(xiàn)crash。
場(chǎng)景:Person聲明了某個(gè)方法并且實(shí)現(xiàn)了方法,然后Student繼承Person,沒有重寫父類的這個(gè)方法,依然不影響直接調(diào)用和使用Person中的這個(gè)方法。 如果此時(shí)我們?cè)赟tudent的分類中交換父類的這個(gè)方法,會(huì)發(fā)生了什么?
代碼如下:
@interface LLPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation LLPerson
- (void)personInstanceMethod{
NSLog(@"person對(duì)象方法, 調(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
運(yùn)行結(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對(duì)象方法, 調(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];這一行??吹竭@里,可能大家會(huì)有點(diǎn)蒙,會(huì)有下邊這幾個(gè)問題:
- 不是已經(jīng)交換了父類的方法嗎,為什么執(zhí)行[person personInstanceMethod]會(huì)crash呢?
- 為什么
[stu personInstanceMethod]執(zhí)行之后沒有crash? - 為什么在父類的分類中交換方法就沒有問題呢?
- 怎么處理這個(gè)問題呢?
分析如下:
問題一:為什么執(zhí)行[person personInstanceMethod]會(huì)crash呢?
注意,上邊的代碼是在子類Student的分類中把父類的方法和子類的方法進(jìn)行了交換。交換之后如下:

當(dāng)執(zhí)行[person personInstanceMethod]時(shí),實(shí)際上是執(zhí)行子類中的studentInstanceMethod方法,首先調(diào)用NSLog,輸出結(jié)果。然后調(diào)用studentInstanceMethod方法中的 [self studentInstanceMethod]。這里要特別注意,NSLog打印出的當(dāng)前self是<LLPerson: 0x600003958050>,所以 [self studentInstanceMethod]實(shí)際上就是[person studentInstanceMethod],底層實(shí)現(xiàn)代碼為
objc_msgSend([LLPerson class], @Seletor(studentInstanceMethod))
用我們上邊提到的消息機(jī)制來還原查找方法studentInstanceMethod的過程,類對(duì)象LLPerson中的方法列表methodLists中只有一個(gè)方法personInstanceMethod,且這個(gè)方法的IMP指向了studentInstanceMethod的實(shí)現(xiàn)地址。但是methodLists中根本沒有studentInstanceMethod這個(gè)方法,所以經(jīng)過消息機(jī)制的三個(gè)階段也找不到該方法,最終報(bào)錯(cuò)-[LLPerson studentInstanceMethod]: unrecognized selector sent to instance 0x600003958050。
問題二:為什么[stu personInstanceMethod]執(zhí)行之后沒有crash?
[stu personInstanceMethod]底層實(shí)現(xiàn)代碼為
底層代碼即
objc_msgSend([Student class], @Seletor(personInstanceMethod))
用消息機(jī)制來還原查找方法personInstanceMethod的過程,首先在
- 首先,在實(shí)例對(duì)象stu對(duì)應(yīng)的類對(duì)象Student的方法列表methodLists中查找personInstanceMethod方法,沒有找到。
- 然后通過類對(duì)象Student的superClass找到父類對(duì)象Person,在父類的的方法列表methodLists中查找personInstanceMethod方法,找到了,調(diào)用方法的IMP,此時(shí)是studentInstanceMethod。首先執(zhí)行NSLog,打印結(jié)果。注意打印結(jié)果中的self是<LLStudent: 0x600003964160>,所以接下來調(diào)用
[self studentInstanceMethod]實(shí)際上就是[stu studentInstanceMethod],所以底層實(shí)現(xiàn)代碼是
objc_msgSend([Student class], @Seletor(studentInstanceMethod))
按照消息機(jī)制的查找過程,我們?cè)陬悓?duì)象Student的方法列表methodLists中找到studentInstanceMethod,然后調(diào)用該方法的IMP,此時(shí)是personInstanceMethod。
所以我們可以看到,[stu personInstanceMethod]這行代碼的運(yùn)行結(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對(duì)象方法, 調(diào)用者:<LLStudent: 0x600003964160> 方法:-[LLPerson personInstanceMethod]
問題三:為什么在父類的分類中交換方法就沒有問題呢?
簡(jiǎn)而言之,就是類對(duì)象Person在進(jìn)行方法交換之前,它的方法列表methodLists中已經(jīng)包含了交換前的方法和交換后的方法,不會(huì)存在交換之后,方法找不到的問題。
問題四:怎么處理這個(gè)問題呢?
首先再次明確我們的目的是為了在Student中將使用的父類方法進(jìn)行方法交換。成功的標(biāo)志和直接在父類中的分類中進(jìn)行方法交換的結(jié)果一樣,如果stu執(zhí)行studentInstanceMethod和personInstanceMethod能夠調(diào)用到對(duì)方的實(shí)現(xiàn),就達(dá)到了目的。 一定要明確這一點(diǎn)。
通過上邊的分析,我們知道直接使用method_exchangeImplementations的方法實(shí)現(xiàn)不了我們想要的目的。解決的方式,如問題三中提到的那樣,先讓stu擁有交換前和交換后的方法,然后再進(jìn)行交換。
好了,先看下代碼和運(yùn)行結(jié)果,我們?cè)僮鼍唧w的分析。
@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對(duì)象方法, 調(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對(duì)象方法, 調(diào)用者:<LLPerson: 0x600003614160> 方法:-[LLPerson personInstanceMethod]
通過打印結(jié)果,可以看到已經(jīng)實(shí)現(xiàn)了我們的目的,并且父類依然可以調(diào)用到,沒有崩潰。我們具體分析下:
首先,執(zhí)行class_addMethod方法
BOOL didAddMethod = class_addMethod(self,
@selector(personInstanceMethod),
method_getImplementation(swiMethod),
method_getTypeEncoding(swiMethod)
);
結(jié)果didAddMethod = YES, 我們動(dòng)態(tài)給類對(duì)象Student新添加了personInstanceMethod方法,并且這個(gè)方法的IMP是studentInstanceMethod。 此時(shí)類對(duì)象Student方法列表中就包含了交換前和交換后的方法,而類對(duì)象Person的方法列表我們并沒有進(jìn)行操作,所以不變,看圖二。

然后進(jìn)入if判斷中,執(zhí)行下邊代碼:
class_replaceMethod(self,
@selector(studentInstanceMethod),
method_getImplementation(oriMethod),
method_getTypeEncoding(oriMethod));
類對(duì)象Student中的方法studentInstanceMethod的imp和types替換為oriMethod(即personInstanceMethod)的。此時(shí),類對(duì)象Student中的兩個(gè)方法及它們的imp實(shí)際如下:

是不是很熟悉?沒錯(cuò),和圖一中交換后的類對(duì)象Person的方法列表中一樣。這個(gè)時(shí)候執(zhí)行
[stu personInstanceMethod]就不會(huì)crash且實(shí)現(xiàn)方法交換的效果了。小結(jié)一下:如果想要實(shí)現(xiàn)方法交換,那么交換前后的方法必須都在當(dāng)前類對(duì)象中有實(shí)現(xiàn)才可以。 所以,AFNetworking和其它一些第三方框架要用到class_addMethod、class_replaceMethod兩個(gè)方法,是為了兼顧上邊這種特殊的情況,造成crash。
結(jié)尾
終于把之前的坑補(bǔ)上了。其實(shí)在Runtime交換方法的使用過程中還有其它的情況存在,比如說組內(nèi)多個(gè)人都對(duì)同一個(gè)方法進(jìn)行了交換操作等等,所以我們最好是把這種操作交給一個(gè)人或者一個(gè)組來統(tǒng)一維護(hù),避免這種情況。
交流

希望能和大家交流技術(shù)
我的博客地址: http://www.lilongcnc.cc/