iOS 开发:『Runtime』详解(二)Method Swizzling

  • 本文首发于个人我的博客:『不羁阁』
  • 文章连接:传送门
  • 本文更新时间:2019年07月12日13:21:26

本文用来介绍 iOS 开发中『Runtime』中的黑魔法 Method Swizzling。经过本文,您将了解到:html

  1. Method Swizzling(动态方法交换)简介
  2. Method Swizzling 使用方法(四种方案)
  3. Method Swizzling 使用注意
  4. Method Swizzling 应用场景 4.1 全局页面统计功能 4.2 字体根据屏幕尺寸适配 4.3 处理按钮重复点击 4.4 TableView、CollectionView 异常加载占位图 4.5 APM(应用性能管理)、防止崩溃

文中示例代码在: bujige / YSC-Runtime-MethodSwizzlinggit


咱们在上一篇 iOS 开发:『Runtime』详解(一)基础知识 中,讲解了 iOS 运行时机制(Runtime 系统)的工做原理。包括消息发送以及转发机制的原理和流程。github

从这一篇文章开始,咱们来了解一下 Runtime 在实际开发过程当中,具体的应用场景。objective-c

这一篇咱们来学习一下被称为 Runtime 运行时系统中最具争议的黑魔法:Method Swizzling(动态方法交换)数组


1. Method Swizzling(动态方法交换)简介

Method Swizzling 用于改变一个已经存在的 selector 实现。咱们能够在程序运行时,经过改变 selector 所在 Class(类)的 method list(方法列表)的映射从而改变方法的调用。其实质就是交换两个方法的 IMP(方法实现)。安全

上一篇文章中咱们知道:Method(方法)对应的是 objc_method 结构体;而 objc_method 结构体 中包含了 SEL method_name(方法名)IMP method_imp(方法实现)bash

// objc_method 结构体
typedef struct objc_method *Method;

struct objc_method {
    SEL _Nonnull method_name;                    // 方法名
    char * _Nullable method_types;               // 方法类型
    IMP _Nonnull method_imp;                     // 方法实现
};
复制代码

Method(方法)SEL(方法名)IMP(方法实现)三者的关系能够这样来表示:网络

在运行时,Class(类) 维护了一个 method list(方法列表) 来肯定消息的正确发送。method list(方法列表) 存放的元素就是 Method(方法)。而 Method(方法) 中映射了一对键值对:SEL(方法名):IMP(方法实现)session

Method swizzling 修改了 method list(方法列表),使得不一样 Method(方法)中的键值对发生了交换。好比交换前两个键值对分别为 SEL A : IMP ASEL B : IMP B,交换以后就变为了 SEL A : IMP BSEL B : IMP A。如图所示:框架


2. Method Swizzling 使用方法

假如当前类中有两个方法:- (void)originalFunction;- (void)swizzledFunction;。若是咱们想要交换两个方法的实现,从而实现调用 - (void)originalFunction; 方法实际上调用的是 - (void)swizzledFunction; 方法,而调用 - (void)swizzledFunction; 方法实际上调用的是 - (void)originalFunction; 方法的效果。那么咱们须要像下边代码同样来实现。


2.1 Method Swizzling 简单使用

在当前类的 + (void)load; 方法中增长 Method Swizzling 操做,交换 - (void)originalFunction;- (void)swizzledFunction; 的方法实现。

#import "ViewController.h"
#import <objc/runtime.h>

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self SwizzlingMethod];
    [self originalFunction];
    [self swizzledFunction];
}


