iOS-黑魔法Method-Swizzling的原理与使用
NSLog(@"SHOW TIME");
一、Method-Swizzling是个啥
Method-Swizzling实际就是更换方法所对应的实现函数,如下图1-1,更换前调用方法selector1执行的是IMP1函数,更换后调用selector1执行的就变成了IMP2。
图1-1 method-swizzling更换两个selector方法的实现
二、代码演示下流程
2.1 首先创建个继承NSObject的类,.h文件声明两个实例方法test1、test2:
@interface MethodSwizzling : NSObject
-(void)test1;
-(void)test2;
@end
2.2 在.m文件中实现这两个方法,并交换两个方法的实现:
#import "MethodSwizzling.h"
#import <objc/runtime.h>
@implementation MethodSwizzling
+ (void)load {
// 获取test1、test2方法
Method method_test1 = class_getInstanceMethod(self, @selector(test1));
Method method_test2 = class_getInstanceMethod(self, @selector(test2));
// 交换两个方法的实现
method_exchangeImplementations(method_test1, method_test2);
}
-(void)test1 {
NSLog(@"test1");
}
-(void)test2 {
NSLog(@"test2");
}
@end
2.3 其他地方调用test1方法:
MethodSwizzling *obj = [[MethodSwizzling alloc] init];
[obj test1];
2.4 输出结果为@“test2”:
2020-08-06 18:30:08.966946+0800 RuntimeTest[28219:4174853] test2
由此可知,在方法交换后,调用test1方法,实际执行的函数是test2。这就是黑魔法Method-Swizzling的作用:更换实际执行的方法函数。
2.5 如果此时在test2方法的输出语句前调用test2[self test2],结果又会是什么呢?
-(void)test2 {
// 实际调用test1函数
[self test2];
NSLog(@"test2");
}
2.6 输出结果为@“test1”、@“test2”:
2020-08-06 19:23:35.309874+0800 RuntimeTest[29953:4227972] test1
2020-08-06 19:23:35.310124+0800 RuntimeTest[29953:4227972] test2
小朋友,你是否有很多的问号???
我们来分析一波:
① 当程序考试运行时就会调用+(void)load方法,调用时机就不在这里解释了,在load方法内对两个方法函数进行交换;
② 通过[obj test1]调用test1方法,但此时test1方法的实际操作函数已经在①步骤中被调换到test2方法,所以[obj test1]实际走的是-(void)test2方法;
③ 此时test2方法内部执行[self test2],并不会造成循环,因为此时的test2方法的函数实现已经被调换指向了test1方法,所以会执行-(void)test1方法,输出@"test1"字符;
④ 最后调用test2方法的NSLog,输出@"test2"字符,执行完毕。
三、Method-Swizzling能用来干嘛
3.1 处理Button重复点击
.h文件
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface UIButton (QuickClick)
@property (nonatomic,assign) NSTimeInterval delayTime;
@end
NS_ASSUME_NONNULL_END
.m文件
#import "UIButton+QuickClick.h"
#import <objc/runtime.h>
@implementation UIButton (QuickClick)
static const char* delayTime_str = "delayTime_str";
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originMethod = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
Method replacedMethod = class_getInstanceMethod(self, @selector(miSendAction:to:forEvent:));
method_exchangeImplementations(originMethod, replacedMethod);
});
}
- (void)miSendAction:(nonnull SEL)action to:(id)target forEvent:(UIEvent *)event
{
if (self.delayTime > 0) {
if (self.userInteractionEnabled) {
[self miSendAction:action to:target forEvent:event];
}
self.userInteractionEnabled = NO;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW,
(int64_t)(self.delayTime * NSEC_PER_SEC)),
dispatch_get_main_queue(), ^{
self.userInteractionEnabled = YES;
});
}else{
[self miSendAction:action to:target forEvent:event];
}
}
- (NSTimeInterval)delayTime
{
return [objc_getAssociatedObject(self, delayTime_str) doubleValue];
}
- (void)setDelayTime:(NSTimeInterval)delayTime
{
objc_setAssociatedObject(self, delayTime_str, @(delayTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
@end
3.2 解决往Array或Dictionary中插入nil导致的crash
我们可以用method swizzling修改-[__NSDictionaryM setObject:forKey:]
方法,让它在设值时,先判断是否value为空,为空则不设置。代码如下:
@implementation NSMutableDictionary (Safe)
+ (void)load {
Class dictCls = NSClassFromString(@"__NSDictionaryM");
Method originalMethod = class_getInstanceMethod(dictCls, @selector(setObject:forKey:));
Method swizzledMethod = class_getInstanceMethod(dictCls, @selector(na_setObject:forKey:));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
- (void)na_setObject:(id)anObject forKey:(id<NSCopying>)aKey {
if (!anObject)
return;
[self na_setObject:anObject forKey:aKey];
}
@end
@implementation NSArray (Safe)
+ (void)load {
Method originalMethod = class_getClassMethod(self, @selector(arrayWithObjects:count:));
Method swizzledMethod = class_getClassMethod(self, @selector(na_arrayWithObjects:count:));
method_exchangeImplementations(originalMethod, swizzledMethod);
}
+ (instancetype)na_arrayWithObjects:(const id [])objects count:(NSUInteger)cnt {
id nObjects[cnt];
int i=0, j=0;
for (; i<cnt && j<cnt; i++) {
if (objects[i]) {
nObjects[j] = objects[i];
j++;
}
}
return [self na_arrayWithObjects:nObjects count:j];
}
@end
@implementation NSMutableArray (Safe)
+ (void)load {
Class arrayCls = NSClassFromString(@"__NSArrayM");
Method originalMethod1 = class_getInstanceMethod(arrayCls, @selector(insertObject:atIndex:));
Method swizzledMethod1 = class_getInstanceMethod(arrayCls, @selector(na_insertObject:atIndex:));
method_exchangeImplementations(originalMethod1, swizzledMethod1);
Method originalMethod2 = class_getInstanceMethod(arrayCls, @selector(setObject:atIndex:));
Method swizzledMethod2 = class_getInstanceMethod(arrayCls, @selector(na_setObject:atIndex:));
method_exchangeImplementations(originalMethod2, swizzledMethod2);
}
- (void)na_insertObject:(id)anObject atIndex:(NSUInteger)index {
if (!anObject)
return;
[self na_insertObject:anObject atIndex:index];
}
- (void)na_setObject:(id)anObject atIndex:(NSUInteger)index {
if (!anObject)
return;
[self na_setObject:anObject atIndex:index];
}
@end
NSLog(@"END...");