欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

iOS-黑魔法Method-Swizzling的原理与使用

程序员文章站 2024-03-21 23:57:52
...
NSLog(@"SHOW TIME");

一、Method-Swizzling是个啥

Method-Swizzling实际就是更换方法所对应的实现函数,如下图1-1,更换前调用方法selector1执行的是IMP1函数,更换后调用selector1执行的就变成了IMP2。

iOS-黑魔法Method-Swizzling的原理与使用

                                           图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...");