// 交换 原方法 和 替换方法 的方法实现
- (void)SwizzlingMethod {
    // 当前类
    Class class = [self class];
    
    // 原方法名 和 替换方法名
    SEL originalSelector = @selector(originalFunction);
    SEL swizzledSelector = @selector(swizzledFunction);
    
    // 原方法结构体 和 替换方法结构体
    Method originalMethod = class_getInstanceMethod(class, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
    
    // 调用交换两个方法的实现
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

// 原始方法
- (void)originalFunction {
    NSLog(@"originalFunction");
}

// 替换方法
- (void)swizzledFunction {
    NSLog(@"swizzledFunction");
}

@end
复制代码

打印结果: 2019-07-12 09:59:19.672349+0800 Runtime-MethodSwizzling[91009:30112833] swizzledFunction 2019-07-12 09:59:20.414930+0800 Runtime-MethodSwizzling[91009:30112833] originalFunction

能够看出二者方法成功进行了交换。


刚才咱们简单演示了如何在当前类中如何进行 Method Swizzling 操做。但通常平常开发中,并非直接在原有类中进行 Method Swizzling 操做。更多的是为当前类添加一个分类,而后在分类中进行 Method Swizzling 操做。另外真正使用会比上边写的考虑东西要多一点,要复杂一些。

在平常使用 Method Swizzling 的过程当中,有几种很经常使用的方案,具体状况以下。

2.2 Method Swizzling 方案 A

在该类的分类中添加 Method Swizzling 交换方法,用普通方式

这种方式在开发中应用最多的。可是仍是要注意一些事项,我会在接下来的 3. Method Swizzling 使用注意 进行详细说明。

@implementation UIViewController (Swizzling)

// 交换 原方法 和 替换方法 的方法实现
+ (void)load {
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 当前类
        Class class = [self class];
        
        // 原方法名 和 替换方法名
        SEL originalSelector = @selector(originalFunction);
        SEL swizzledSelector = @selector(swizzledFunction);
        
        // 原方法结构体 和 替换方法结构体
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        /* 若是当前类没有 原方法的 IMP,说明在从父类继承过来的方法实现, * 须要在当前类中添加一个 originalSelector 方法, * 可是用 替换方法 swizzledMethod 去实现它 */
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            // 原方法的 IMP 添加成功后,修改 替换方法的 IMP 为 原始方法的 IMP
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            // 添加失败(说明已包含原方法的 IMP),调用交换两个方法的实现
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

// 原始方法
- (void)originalFunction {
    NSLog(@"originalFunction");
}

// 替换方法
- (void)swizzledFunction {
    NSLog(@"swizzledFunction");
}

@end
复制代码

2.3 Method Swizzling 方案 B

在该类的分类中添加 Method Swizzling 交换方法,可是使用函数指针的方式。

方案 B 和方案 A 的最大不一样之处在于使用了函数指针的方式,使用函数指针最大的好处是能够有效避免命名错误。

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

typedef IMP *IMPPointer;

// 交换方法函数
static void MethodSwizzle(id self, SEL _cmd, id arg1);
// 原始方法函数指针
static void (*MethodOriginal)(id self, SEL _cmd, id arg1);

// 交换方法函数
static void MethodSwizzle(id self, SEL _cmd, id arg1) {
    
    // 在这里添加 交换方法的相关代码
    NSLog(@"swizzledFunc");
    
    MethodOriginal(self, _cmd, arg1);
}

BOOL class_swizzleMethodAndStore(Class class, SEL original, IMP replacement, IMPPointer store) {
    IMP imp = NULL;
    Method method = class_getInstanceMethod(class, original);
    if (method) {
        const char *type = method_getTypeEncoding(method);
        imp = class_replaceMethod(class, original, replacement, type);
        if (!imp) {
            imp = method_getImplementation(method);
        }
    }
    if (imp && store) { *store = imp; }
    return (imp != NULL);
}

@implementation UIViewController (PointerSwizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzle:@selector(originalFunc) with:(IMP)MethodSwizzle store:(IMP *)&MethodOriginal];
    });
}

+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
    return class_swizzleMethodAndStore(self, original, replacement, store);
}

// 原始方法
- (void)originalFunc {
    NSLog(@"originalFunc");
}

@end
复制代码

2.4 Method Swizzling 方案 C

在其余类中添加 Method Swizzling 交换方法

