什么是method-swizzling躯畴?
method-swizzling
俗稱黑魔法民鼓,在前幾篇文章中說過,在OC中調(diào)用一個(gè)方法蓬抄,其實(shí)就是向一個(gè)對(duì)象發(fā)送消息丰嘉,而查找消息的唯一依據(jù)是selector
的名字,通過名字查找到IMP
嚷缭。利用OC的動(dòng)態(tài)特性饮亏,可以實(shí)現(xiàn)在運(yùn)行時(shí)偷換selector
對(duì)應(yīng)的方法實(shí)現(xiàn),達(dá)到方法實(shí)現(xiàn)交換的效果阅爽。
可以通過下圖去理解
在什么地方進(jìn)行方法交換路幸,為什么?
+load
方法里付翁,原因有三:(后面文章里會(huì)具體分析這個(gè)+load
方法)
- 執(zhí)行比較早简肴,在
main
函數(shù)之前調(diào)用 - 自動(dòng)執(zhí)行,不需要手動(dòng)執(zhí)行
- 唯一性百侧,不用擔(dān)心被子類覆蓋
坑一:找不到真正的方法歸屬
數(shù)組越界砰识,是開發(fā)中最常見的一個(gè)錯(cuò)誤,看下面代碼
- (void)viewDidLoad {
[super viewDidLoad];
self.dataArray = @[@"AA",@"BB",@"CC",@"DD"];
NSLog(@"%@",self.dataArray[4]);
}
這段代碼執(zhí)行結(jié)果佣渴,相信我不用說了辫狼,奔潰,如下圖(其實(shí)都不用給圖的辛润。膨处。。)
其實(shí)可以利用這個(gè)黑魔法频蛔,去規(guī)避App的奔潰灵迫,同時(shí)可以打印出奔潰的類的所在代碼的行數(shù),下面就用這個(gè)機(jī)制晦溪,阻止它的奔潰,代碼如下
@implementation ZBRuntimeTool
+ (void)zb_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL {
if (!cls) NSLog(@"傳入的交換類不能為空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
method_exchangeImplementations(oriMethod, swiMethod);
}
@end
// 分類中
@implementation NSArray (ZB)
+ (void)load {
[ZBRuntimeTool zb_methodSwizzlingWithClass:self oriSEL:@selector(objectAtIndex:) swizzledSEL:@selector(zb_objectAtIndex:)];
}
- (id)zb_objectAtIndex:(NSUInteger)index {
if (index > self.count-1) {
NSLog(@"取值越界了,請(qǐng)記錄 : %lu > %lu",(unsigned long)index,self.count-1);
return nil;
}
return [self zb_objectAtIndex:index];
}
@end
搞定挣跋,是不是感覺很簡(jiǎn)單三圆,編譯運(yùn)行看打印效果,一頓操作猛如虎,結(jié)果發(fā)現(xiàn)運(yùn)行還是奔潰??舟肉,其實(shí)這就是在上面我為什么要給出奔潰的截圖修噪,細(xì)心的小伙伴可能已經(jīng)知道來原因,上面代碼的錯(cuò)誤有兩處:
第一就是方法的歸屬路媚,我們寫的是self
黄琼,指的是NSArray
,但是通過它報(bào)錯(cuò)的結(jié)果可以知道應(yīng)該是__NSArrayI
整慎,它是一個(gè)族類脏款,這個(gè)一定要分清;
第二是我們交換的方法錯(cuò)誤裤园,同樣還是可以通過上面的截圖可以看得出來撤师,我們應(yīng)該交換objectAtIndexedSubscript:
方法
最終代碼如下:
@implementation NSArray (ZB)
+ (void)load {
[ZBRuntimeTool zb_methodSwizzlingWithClass:objc_getClass("__NSArrayI") oriSEL:@selector(objectAtIndexedSubscript:) swizzledSEL:@selector(zb_objectAtIndexedSubscript:)];
}
- (id)zb_objectAtIndexedSubscript:(NSUInteger)index {
if (index > self.count-1) {
NSLog(@"取值越界了,請(qǐng)記錄 : %lu > %lu",(unsigned long)index,self.count-1);
return nil;
}
return [self zb_objectAtIndexedSubscript:index];
}
@end
這段代碼可以解決上面所遇到的問題,但是很明顯拧揽,這樣的代碼漏洞百出剃盾,一個(gè)優(yōu)秀的程序員不應(yīng)該寫到這里就停止的,后面篇章中淤袜,會(huì)持續(xù)優(yōu)化它痒谴。
坑二:可能會(huì)主動(dòng)調(diào)用load方法
還可以拿上面的例子來說事,下面代碼中的調(diào)用
- (void)viewDidLoad {
[super viewDidLoad];
self.dataArray = @[@"AA",@"BB",@"CC",@"DD"];
[NSArray load];
NSLog(@"%@",self.dataArray[4]);
}
運(yùn)行結(jié)果和上面的結(jié)果一摸一樣铡羡,直接奔潰积蔚,而且報(bào)錯(cuò)信息都是一樣的,應(yīng)該能夠想到原因蓖墅,兩次的交換库倘,使得它們都指向了原來的IMP
,所以為了防止這種多次調(diào)用的情況论矾,我們可以通過讓它只運(yùn)行一次來解決這個(gè)問題教翩,代碼如下:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[ZBRuntimeTool zb_methodSwizzlingWithClass:objc_getClass("__NSArrayI") oriSEL:@selector(objectAtIndexedSubscript:) swizzledSEL:@selector(zb_objectAtIndexedSubscript:)];
});
}
對(duì)的,沒有錯(cuò)贪壳,利用單例的方式去解決上面的這個(gè)問題饱亿。
坑三:子類沒有實(shí)現(xiàn)父類的方法,子類交換了父類的方法
下看看下面代碼
// Person類
@interface ZBPerson : NSObject
- (void)personInstanceMethod;
@end
@implementation ZBPerson
- (void)personInstanceMethod {
NSLog(@"person對(duì)象方法:%s",__func__);
}
@end
// Student類
@interface ZBStudent : ZBPerson
- (void)helloWorld;
+ (void)helloWorld1;
// 沒有實(shí)現(xiàn)父類ZBPerson的方法
@end
// 調(diào)用
- (void)viewDidLoad {
[super viewDidLoad];
ZBStudent *s = [[ZBStudent alloc] init];
[s personInstanceMethod];
ZBPerson *p = [[ZBPerson alloc] init];
[p personInstanceMethod];
}
上面代碼中有兩個(gè)類闰靴,ZBPerson
彪笼、ZBStudent
,其中ZBStudent
是繼承于ZBPerson
蚂且,也沒有實(shí)現(xiàn)ZBPerson
中的方法personInstanceMethod
配猫。
下面進(jìn)行方法交換
@implementation ZBStudent (ZB)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[ZBRuntimeTool zb_betterMethodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(zb_studentInstanceMethod)];
});
}
- (void)zb_studentInstanceMethod{
NSLog(@"ZBStudent分類添加的zb對(duì)象方法:%s",__func__);
[self zb_studentInstanceMethod];
}
@end
如果ZBRuntimeTool
類里面的方法交換還是和上面寫的一樣的話,運(yùn)行結(jié)果肯定是奔潰的杏死,這里就不過多描述這個(gè)錯(cuò)誤泵肄,下面是優(yōu)化過的代碼
+ (void)zb_betterMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"傳入的交換類不能為空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
// 一般交換方法: 交換自己有的方法 -- 走下面 因?yàn)樽约河幸馕短砑臃椒ㄊ? // 交換自己沒有實(shí)現(xiàn)的方法:
// 首先第一步:會(huì)先嘗試給自己添加要交換的方法 :personInstanceMethod (SEL) -> swiMethod(IMP)
// 然后再將父類的IMP給swizzle personInstanceMethod(imp) -> swizzledSEL
//oriSEL:personInstanceMethod
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);
}
}
這段代碼比之前的多了幾步操作捆交,第一步會(huì)先嘗試給自己添加要交換的方法,如果添加成功腐巢,說明本類中沒有實(shí)現(xiàn)這個(gè)方法品追,那么就直接添加一個(gè)swiMethod
的IMP
,然后通過方法class_replaceMethod
進(jìn)行替換冯丙;如果添加失敗肉瓦,說明類中存在了這個(gè)方法的IMP
,那么可以直接利用method_exchangeImplementations
方法進(jìn)行交換胃惜。
坑四:交換根本沒有實(shí)現(xiàn)的方法
顧名思義就是說交換的方法泞莉,不僅本類未實(shí)現(xiàn),其父類中也沒有實(shí)現(xiàn)蛹疯,同樣可以拿上面例子說起戒财,ZBStudent
類中的方法helloWorld
,在其父類以及本類中捺弦,都沒有實(shí)現(xiàn)饮寞。
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[ZBRuntimeTool zb_betterMethodSwizzlingWithClass:self oriSEL:@selector(helloWorld) swizzledSEL:@selector(zb_studentInstanceMethod)];
});
}
- (void)zb_studentInstanceMethod{
NSLog(@"ZBStudent分類添加的zb對(duì)象方法:%s",__func__);
[self zb_studentInstanceMethod];
}
運(yùn)行結(jié)果如下:
可以看出,進(jìn)入了遞歸列吼。
思考一下幽崩,這里為什么會(huì)進(jìn)入遞歸的死循環(huán)呢?
分析: Method-Swizzling
的原理就是進(jìn)行消息IMP
的交換寞钥,執(zhí)行上面load
方法后慌申,先會(huì)把方法helloWorld
的IMP
指向zb_studentInstanceMethod(IMP)
,然后把zb_studentInstanceMethod
方法的IMP
指向helloWorld(IMP)
理郑。注意了蹄溉,這里的helloWorld(IMP)
為空,意思是方法zb_studentInstanceMethod
的IMP
并沒有改變成功您炉,還是指向了自己的IMP
柒爵,和方法helloWorld
一樣。所以會(huì)一直調(diào)用方法zb_studentInstanceMethod
赚爵,進(jìn)入了死循環(huán)的遞歸棉胀。
優(yōu)化代碼:
+ (void)zb_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"傳入的交換類不能為空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swizzledSEL);
if (!oriMethod) {
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
// IMP指向了一個(gè)空的block方法(空IMP)
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);
}
}
運(yùn)行結(jié)果:
牛逼哦 老鐵們!<较ァ唁奢!
坑五:類方法--類方法存在元類中
其實(shí)類方法的method-swizzling
和對(duì)象方法基本類似,但是有一個(gè)很大的區(qū)別窝剖,就是類方法存在元類中麻掸,代碼如下
@implementation ZBStudent (ZB)
+ (void)load{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[ZBRuntimeTool zb_bestMethodSwizzlingWithClass:self oriSEL:@selector(helloWorld1) swizzledSEL:@selector(zb_studentInstanceMethod1)];
});
}
+ (void)zb_studentInstanceMethod1{
NSLog(@"ZBStudent分類添加的zb對(duì)象方法:%s",__func__);
[[self class] zb_studentInstanceMethod1];
}
@end
+ (void)zb_bestMethodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swizzledSEL{
if (!cls) NSLog(@"傳入的交換類不能為空");
Method swiCLassMethod = class_getClassMethod([cls class], swizzledSEL);
Method oriClassMethod = class_getClassMethod([cls class], oriSEL);
if (!oriClassMethod) {
class_addMethod(object_getClass([cls class]), oriSEL, method_getImplementation(swiCLassMethod), method_getTypeEncoding(swiCLassMethod));
method_setImplementation(swiCLassMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}
BOOL didAddMethod = class_addMethod(object_getClass([cls class]), oriSEL, method_getImplementation(swiCLassMethod), method_getTypeEncoding(swiCLassMethod));
if (didAddMethod) {
class_replaceMethod(object_getClass([cls class]), swizzledSEL, method_getImplementation(oriClassMethod), method_getTypeEncoding(oriClassMethod));
}else {
method_exchangeImplementations(oriClassMethod, swiCLassMethod);
}
}
代碼執(zhí)行結(jié)果:
嗯,上面代碼中多處用到
object_getClass([cls class])
赐纱,實(shí)際上這個(gè)就是元類论笔,關(guān)于類方法的一些IMP
的交換都是在元類中進(jìn)行的采郎,因?yàn)?strong>類方法存在元類中千所。
以上總結(jié)了一些關(guān)于Method-Swizzling
的坑狂魔,當(dāng)然肯定不止這幾個(gè)。在日常開發(fā)中還是慎用淫痰,不過真的很強(qiáng)大最楷。上面的幾種模式下的代碼,值得細(xì)細(xì)閱讀待错,可以增加對(duì)Method-Swizzling
的理解籽孙,如有錯(cuò)誤,希望指出火俄,謝謝
如果對(duì)此很感興趣犯建,可以了解一下AOP切面設(shè)計(jì)的源碼實(shí)現(xiàn)