method swizzling 就是常說(shuō)的方法交換李请,也常被成為黑魔法毫玖。
簡(jiǎn)單點(diǎn)說(shuō)就是 定義并實(shí)現(xiàn)了方法A和B,在調(diào)用方法A的時(shí)候叠艳,執(zhí)行的確實(shí)方法B
基本概念
在了解method swizzling之前先來(lái)了解幾個(gè)概念:
SEL/@selector(方法名)
SEL 又叫選擇器烁登,但是一般我們將它稱(chēng)之為方法編號(hào)怯屉。源碼中的定義為
typedef struct objc_selector *SEL;
以下關(guān)于方法編號(hào)的解釋來(lái)自這篇文章 (未得專(zhuān)業(yè)驗(yàn)證)
方法以 selector 作為索引. selector 的數(shù)據(jù)類(lèi)型是 SEL. 雖然 SEL 定義成 char*, 我們可 以把它想象成 int. 每個(gè)方法的名字對(duì)應(yīng)一個(gè)唯一的 int 值.比如, 方法 addObject: 可能 對(duì)應(yīng)的是 12. 當(dāng)尋找該方法是, 使用的是 selector,而不是名字 @"addObject:"
Objective-C 數(shù)據(jù)結(jié)構(gòu)中,存在一個(gè) name - selector 的映射表如下圖
在編譯的時(shí)候, 只要有方法的調(diào)用, 編譯器都會(huì)通過(guò) selector 來(lái)查找,所以 (假設(shè) addObject 的 selector 為 12)
[myObject addObject:yourObject];
將會(huì)編譯變成
objc_msgSend(myObject, 12, yourObject);
這里,objec_msgSend()函數(shù)將會(huì)使用 myObjec 的 isa 指針來(lái)找到 myObject 的類(lèi)空間結(jié)構(gòu)并 在類(lèi)空間結(jié)構(gòu)中查找 selector 12 所對(duì)應(yīng)的方法.如果沒(méi)有找到,那么將使用指向父類(lèi)的指 針找到父類(lèi)空間結(jié)構(gòu)進(jìn)行 selector 12 的查找. 如果仍然沒(méi)有找到,就繼續(xù)往父類(lèi)的父類(lèi)一 直找,直到找到為止, 如果到了根類(lèi) NSObject 中仍然找不到,將會(huì)拋出異常.
我們可以看到, 這是一個(gè)很動(dòng)態(tài)的查找過(guò)程.類(lèi)的結(jié)構(gòu)可以在運(yùn)行的時(shí)候改變,這樣可以很 容易來(lái)進(jìn)行功能擴(kuò)展Objective-C 語(yǔ)言是動(dòng)態(tài)語(yǔ)言, 支持動(dòng)態(tài)綁定.
??文章摘要結(jié)束
IMP
IMP指向方法實(shí)現(xiàn)的首地址,類(lèi)似C語(yǔ)言的函數(shù)指針饵沧。IMP是消息最終調(diào)用的執(zhí)行代碼锨络,是方法真正的實(shí)現(xiàn)代碼 。
源碼中的定義:
#if !OBJC_OLD_DISPATCH_PROTOTYPES
typedef void (*IMP)(void /* id, SEL, ... */ );
#else
typedef id _Nullable (*IMP)(id _Nonnull, SEL _Nonnull, ...);
#endif
每一個(gè)實(shí)現(xiàn)了的方法都存在一個(gè)SEL和IMP(這不廢話狼牺,不然怎么可以成功調(diào)用羡儿?),寫(xiě)句的原因是可能剛理解這些概念的時(shí)候有些繞:
只有聲明是钥,沒(méi)有實(shí)現(xiàn)的方法 有沒(méi)有SEL/IMP掠归?比如只是在.h文件中寫(xiě)入- (void)test
?
只有實(shí)現(xiàn)沒(méi)有聲明的方法,有沒(méi)有SEL/IMP咏瑟? 比如只是在.m文件中寫(xiě)- (void)test{}
?
上面問(wèn)題的答案就是:
只有聲明拂到,沒(méi)有實(shí)現(xiàn):SEL和IMP都沒(méi)有
沒(méi)有聲明痪署,只有實(shí)現(xiàn):SEL和IMP都有码泞。
在只有聲明,沒(méi)有實(shí)現(xiàn)的情況下狼犯,打印類(lèi)結(jié)構(gòu)信息余寥。在ro里面的信息是 baseMethodList = 0x0000000000000000
Method
主要包含三部分
方法名:方法名為此方法的簽名,有著相同函數(shù)名和參數(shù)名的方法有著相同的方法名悯森。
方法類(lèi)型:方法類(lèi)型描述了參數(shù)的類(lèi)型宋舷。
IMP: IMP即函數(shù)指針,為方法具體實(shí)現(xiàn)代碼塊的地址瓢姻,可像普通C函數(shù)調(diào)用一樣使用IMP祝蝠。
實(shí)際上相當(dāng)于在SEL和IMP之間作了一個(gè)映射。有了SEL,我們便可以找到對(duì)應(yīng)的IMP绎狭。
源碼中的定義:
struct objc_method {
SEL _Nonnull method_name;
char * _Nullable method_types;
IMP _Nonnull method_imp;
}
method swizzling 的應(yīng)用
在實(shí)際開(kāi)發(fā)中细溅,經(jīng)常會(huì)遇到這樣的情況
NSArray *array = @[@"1",@"2",@"3"];
NSLog(@"%@",array[4]);
一般來(lái)說(shuō),在使用下標(biāo)取值之前儡嘶,都需要先判斷
if (array.count < 4) {
NSLog(@"%@",array[4]);
}
但是總有漏掉判斷或其他情況導(dǎo)致crash
為了避免這種情況或者少寫(xiě)重復(fù)代碼喇聊,我們可以使用動(dòng)態(tài)方法交換的方式來(lái)處理
定義一個(gè)方法JERuntimeTool
在.h中定義方法并在.m中實(shí)現(xiàn)
#import <objc/runtime.h>
/**
交換方法
@param cls 交換對(duì)象
@param oriSEL 原始方法編號(hào)
@param swizzledSEL 交換的方法編號(hào)
*/
+ (void)je_methodSwizzlingWithClass:(Class)cls
oriSEL:(SEL)oriSEL
swizzledSEL:(SEL)swizzledSEL{
if (!cls){
NSLog(@"傳入的交換類(lèi)不能為空");
return;
}
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(oriMethod, swiMethod);
}
定義一個(gè)拓展NSArray的JE類(lèi)NSArray+JE.h
針對(duì)下標(biāo)取值的情況,有多個(gè)方法蹦狂,都需要進(jìn)行異常捕獲
- (id)je_objectAtIndex:(NSUInteger)index{
if (index > self.count-1) {
//異常處理或記錄打印
return nil;
}
return [self lg_objectAtIndex:index];
}
- (id)je_objectAtIndexedSubscript:(NSUInteger)index{
if (index > self.count-1) {
//異常處理或記錄打印
return nil;
}
return [self lg_objectAtIndexedSubscript:index];
}
進(jìn)行方法交換
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[JERuntimeTool je_methodSwizzlingWithClass:objc_getClass("__NSArrayI")
oriSEL:@selector(objectAtIndex:)
swizzledSEL:@selector(je_objectAtIndex:)];
[JERuntimeTool je_methodSwizzlingWithClass:objc_getClass("__NSArrayI")
oriSEL:@selector(objectAtIndexedSubscript:)
swizzledSEL:@selector(je_objectAtIndexedSubscript:)];
});
}
這里有幾點(diǎn)需要注意:
1誓篱、在+ (void)load
方法中調(diào)用
2、調(diào)用的時(shí)候凯楔,用單例的方式
3窜骄、對(duì)于系統(tǒng)的有些方法應(yīng)該交換哪個(gè)方法?
4摆屯、交換的類(lèi)應(yīng)該是那個(gè)啊研?
在crash的時(shí)候有一個(gè)錯(cuò)誤信息,其中有一段:reason: '*** -[__NSArrayI objectAtIndexedSubscript:]: index 4 beyond bounds [0 .. 2]
,其中__NSArrayI
是需要交換的類(lèi)鸥拧,objectAtIndexedSubscript:
是需要交換的方法党远。
對(duì)于我們自定義的類(lèi)一般來(lái)說(shuō)直接就是類(lèi)名和自定義的方法SEL,但是系統(tǒng)的抽象類(lèi)類(lèi)是不能直接作為類(lèi)對(duì)象傳入的富弦,比如NSArray/NSMutableArrya沟娱、NSDictionary/NSMutableDictionary、 NSData/NSMutableData等腕柜。
上面的方法看似很完美济似,但是下面坑就是在你不經(jīng)意間就出現(xiàn)在腳下。
會(huì)出現(xiàn)的問(wèn)題
問(wèn)題1
背景:子類(lèi)沒(méi)有實(shí)現(xiàn)父類(lèi)的方法 但是對(duì)子類(lèi)的方法進(jìn)行了交換
比如:Person:NSObjec Student:Person
Person定義并實(shí)現(xiàn)了方法:-(void)personMethod;
Studen 沒(méi)有實(shí)現(xiàn) -(void)personMethod;
在Student的分類(lèi)中中交換方法
Student+JE.h
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[JERuntimeTool je_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(je_studentInstanceMethod)];
});
}
- (void)je_studentInstanceMethod{
NSLog(@"do something");
[self je_studentInstanceMethod];
}
執(zhí)行代碼
Student *s = [Student new];
[s personInstanceMethod];
Person *p = [Person new];
[p personInstanceMethod];
運(yùn)行結(jié)果:
do something
person對(duì)象方法:-[Person personInstanceMethod]
do something
[Person je_studentInstanceMethod]: unrecognized selector sent to instance 0x600002e5c440
在person調(diào)用 personInstanceMethod 方法的時(shí)候 出現(xiàn)了問(wèn)題盏缤,原因是:
1砰蠢、person 調(diào)用 personInstanceMethod(SEL) ,由于進(jìn)行了交換唉铜,將執(zhí)行
je_studentInstanceMethod(IMP)
2台舱、繼續(xù)調(diào)用[self je_studentInstanceMethod],person 調(diào)用 je_studentInstanceMethod (SEL)
3、由于je_studentInstanceMethod(SEL) 屬于Student(調(diào)用SEL需要執(zhí)行相對(duì)應(yīng)的IMP潭流,這個(gè)時(shí)候竞惋,在Person中沒(méi)有相應(yīng)的IMP) 所以person 調(diào)用的時(shí)候 出現(xiàn)了 [Person je_studentInstanceMethod]: unrecognized的錯(cuò)誤
知道了原因,我們需要針對(duì)性解決問(wèn)題:
在Student中添加一個(gè) 方法灰嫉,對(duì)應(yīng)關(guān)系為:swizzleSEL + Ori IMP拆宛。(可見(jiàn)下圖)
在這里,采用的是 :向Student中添加
je_studentInstanceMethod(SEL) + personInstanceMethod(IMP)讼撒,如果添加成功浑厚,說(shuō)明Student沒(méi)有實(shí)現(xiàn)這個(gè)方法股耽,這樣來(lái)達(dá)到通用性,代碼如下
+ (void)je_methodSwizzlingWithClass:(Class)cls
oriSEL:(SEL)oriSEL
swizzledSEL:(SEL)swizzledSEL{
if (!cls){
NSLog(@"傳入的交換類(lèi)不能為空");
return;
}
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swiMethod);
}
}
執(zhí)行步驟如下圖
問(wèn)題2
背景:父類(lèi)和子類(lèi)都沒(méi)有實(shí)現(xiàn)
比如:子類(lèi)Student 只申明一個(gè)方法readBook钳幅;
在Student的分類(lèi)中中交換方法
Student+JE.h
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[JERuntimeTool je_methodSwizzlingWithClass:self oriSEL:@selector(readBook) swizzledSEL:@selector(je_readBook)];
});
}
- (void)je_readBook{
NSLog(@"do something");
[self je_readBook];
}
執(zhí)行代碼
Student *s = [Student new];
[s personInstanceMethod];
運(yùn)行結(jié)果:
不停的調(diào)用 NSLog(@"do something"); 這一句
這里產(chǎn)生了遞歸豺谈,原因是
交換不完全
。
步驟解析:
1贡这、交換前:
2茬末、執(zhí)行class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
3、執(zhí)行 class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
這里需要注意的是·將 swizzleSEL (也就是圖中的5) 對(duì)應(yīng)的 swizzleIMP (圖中的6) 替換成 一個(gè)nil盖矫,這里需要注意的是丽惭,從底層源碼中看:
class_replaceMethod —>
addMethod —>
_method_setImplementation
如果reply一個(gè)nil IMP,那么是不會(huì)執(zhí)行的辈双,所以最終指向結(jié)構(gòu)還是2圖的結(jié)構(gòu)
static IMP
_method_setImplementation(Class cls, method_t *m, IMP imp)
{
runtimeLock.assertLocked();
if (!m) return nil;
if (!imp) return nil;
IMP old = m->imp;
m->imp = imp;
....
return old;
}
執(zhí)行過(guò)程:
結(jié)合圖2:
調(diào)用9 -》 執(zhí)行 10责掏;
10 調(diào)用 5 -》 執(zhí)行 6
6 調(diào)用5 -》 執(zhí)行6
....
所以發(fā)生了遞歸
錯(cuò)誤原因:查看替換后的結(jié)構(gòu)
按照之前的方法替換后的結(jié)構(gòu)
知道了原因,針對(duì)這個(gè)問(wèn)題來(lái)解決:
解決思路就是:判斷有沒(méi)有IMP湃望,如果沒(méi)有IMP换衬,就添加一個(gè)默認(rèn)的IMP(在這里就是 - (void)readBook {})
代碼如下:
+ (void)je_methodSwizzlingWithClass:(Class)cls
oriSEL:(SEL)oriSEL
swizzledSEL:(SEL)swizzledSEL{
if (!cls){
NSLog(@"傳入的交換類(lèi)不能為空");
return;
}
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!oriMethod) {
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}
BOOL didAddMethod = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (didAddMethod) {
class_replaceMethod(cls, swizzledSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}else{
method_exchangeImplementations(oriMethod, swiMethod);
}
}
按照最終的方案替換后的結(jié)構(gòu)
最終交換后的結(jié)構(gòu)
類(lèi)方法交換
上面說(shuō)了這么多,都是針對(duì)對(duì)象方法(實(shí)例方法)來(lái)講的证芭,那么如果想交換類(lèi)方法要怎么處理瞳浦?
如果搞明白了 對(duì)象 -> 類(lèi) —> 元類(lèi) —> 根源類(lèi) 的關(guān)系,并且知道實(shí)例方法和類(lèi)方法的存儲(chǔ)位置废士,這個(gè)問(wèn)題就很容易解決叫潦。
知識(shí)點(diǎn)補(bǔ)充:
1、類(lèi)的isa 指向 元類(lèi) 官硝,元類(lèi)的isa指向 根源類(lèi) 參照之前的文章
2矗蕊、實(shí)例方法存儲(chǔ)在類(lèi)對(duì)象中,類(lèi)方法存儲(chǔ)在元類(lèi)對(duì)象中
解決上面的問(wèn)題氢架,只需要在交換的時(shí)候 傳入的類(lèi) 傳入元類(lèi)就好了傻咖。
代碼如下:
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = object_getClass(self);
[JERuntimeTool je_methodSwizzlingWithClass:class oriSEL:@selector(personMethod) swizzledSEL:@selector(je_readBook)];
});
}
method swizzling 總結(jié)和注意事項(xiàng)
一、方法交換的調(diào)用時(shí)機(jī):
在+ (void)load方法中調(diào)用岖研。
為什么:
1)卿操、自動(dòng)調(diào)用 2)、調(diào)用的早缎玫。 load方法在app啟動(dòng)的時(shí)候硬纤,就由系統(tǒng)自動(dòng)調(diào)用了解滓。
2赃磨、保證交換的唯一性(需要用單例的形式)
二、load方法的加載順序
1)洼裤、有繼承關(guān)系的類(lèi) 先加載父類(lèi)(不包含其拓展)再加載子類(lèi)
2)邻辉、不同類(lèi)之間的load是按照編譯順序來(lái)決定的(即使是有繼承關(guān)系的類(lèi) 他們的拓展之間也是按照編譯順序來(lái)的)
3)、推展類(lèi)的調(diào)用是在所有的類(lèi)加載完成之后,(可參照源碼 中的map_images
方法: 類(lèi) > protocol > category)