这种状况通常用的很少,最出名的就是 AFNetworking 中的_AFURLSessionTaskSwizzling 私有类。_AFURLSessionTaskSwizzling 主要解决了 iOS7 和 iOS8 系统上 NSURLSession 差异的处理。让不一样系统版本 NSURLSession 版本基本一致。

static inline void af_swizzleSelector(Class theClass, SEL originalSelector, SEL swizzledSelector) {
    Method originalMethod = class_getInstanceMethod(theClass, originalSelector);
    Method swizzledMethod = class_getInstanceMethod(theClass, swizzledSelector);
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

static inline BOOL af_addMethod(Class theClass, SEL selector, Method method) {
    return class_addMethod(theClass, selector,  method_getImplementation(method),  method_getTypeEncoding(method));
}

@interface _AFURLSessionTaskSwizzling : NSObject

@end

@implementation _AFURLSessionTaskSwizzling

+ (void)load {
    if (NSClassFromString(@"NSURLSessionTask")) {
        
        NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
        NSURLSession * session = [NSURLSession sessionWithConfiguration:configuration];
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wnonnull"
        NSURLSessionDataTask *localDataTask = [session dataTaskWithURL:nil];
#pragma clang diagnostic pop
        IMP originalAFResumeIMP = method_getImplementation(class_getInstanceMethod([self class], @selector(af_resume)));
        Class currentClass = [localDataTask class];
        
        while (class_getInstanceMethod(currentClass, @selector(resume))) {
            Class superClass = [currentClass superclass];
            IMP classResumeIMP = method_getImplementation(class_getInstanceMethod(currentClass, @selector(resume)));
            IMP superclassResumeIMP = method_getImplementation(class_getInstanceMethod(superClass, @selector(resume)));
            if (classResumeIMP != superclassResumeIMP &&
                originalAFResumeIMP != classResumeIMP) {
                [self swizzleResumeAndSuspendMethodForClass:currentClass];
            }
            currentClass = [currentClass superclass];
        }
        
        [localDataTask cancel];
        [session finishTasksAndInvalidate];
    }
}

+ (void)swizzleResumeAndSuspendMethodForClass:(Class)theClass {
    Method afResumeMethod = class_getInstanceMethod(self, @selector(af_resume));
    Method afSuspendMethod = class_getInstanceMethod(self, @selector(af_suspend));

    if (af_addMethod(theClass, @selector(af_resume), afResumeMethod)) {
        af_swizzleSelector(theClass, @selector(resume), @selector(af_resume));
    }

    if (af_addMethod(theClass, @selector(af_suspend), afSuspendMethod)) {
        af_swizzleSelector(theClass, @selector(suspend), @selector(af_suspend));
    }
}

- (void)af_resume {
    NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");
    NSURLSessionTaskState state = [self state];
    [self af_resume];
    
    if (state != NSURLSessionTaskStateRunning) {
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidResumeNotification object:self];
    }
}

- (void)af_suspend {
    NSAssert([self respondsToSelector:@selector(state)], @"Does not respond to state");
    NSURLSessionTaskState state = [self state];
    [self af_suspend];
    
    if (state != NSURLSessionTaskStateSuspended) {
        [[NSNotificationCenter defaultCenter] postNotificationName:AFNSURLSessionTaskDidSuspendNotification object:self];
    }
}
复制代码

2.5 Method Swizzling 方案 D

优秀的第三方框架:JRSwizzleRSSwizzle

JRSwizzle 和 RSSwizzle 都是优秀的封装 Method Swizzling 的第三方框架。

  1. JRSwizzle 尝试解决在不一样平台和系统版本上的 Method Swizzling 与类继承关系的冲突。对各平台低版本系统兼容性较强。JRSwizzle 核心是用到了 method_exchangeImplementations 方法。在健壮性上先作了 class_addMethod 操做。

  2. RSSwizzle 主要用到了 class_replaceMethod 方法,避免了子类的替换影响了父类。并且对交换方法过程加了锁,加强了线程安全。它用很复杂的方式解决了 What are the dangers of method swizzling in Objective-C? 中提到的问题。是一种更安全优雅的 Method Swizzling 解决方案。


