程序员iOS

第四篇:runtime的实际应用场景——黑魔法(Method S

2017-08-16  本文已影响85人  意一ineyee

目录

一、什么是黑魔法
二、黑魔法的实际应用场景
 1、从全局上为导航栏添加返回按钮
 2、从全局上防止button的暴力点击
 3、刷新tableView、collectionView时,自动判断是否显示暂无数据提示图

本篇主要讲解runtime的实际应用场景:黑魔法。是对方法应用的一个例子。

一、什么是黑魔法


黑魔法其实就是指我们在运行时(更具体的说是在编译结束到方法真正被调用之前这段空档期)改变一个方法的实现,它没那么神秘,就这么简单。

举个例子,比方说我们想在每个ViewController加载完成后都打印一下它的名字,有三种方案:

#import "UIViewController+MethodSwizzling.h"
#import <objc/runtime.h>

@implementation UIViewController (MethodSwizzling)

// 把方法的替换操作写在类的+load方法里来,来保证替换操作肯定执行了
+ (void)load {

    // 用dispatch_once来保证方法的替换操作只执行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        // 获取方法的选择子
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(yy_viewDidLoad);
        
        // 获取实例方法
        Method originalMethod = class_getInstanceMethod(self, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
        
        // 获取方法的实现
        IMP originalIMP = method_getImplementation(originalMethod);
        IMP swizzleIMP = method_getImplementation(swizzledMethod);
        
        // 获取方法的参数和返回值信息
        const char *originalTypeEncoding = method_getTypeEncoding(originalMethod);
        const char *swizzledTypeEncoding = method_getTypeEncoding(swizzledMethod);
        
        // 先尝试添加方法,因为如果原生方法根本没实现的话,是交换不成功的
        BOOL didAddMethod = class_addMethod(self, originalSelector, swizzleIMP, swizzledTypeEncoding);
        
        if (didAddMethod) {// 原生方法没实现,此时originalSelector已经指向新方法,我们把swizzledSelector指向原生方法,为的下面新方法还要调用一下原生方法,避免丢掉原生方法的实现
            
            class_replaceMethod(self, swizzledSelector, originalIMP, originalTypeEncoding);
        } else {// 原生方法实现了,直接交换两个方法

            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)yy_viewDidLoad {
    
    // 调用一下方法的原生实现,避免丢掉方法的原生实现而导致不可预知的bug。这里不会产生死循环,因为此时yy_viewDidLoad已经指向系统的原生方法viewDidLoad了
    [self yy_viewDidLoad];
    
    NSLog(@"===========>%@", [self class]);
}

@end

不过使用黑魔法一定要慎重,不能滥用,否则可能出现你不可预知的bug,有下面几点需要注意:

二、黑魔法的实际应用场景


黑魔法的实际应用场景主要就是:

下面仅仅是举三个我在实际开发中用到黑魔法的例子,只要我们理解了黑魔法其实就是指我们在运行时(更具体的说是在编译结束到方法真正被调用之前这段空档期)改变一个方法的实现这一概念,就可以按自己的开发需求灵活的运用它了。

1、从全局上为导航栏添加返回按钮

开发中,我们几乎总是要为一个ViewController添加一个返回按钮,添加的方案也有很多种:

#import "UIViewController+YY_NavigationBar.h"
#import <objc/runtime.h>

@implementation UIViewController (YY_NavigationBar)

+ (void)load {
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        SEL originalSelector = @selector(viewDidLoad);
        SEL swizzledSelector = @selector(yy_viewDidLoad);
        
        Method originalMethod = class_getInstanceMethod(self, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
        
        IMP originalIMP = method_getImplementation(originalMethod);
        IMP swizzleIMP = method_getImplementation(swizzledMethod);
        
        const char *originalTypeEncoding = method_getTypeEncoding(originalMethod);
        const char *swizzledTypeEncoding = method_getTypeEncoding(swizzledMethod);
        
        BOOL didAddMethod = class_addMethod(self, originalSelector, swizzleIMP, swizzledTypeEncoding);
        
        if (didAddMethod) {
            
            class_replaceMethod(self, swizzledSelector, originalIMP, originalTypeEncoding);
        } else {
            
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)yy_viewDidLoad {
    
    [self yy_viewDidLoad];
    
    if (self.navigationController.viewControllers.count > 1) {// 控制器数量超过两个才添加
        
        self.navigationItem.leftBarButtonItem = [[UIBarButtonItem alloc] initWithTitle:@"返回" style:(UIBarButtonItemStylePlain) target:self action:@selector(yy_leftBarButtonItemAction:)];
    }
}

- (void)yy_leftBarButtonItemAction:(UIBarButtonItem *)leftBarButtonItem {
    
    [self.navigationController popViewControllerAnimated:YES];
}

@end
2、从全局上防止button的暴力点击

开发中,我们经常会添加button的点击事件,因此防止button的暴力点击就显得很有必要,否则很容易出现bug。考虑一下方案:

- (IBAction)buttonAction:(UIButton *)button {
    
    // 禁掉button的userInteractionEnabled
    button.userInteractionEnabled = NO;
    
    // 执行button的点击的事件,这里假设事件在3s后结束
    NSLog(@"11111111111");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        
        // 点击事件处理完再打开button的userInteractionEnabled
        button.userInteractionEnabled = YES;
    });
}

这样做确实可以防止button的暴力点击,但是有一个麻烦事儿在于我们要为项目里所有button的点击事件都分别添加这样的处理,而且由于不同button的点击事件不一样,我们还没办法把其中的公共部分给提取出来,所以这种方案工作量太大,可以放弃。

方案一虽然被我们放弃了,但它的实现思路还是可取的,它的实现思路其实就是:第一次点击button的时候,让button响应事件,然后后面如果出现对button的暴力点击,则不让button响应事件

根据这一实现思路,我们可以通过判断这一次点击button和上一次点击button的时间间隔,来决定此次点击是否被认定为暴力点击,如果被认定为暴力点击则不让button处理事件,否则让button正常处理事件,这个时间间隔由我们自己设定

此外,我们知道所有继承自UIControl的类都能响应事件,而当它们处理事件时都会触发sendAction:to:forEvent:方法,因此我们可以用黑魔法替换掉这个方法的原生实现,为它新增一小点功能----即我们上面所陈述的实现思路。

#import "UIButton+YY_PreventViolentClick.h"
#import <objc/runtime.h>

#define kTwoTimeClickTimeInterval 1.0// 两次点击的时间间隔,用来确定后一次点击是否被认定为暴力点击

@interface UIButton ()

@property (nonatomic, assign) NSTimeInterval yy_lastTimeClickTimestamp;// 上一次点击的时间戳

@end

@implementation UIButton (YY_PreventViolentClick)

+ (void)load {
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        SEL originalSelector = @selector(sendAction:to:forEvent:);
        SEL swizzledSelector = @selector(yy_sendAction:to:forEvent:);
        
        Method originalMethod = class_getInstanceMethod(self, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
        
        IMP originalIMP = method_getImplementation(originalMethod);
        IMP swizzleIMP = method_getImplementation(swizzledMethod);
        
        const char *originalTypeEncoding = method_getTypeEncoding(originalMethod);
        const char *swizzledTypeEncoding = method_getTypeEncoding(swizzledMethod);
        
        BOOL didAddMethod = class_addMethod(self, originalSelector, swizzleIMP, swizzledTypeEncoding);
        
        if (didAddMethod) {
            
            class_replaceMethod(self, swizzledSelector, originalIMP, originalTypeEncoding);
        } else {
            
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)yy_sendAction:(SEL)action to:(nullable id)target forEvent:(nullable UIEvent *)event  {
    
    if ([[self class] isEqual:[UIButton class]]) {// 防止替换掉UIButton类簇里子类方法的实现
        
        // 获取此次点击的时间戳
        NSTimeInterval currentTimeClickTimestamp = [[NSDate date] timeIntervalSince1970];
        
        if (currentTimeClickTimestamp - self.yy_lastTimeClickTimestamp < kTwoTimeClickTimeInterval) {// 如果此次点击和上一次点击的时间间隔小于我们设定的时间间隔,则判定此次点击为暴力点击,什么都不做
            
            return;
        } else {// 否则我们判定此次点击为正常点击,button正常处理事件
            
            // 记录上次点击的时间戳
            self.yy_lastTimeClickTimestamp = currentTimeClickTimestamp;
            
            [self yy_sendAction:action to:target forEvent:event];
        }
    }else {
        
        [self yy_sendAction:action to:target forEvent:event];
    }
}

- (void)setYy_lastTimeClickTimestamp:(NSTimeInterval)yy_lastTimeClickTimestamp {
    
    objc_setAssociatedObject(self, @"yy_lastTimeClickTimestamp", @(yy_lastTimeClickTimestamp), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSTimeInterval)yy_lastTimeClickTimestamp {
    
    return [objc_getAssociatedObject(self, @"yy_lastTimeClickTimestamp") doubleValue];
}

@end
3、刷新tableView、collectionView时,自动判断是否显示暂无数据提示图

当我们遇到请求数据为空时,就需要为tableView和collectionView添加一个暂无数据的提示图。考虑一下方案(tableView和collectionView类似,下面以tableView为例):

-----------UITableView+YY_PromptImage.h-----------

@interface UITableView (YY_PromptImage)

/// 提示图的名字
@property (nonatomic, copy) NSString *yy_promptImageName;
/// 点击提示图的回调
@property (nonatomic, copy) void(^yy_didTapPromptImage)(void);

/// 不使用该分类里的这套判定规则
@property (nonatomic, assign) BOOL yy_dontUseThisCategory;

@end


-----------UITableView+YY_PromptImage.m-----------

#import "UITableView+YY_PromptImage.h"
#import <objc/runtime.h>

@interface UITableView ()

// 已经调用过reloadData方法了
@property (nonatomic, assign) BOOL yy_hasInvokedReloadData;

// 提示图
@property (nonatomic, strong) UIImageView *yy_promptImageView;

@end

@implementation UITableView (YY_PromptImage)

+ (void)load {
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        SEL originalSelector = @selector(reloadData);
        SEL swizzledSelector = @selector(yy_reloadData);
        
        Method originalMethod = class_getInstanceMethod(self, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
        
        IMP originalIMP = method_getImplementation(originalMethod);
        IMP swizzleIMP = method_getImplementation(swizzledMethod);
        
        const char *originalTypeEncoding = method_getTypeEncoding(originalMethod);
        const char *swizzledTypeEncoding = method_getTypeEncoding(swizzledMethod);
        
        BOOL didAddMethod = class_addMethod(self, originalSelector, swizzleIMP, swizzledTypeEncoding);
        
        if (didAddMethod) {
            
            class_replaceMethod(self, swizzledSelector, originalIMP, originalTypeEncoding);
        } else {
            
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)yy_reloadData {
    
    if ([[self class] isEqual:[UITableView class]] && !self.yy_dontUseThisCategory) {// 防止替换掉UITableView类簇里子类方法的实现
        
        [self yy_reloadData];
        
        if (self.yy_hasInvokedReloadData) {// 而是只在请求数据完成后,调用reloadData刷新界面时才处理提示图的显隐
        
            [self yy_handlePromptImage];
        } else {// tableView第一次加载的时候会自动调用一下reloadData方法,这一次调用我们不处理提示图的显隐

            self.yy_hasInvokedReloadData = YES;
        }
    } else {
        
        [self yy_reloadData];
    }
}


#pragma mark - private method

// 提示图的显隐
- (void)yy_handlePromptImage {
    
    if ([self yy_dataIsEmpty]) {
        
        [self yy_showPromptImage];
    }else {
        
        [self yy_hidePromptImage];
    }
}

// 判断请求到的数据是否为空
- (BOOL)yy_dataIsEmpty {
    
    // 获取分区数
    NSInteger sections = 0;
    if ([self.dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) {// 如果外界实现了该方法,则读取外界提供的分区数
        
        sections = [self numberOfSections];
    } else {// 如果外界没实现该方法,系统不是会自动给我们返回一个分区嘛
        
        sections = 1;
    }
    
    if (sections == 0) {// 分区数为0,说明数据为空
        
        return YES;
    }
    
    
    // 分区数不为0,则需要判断每个分区下的行数
    for (int i = 0; i < sections; i ++) {
        
        // 获取各个分区的行数
        NSInteger rows = [self numberOfRowsInSection:i];
        
        if (rows != 0) {// 但凡有一个分区下的行数不为0,说明数据不为空
            
            return NO;
        }
    }
    
    
    // 如果所有分区下的行数都为0,才说明数据为空
    return YES;
}

// 显示提示图
- (void)yy_showPromptImage {
    
    if (self.yy_promptImageView == nil) {
        
        self.yy_promptImageView = [[UIImageView alloc] initWithFrame:self.backgroundView.bounds];
        self.yy_promptImageView.backgroundColor = [UIColor clearColor];
        self.yy_promptImageView.contentMode = UIViewContentModeCenter;
        self.yy_promptImageView.userInteractionEnabled = YES;
        
        if (self.yy_promptImageName.length == 0) {
            
            self.yy_promptImageName = @"YY_PromptImage";
        }
        self.yy_promptImageView.image = [UIImage imageNamed:self.yy_promptImageName];
        
        UITapGestureRecognizer *tapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(yy_didTapPromptImage:)];
        [self.yy_promptImageView addGestureRecognizer:tapGestureRecognizer];
    }
    
    self.backgroundView = self.yy_promptImageView;
}

// 隐藏提示图
- (void)yy_hidePromptImage {
    
    self.backgroundView = nil;
}

// 点击提示图的回调
- (void)yy_didTapPromptImage:(UITapGestureRecognizer *)tapGestureRecognizer {
    
    if (self.yy_didTapPromptImage) {
        
        self.yy_didTapPromptImage();
    }
}


#pragma mark - setter, getter

- (void)setYy_hasInvokedReloadData:(BOOL)yy_hasInvokedReloadData {
    
    objc_setAssociatedObject(self, @"yy_hasInvokedReloadData", @(yy_hasInvokedReloadData), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)yy_hasInvokedReloadData {
    
    return [objc_getAssociatedObject(self, @"yy_hasInvokedReloadData") boolValue];
}

- (void)setYy_promptImageView:(UIImageView *)yy_promptImageView {
    
    objc_setAssociatedObject(self, @"yy_promptImageView", yy_promptImageView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (UIImageView *)yy_promptImageView {
    
    return objc_getAssociatedObject(self, @"yy_promptImageView");
}

- (void)setYy_promptImageName:(NSString *)yy_promptImageName {
    
    objc_setAssociatedObject(self, @"yy_promptImageName", yy_promptImageName, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)yy_promptImageName {
    
    return objc_getAssociatedObject(self, @"yy_promptImageName");
}

- (void)setYy_didTapPromptImage:(void (^)(void))yy_didTapPromptImage {
 
    objc_setAssociatedObject(self, @"yy_didTapPromptImage", yy_didTapPromptImage, OBJC_ASSOCIATION_COPY);
}

- (void (^)(void))yy_didTapPromptImage {
    
    return objc_getAssociatedObject(self, @"yy_didTapPromptImage");
}

- (void)setYy_dontUseThisCategory:(BOOL)yy_dontUseThisCategory {
 
    objc_setAssociatedObject(self, @"yy_dontUseThisCategory", @(yy_dontUseThisCategory), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (BOOL)yy_dontUseThisCategory {
 
    return [objc_getAssociatedObject(self, @"yy_dontUseThisCategory") boolValue];
}

@end
上一篇下一篇

猜你喜欢

热点阅读