总结:

在开发中咱们一般使用方案 A,或者方案 D 中的第三方框架 RSSwizzle 来实现 Method Swizzling。在接下来 3. Method Swizzling 使用注意 中,咱们还讲看到不少的注意事项。这些注意事项并非为了吓退初学者,而是为了更好的使用 Method Swizzling 这一利器。而至于方案的选择,不管是选择哪一种方案,我认为只有最适合项目的方案才是最佳方案。


3. Method Swizzling 使用注意

Method Swizzling 之因此被你们称为黑魔法,就是由于使用 Method Swizzling 进行方法交换是一个危险的操做。Stack Overflow 上边有人提出了使用 Method Swizzling 会形成的一些危险和缺陷。更是把 Method Swizzling 比做是厨房里一把锋利的刀。有些人会惧怕刀过于锋利,会伤到本身,从而放弃了刀,或者使用了钝刀。可是事实倒是:锋利的刀比钝刀反而更加安全,前提是你有足够的经验。

Method Swizzling 可用于编写更好,更高效,更易维护的代码。但也可能由于被滥用而致使可怕的错误。因此在使用 Method Swizzling 的时候,咱们仍是要注意一些事项,以规避可能出现的危险。

下面咱们结合还有其余博主关于 Method Swizzling 的博文、 以及 Stack Overflow 上边提到的危险和缺陷,还有笔者的我的看法,来综合说一下使用 Method Swizzling 须要注意的地方。

  1. 应该只在 +load 中执行 Method Swizzling。

程序在启动的时候,会先加载全部的类,这时会调用每一个类的 +load 方法。并且在整个程序运行周期只会调用一次(不包括外部显示调用)。因此在 +load 方法进行 Method Swizzling 再好不过了。

而为何不用 +initialize 方法呢。

由于 +initialize 方法的调用时机是在 第一次向该类发送第一个消息的时候才会被调用。若是该类只是引用,没有调用,则不会执行 +initialize 方法。 Method Swizzling 影响的是全局状态,+load 方法能保证在加载类的时候就进行交换,保证交换结果。而使用 +initialize 方法则不能保证这一点,有可能在使用的时候起不到交换方法的做用。

  1. Method Swizzling 在 +load 中执行时,不要调用 [super load];

上边咱们说了,程序在启动的时候,会先加载全部的类。若是在 + (void)load方法中调用 [super load] 方法,就会致使父类的 Method Swizzling 被重复执行两次,而方法交换也被执行了两次,至关于互换了一次方法以后,第二次又换回去了,从而使得父类的 Method Swizzling 失效。

  1. Method Swizzling 应该老是在 dispatch_once 中执行。

Method Swizzling 不是原子操做,dispatch_once 能够保证即便在不一样的线程中也能确保代码只执行一次。因此,咱们应该老是在 dispatch_once 中执行 Method Swizzling 操做,保证方法替换只被执行一次。

  1. 使用 Method Swizzling 后要记得调用原生方法的实现。

在交换方法实现后记得要调用原生方法的实现(除非你很是肯定能够不用调用原生方法的实现):APIs 提供了输入输出的规则,而在输入输出中间的方法实现就是一个看不见的黑盒。交换了方法实现而且一些回调方法不会调用原生方法的实现这可能会形成底层实现的崩溃。

  1. 避免命名冲突和参数 _cmd 被篡改。
  1. 避免命名冲突一个比较好的作法是为替换的方法加个前缀以区别原生方法。必定要确保调用了原生方法的全部地方不会由于本身交换了方法的实现而出现意料不到的结果。 在使用 Method Swizzling 交换方法后记得要在交换方法中调用原生方法的实现。在交换了方法后而且不调用原生方法的实现可能会形成底层实现的崩溃。

  2. 避免方法命名冲突另外一个更好的作法是使用函数指针,也就是上边提到的 方案 B,这种方案能有效避免方法命名冲突和参数 _cmd 被篡改。

  1. 谨慎对待 Method Swizzling。

使用 Method Swizzling,会改变非本身拥有的代码。咱们使用 Method Swizzling 一般会更改一些系统框架的对象方法,或是类方法。咱们改变的不仅是一个对象实例,而是改变了项目中全部的该类的对象实例,以及全部子类的对象实例。因此,在使用 Method Swizzling 的时候,应该保持足够的谨慎。

例如,你在一个类中重写一个方法,而且不调用 super 方法,则可能会出现问题。在大多数状况下,super 方法是指望被调用的(除非有特殊说明)。若是你是用一样的思想来进行 Method Swizzling ,可能就会引发不少问题。若是你不调用原始的方法实现,那么你 Method Swizzling 改变的越多代码就越不安全。

  1. 对于 Method Swizzling 来讲,调用顺序 很重要。

+ load 方法的调用规则为:

  1. 先调用主类,按照编译顺序,顺序地根据继承关系由父类向子类调用;
  2. 再调用分类,按照编译顺序,依次调用;
  3. + load 方法除非主动调用,不然只会调用一次。

这样的调用规则致使了 + load 方法调用顺序并不必定肯定。一个顺序多是:父类 -> 子类 -> 父类类别 -> 子类类别,也多是 父类 -> 子类 -> 子类类别 -> 父类类别。因此 Method Swizzling 的顺序不能保证,那么就不能保证 Method Swizzling 后方法的调用顺序是正确的。

因此被用于 Method Swizzling 的方法必须是当前类自身的方法,若是把继承父类来的 IMP 复制到自身上面可能会存在问题。若是 + load 方法调用顺序为:父类 -> 子类 -> 父类类别 -> 子类类别,那么形成的影响就是调用子类的替换方法并不能正确调起父类分类的替换方法。缘由解释能够参考这篇文章:南栀倾寒:iOS界的毒瘤-MethodSwizzling

关于调用顺序更细致的研究能够参考这篇博文:玉令天下的博客:Objective-C Method Swizzling


4. Method Swizzling 应用场景

Method Swizzling 能够交换两个方法的实现,在开发中更多的是应用于系统类库,以及第三方框架的方法替换。在官方不公开源码的状况下,咱们能够借助 Runtime 的 Method Swizzling 为原有方法添加额外的功能,这使得咱们能够作不少有趣的事情。


4.1 全局页面统计功能

需求:在全部页面添加统计功能,用户每进入一次页面就统计一次。

若是有一天公司产品须要咱们来实现这个需求。咱们应该如何来实现?

先来思考一下有几种实现方式:

第一种:手动添加

直接在全部页面添加一次统计代码。你须要作的是写一份统计代码,而后在全部页面的 viewWillAppear: 中不停的进行复制、粘贴。

第二种:利用继承

建立基类,全部页面都继承自基类。这样的话只须要在基类的 viewDidAppear: 中添加一次统计功能。这样修改代码仍是不少,若是全部页面不是一开始继承自定义的基类,那么就须要把全部页面的继承关系修改一下,一样会形成不少重复代码,和极大的工做量。

第三种:利用分类 + Method Swizzling

咱们能够利用 Category 的特性来实现这个功能。若是一个类的分类重写了这个类的方法以后,那么该类的方法将会失效,起做用的将会是分类中重写的方法。

这样的话,咱们能够为 UIViewController 创建一个 Category,在分类中重写 viewWillAppear:,在其中添加统计代码,而后在全部的控制器中引入这个 Category。可是这样的话,全部继承自 UIViewController 类自身的 viewWillAppear: 就失效了,不会被调用。

这就须要用 Method Swizzling 来实现了。步骤以下:

  1. 在分类中实现一个自定义的xxx_viewWillAppear: 方法;
  2. 利用 Method Swizzling 将 viewDidAppear: 和自定义的 xxx_viewWillAppear: 进行方法交换。
  3. 而后在 xxx_viewWillAppear: 中添加统计代码和调用xxx_viewWillAppear:实现; 由于两个方法发生了交换,因此最后实质是调用了 viewWillAppear: 方法。
  • 代码实现:
#import "UIViewController+Swizzling.h"
#import <objc/runtime.h>

@implementation UIViewController (Swizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];
        
        SEL originalSelector = @selector(viewWillAppear:);
        SEL swizzledSelector = @selector(xxx_viewWillAppear:);
        
        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        
        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));
        
        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

#pragma mark - Method Swizzling

- (void)xxx_viewWillAppear:(BOOL)animated {
    
    if (![self isKindOfClass:[UIViewController class]]) {  // 剔除系统 UIViewController
        // 添加统计代码
        NSLog(@"进入页面:%@", [self class]);
    }
    
    [self xxx_viewWillAppear:animated];
}

@end
复制代码

4.2 字体根据屏幕尺寸适配

需求:全部的控件字体必须依据屏幕的尺寸等比缩放。

照例,咱们先来想一想几种实现方式。

第一种:手动修改

全部用到的 UIFont 的地方,手动判断,添加适配代码。一想到那个工做量,不忍直视。

第二种:利用宏定义

在 PCH 文件定义一个计算缩放字体的方法。在使用设置字体时,先调用宏定义的缩放字体的方法。可是这样一样须要修改全部用到的 UIFont 的地方。工做量依旧很大。

//宏定义
#define UISCREEN_WIDTH ([UIScreen mainScreen].bounds.size.width)

/**
 *  计算缩放字体的方法
 */
static inline CGFloat FontSize(CGFloat fontSize){
    return fontSize * UISCREEN_WIDTH / XXX_UISCREEN_WIDTH;
}
复制代码

第三种:利用分类 + Method Swizzling

  1. 为 UIFont 创建一个 Category。
  2. 在分类中实现一个自定义的 xxx_systemFontOfSize: 方法,在其中添加缩放字体的方法。
  3. 利用 Method Swizzling 将 systemFontOfSize: 方法和 xxx_systemFontOfSize: 进行方法交换。
  • 代码实现:
#import "UIFont+AdjustSwizzling.h"
#import <objc/runtime.h>

#define XXX_UISCREEN_WIDTH 375

@implementation UIFont (AdjustSwizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(systemFontOfSize:);
        SEL swizzledSelector = @selector(xxx_systemFontOfSize:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

+ (UIFont *)xxx_systemFontOfSize:(CGFloat)fontSize {
    UIFont *newFont = nil;
    newFont = [UIFont xxx_systemFontOfSize:fontSize * [UIScreen mainScreen].bounds.size.width / XXX_UISCREEN_WIDTH];
    
    return newFont;
}

@end
复制代码

注意:这种方式只适用于纯代码的状况,关于 XIB 字体根据屏幕尺寸适配,能够参考这篇博文: 小生不怕:iOS xib文件根据屏幕等比例缩放的适配


4.3 处理按钮重复点击

需求:避免一个按钮被快速屡次点击。

仍是来思考一下有几种作法。

第一种:利用 Delay 延迟,和不可点击方法。

这种方法很直观,也很简单。但就是工做量很大,须要在全部有按钮的地方添加代码。很不想认可:在以前项目中,我使用的就是这种方式。

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIButton *button = [[UIButton alloc]initWithFrame:CGRectMake(100, 100, 100, 100)];
    button.backgroundColor = [UIColor redColor];
    [button addTarget:self action:@selector(buttonClick:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

- (void)buttonClick:(UIButton *)sender {
    sender.enabled = NO;
    [self performSelector:@selector(changeButtonStatus:) withObject:sender afterDelay:0.8f];
    
    NSLog(@"点击了按钮");
}

- (void)changeButtonStatus:(UIButton *)sender {
    sender.enabled = YES;
}
复制代码

第二种:利用分类 + Method Swizzling

  1. UIControlUIButton 创建一个 Category。
  2. 在分类中添加一个 NSTimeInterval xxx_acceptEventInterval; 的属性,设定重复点击间隔
  3. 在分类中实现一个自定义的 xxx_sendAction:to:forEvent: 方法,在其中添加限定时间相应的方法。
  4. 利用 Method Swizzling 将 sendAction:to:forEvent: 方法和 xxx_sendAction:to:forEvent: 进行方法交换。
  • 代码实现:
#import "UIButton+DelaySwizzling.h"
#import <objc/runtime.h>

@interface UIButton()

// 重复点击间隔
@property (nonatomic, assign) NSTimeInterval xxx_acceptEventInterval;

@end


@implementation UIButton (DelaySwizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(sendAction:to:forEvent:);
        SEL swizzledSelector = @selector(xxx_sendAction:to:forEvent:);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)xxx_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
    
    // 若是想要设置统一的间隔时间,能够在此处加上如下几句
    if (self.xxx_acceptEventInterval <= 0) {
        // 若是没有自定义时间间隔,则默认为 0.4 秒
        self.xxx_acceptEventInterval = 0.4;
    }
    
    // 是否小于设定的时间间隔
    BOOL needSendAction = (NSDate.date.timeIntervalSince1970 - self.xxx_acceptEventTime >= self.xxx_acceptEventInterval);
    
    // 更新上一次点击时间戳
    if (self.xxx_acceptEventInterval > 0) {
        self.xxx_acceptEventTime = NSDate.date.timeIntervalSince1970;
    }
    
    // 两次点击的时间间隔小于设定的时间间隔时,才执行响应事件
    if (needSendAction) {
        [self xxx_sendAction:action to:target forEvent:event];
    }
}

- (NSTimeInterval )xxx_acceptEventInterval{
    return [objc_getAssociatedObject(self, "UIControl_acceptEventInterval") doubleValue];
}

- (void)setXxx_acceptEventInterval:(NSTimeInterval)xxx_acceptEventInterval{
    objc_setAssociatedObject(self, "UIControl_acceptEventInterval", @(xxx_acceptEventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSTimeInterval )xxx_acceptEventTime{
    return [objc_getAssociatedObject(self, "UIControl_acceptEventTime") doubleValue];
}

- (void)setXxx_acceptEventTime:(NSTimeInterval)xxx_acceptEventTime{
    objc_setAssociatedObject(self, "UIControl_acceptEventTime", @(xxx_acceptEventTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end
复制代码

参考博文:大斑马小斑马:IOS 防止UIButton 重复点击


4.4 TableView、CollectionView 异常加载占位图

在项目中遇到网络异常,或者其余各类缘由形成 TableView、CollectionView 数据为空的时候,一般须要加载占位图显示。那么加载占位图有没有什么好的方法或技巧?

第一种:刷新数据后进行判断

这应该是一般的作法。当返回数据,刷新 TableView、CollectionView 时候,进行判断,若是数据为空,则加载占位图。若是数据不为空,则移除占位图,显示数据。

第二种:利用分类 + Method Swizzling 重写 reloadData 方法。

以 TableView 为例:

  1. 为 TableView 创建一个 Category,Category 中添加刷新回调 block 属性、占位图 View 属性。
  2. 在分类中实现一个自定义的 xxx_reloadData 方法,在其中添加判断是否为空,以及加载占位图、隐藏占位图的相关代码。
  3. 利用 Method Swizzling 将 reloadData 方法和 xxx_reloadData 进行方法交换。
  • 代码实现:
#import <UIKit/UIKit.h>

NS_ASSUME_NONNULL_BEGIN

@interface UITableView (ReloadDataSwizzling)

@property (nonatomic, assign) BOOL firstReload;
@property (nonatomic, strong) UIView *placeholderView;
@property (nonatomic,   copy) void(^reloadBlock)(void);

@end

/*--------------------------------------*/

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

@implementation UITableView (ReloadDataSwizzling)


+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        Class class = [self class];

        SEL originalSelector = @selector(reloadData);
        SEL swizzledSelector = @selector(xxx_reloadData);

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL didAddMethod = class_addMethod(class,
                                            originalSelector,
                                            method_getImplementation(swizzledMethod),
                                            method_getTypeEncoding(swizzledMethod));

        if (didAddMethod) {
            class_replaceMethod(class,
                                swizzledSelector,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
    });
}

- (void)xxx_reloadData {
    if (!self.firstReload) {
        [self checkEmpty];
    }
    self.firstReload = NO;
    
    [self xxx_reloadData];
}


- (void)checkEmpty {
    BOOL isEmpty = YES; // 判空 flag 标示
    
    id <UITableViewDataSource> dataSource = self.dataSource;
    NSInteger sections = 1; // 默认TableView 只有一组
    if ([dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) {
        sections = [dataSource numberOfSectionsInTableView:self] - 1; // 获取当前TableView 组数
    }
    
    for (NSInteger i = 0; i <= sections; i++) {
        NSInteger rows = [dataSource tableView:self numberOfRowsInSection:i]; // 获取当前TableView各组行数
        if (rows) {
            isEmpty = NO; // 若行数存在,不为空
        }
    }
    if (isEmpty) { // 若为空,加载占位图
        if (!self.placeholderView) { // 若未自定义,加载默认占位图
            [self makeDefaultPlaceholderView];
        }
        self.placeholderView.hidden = NO;
        [self addSubview:self.placeholderView];
    } else { // 不为空,隐藏占位图
        self.placeholderView.hidden = YES;
    }
}

- (void)makeDefaultPlaceholderView {
    self.bounds = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height);
    XXXPlaceholderView *placeholderView = [[XXXPlaceholderView alloc] initWithFrame:self.bounds];
    __weak typeof(self) weakSelf = self;
    [placeholderView setReloadClickBlock:^{
        if (weakSelf.reloadBlock) {
            weakSelf.reloadBlock();
        }
    }];
    self.placeholderView = placeholderView;
}

- (BOOL)firstReload {
    return [objc_getAssociatedObject(self, @selector(firstReload)) boolValue];
}

- (void)setFirstReload:(BOOL)firstReload {
    objc_setAssociatedObject(self, @selector(firstReload), @(firstReload), OBJC_ASSOCIATION_ASSIGN);
}

- (UIView *)placeholderView {
    return objc_getAssociatedObject(self, @selector(placeholderView));
}

- (void)setPlaceholderView:(UIView *)placeholderView {
    objc_setAssociatedObject(self, @selector(placeholderView), placeholderView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (void (^)(void))reloadBlock {
    return objc_getAssociatedObject(self, @selector(reloadBlock));
}

- (void)setReloadBlock:(void (^)(void))reloadBlock {
    objc_setAssociatedObject(self, @selector(reloadBlock), reloadBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end
复制代码

参考博文:卖报的小画家Sure:零行代码为App添加异常加载占位图


4.5 APM(应用性能管理)、防止程序崩溃

  1. 经过 Method Swizzling 替换 NSURLConnection , NSURLSession 相关的原始实现(例如 NSURLConnection 的构造方法和 start 方法),在实现中加入网络性能埋点行为,而后调用原始实现。从而来监控网络。
  2. 防止程序崩溃,能够经过 Method Swizzling 拦截容易形成崩溃的系统方法,而后在替换方法捕获异常类型 NSException ,再对异常进行处理。最多见的例子就是拦截 arrayWithObjects:count: 方法避免数组越界,这种例子网上不少,就再也不展现代码了。

参考资料


最后

写 Method Swizzling 花费了整整两周的时间,其中查阅了大量的 Method Swizzling 相关的资料,但获得的收获是很值得的。同时但愿能带给你们一些帮助。

下一篇,我打算 Runtime 中 Category(分类)的底层原理。

文中如如有误,烦请指正,感谢。


iOS 开发:『Runtime』详解 系列文章:

还没有完成:

  • iOS 开发:『Runtime』详解(五)Crash 防御系统
  • iOS 开发:『Runtime』详解(六)Objective-C 2.0 结构解析
  • iOS 开发:『Runtime』详解(七)KVO 底层实现
相关文章
相关标签/搜索