iOS底层探索 --- Runtime(二)Method Swizzling

image

本文摘抄自iOS 开发:『Runtime』详解(二)Method Swizzling,不做任何商业用途。优秀的作品要大家一起欣赏,如有疑问请联系删除。

本文主要探索iOS开发中「Runtime」中的黑魔法Method Swizzling。本文主要探索一下几点:

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

1、Method Swizzling (动态交换) 简介

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

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

/// An opaque type that represents a method in a class definition.
/// 代表类定义中一个方法的不透明类型
typedef struct objc_method *Method;

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

Method (方法)SEL (方法名)IMP (方法实现)三者的关系可以这样来表示:

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

Method Swizzling修改了method list (方法列表),使得不同Method (方法)中的键值对发生了交换。比如交换前两个键值分别为:SEL A : IMP ASEL B : IMP B,交换之后变成了SEL A : IMP BSEL B : IMP A。如图所示:

image


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"
#include "objc/runtime.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    [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");
}

打印结果:
2021-05-25 16:08:22.888085+0800 test[25546:896509] -----------> swizzledFunction
2021-05-25 16:08:22.888222+0800 test[25546:896509] -----------> originalFunction

可以看出,两个方法已经成功交换了。
在日常开发的过程中,Method Swizzling的操作不会这么简单,一般是放在分类里面去执行,另外还有一些其他方面的因素要考虑。下面我们一起来探索一下日常开发的过程中,Method Swizzling该如何使用。

2.2、Method Swizzling 方案一

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

这种方法在开发中应用的最多。但是还是有一些注意事项,我们在最后会着重强调一下注意事项。

#import "Person+Swizzling.h"
#include "objc/runtime.h"

@implementation Person (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 方案二

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

方案二方案一 最大的不同就是使用了函数指针的方式,使用函数指针最大的好处是可以有效避免命名错误。

#import "Person+SwizzlingTwo.h"
#import 

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);
}
/**
 @param class       当前类
 @param original    原始方法名
 @param replacement 替代方法实现(替代品)
 @param store       方法实现的指针(IMP指针)
 */
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);
        ///class_replaceMethod 返回的是原方法对应的IMP
        ///如果原方法没有实现就返回nil
        ///class_replaceMethod会把原方法的IMP指向另一个方法的实现
        ///此处执行完之后,原始方法的IMP指向的是替代方法的IMP
        imp = class_replaceMethod(class, original, replacement, type);
        if (!imp) {
            imp = method_getImplementation(method);
        }
    }
    if (imp && store) {
        ///已经替换过的IMP 和 IMP指针都存在的话
        ///将替换过的IMP 赋值给 原始的SEL指针内容
        ///注意store 是 (IMP *)&MethodOriginal
        *store = imp;
    }
    return imp != NULL;
}

@implementation Person (SwizzlingTwo)

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

/**
 @param original        原始方法的名字
 @param replacement     替换方法的实现
 @param store           方法实现的指针
 */
+ (BOOL)swizzle:(SEL)original with:(IMP)replacement store:(IMPPointer)store {
    return class_swizzleMethodAndStore(self, original, replacement, store);
}

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

2.4、Method Swizzling 方案三

优秀的第三方框架:jrswizzle 和 RSSwizzle

jrswizzleRSSwizzle 都是优秀的封装 ·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·解决方案。

3、Method Swizzling 使用注意

·Method Swizzling· 之所以被大家称为黑魔法,就是因为使用 ·Method Swizzling· 进行方法交换是一个危险的操作。·Stack Overflow· 上面有人提出了使用 ·Method Swizzling· 会造成的一些危险和缺陷。

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

下面我们一起来看一下在使用 ·Method Swizzling· 的时候需要注意什么?

1、应该只在+load中执行 ·Method Swizzling·

程序在启动的时候,会先加载所有的类,这时会调用每个类的+load方法。而且在整个程序运行周期中只会调用一次(不包括外部显示调用)。所以在+load方法中进行 ·Method Swizzling· 是最好的选择了。

那为什么不在+initialize方法中使用呢?

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

2、·Method Swizzling· 在+load中执行时,不要调用[super load];

上面我们说了,程序在启动的时候,会先加载所有类。如果在+(void)load方法中调用[super load]方法,就会导致父类的Method Swizzling被重复执行两次,而方法交换也被执行了两次,相当于互转了一次方法后,第二次又换回来了,从而是的父类的Method Swizzling失效。

3、·Method Swizzling· 应该总在dispatch_once中执行。

·Method Swizzling· 不是原子操作,dispatch_once可以保证即使在不同的线程中也能确保代码只执行一次。所以,我们应该总在dispatch_once中执行 ·Method Swizzling· 操作,保证方法替换只被执行一次。

4、使用 ·Method Swizzling· 后要记得调用原生方法的实现。

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

5、避免命名冲突和参数_cmd被篡改。

  1. 避免命名冲突一个比较好的做法是为替换的的方法交个前缀,以区别原生方法。一定要确保调用了原生方法的所有地方不会因为自己交换了方法的实现而出现意料不到的结果。
  2. 避免方法命名冲突另一更好的做法就是使用函数指针;也就是上面的方案二,这种方案能有效的避免方法命名冲突和参数_cmd被篡改。

6、谨慎对待 ·Method Swizzling·

使用 ·Method Swizzling·,会改变非自己拥有的代码。我么使用 ·Method Swizzling· 通常会更改一些系统框架的对象方法,或是类方法。我们改变的不只是一个对象实例,而是改变了项目中所有的该类的对象实例,以及所有子类的对象实例。所以,在使用 ·Method Swizzling· 的时候,应该保持足够的谨慎。

例如,在一个类中重写一个方法,并且不调用super方法,则可能出现问题。在大多数情况下,super方法是期望被调用的(除非有特殊说明)。如果你是用同样的思维方式进行 ·Method Swizzling·,可能就会引起很多问题。如果你不调用原始的方法实现,那么你的 ·Method Swizzling· 改变的越多,代码就越不安全。

7、对于 ·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· 可以交换两个方法的实现,在开发中更多的是应用与系统类库,以及第三方框架的方法替换。在官方不公开源码的情况下,我们可以借助RuntimeMethod Swizzling为原有方法添加额外的功能,这使得我们可以做很多有趣的事情。

4.1、全局页面统计功能

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

如果有一天公司产品需要我们来实现这个需求。我们应该如何实现呢?
先来思考一下有几种实现方式:

第一种:手动添加

直接在所有页面添加一次统计代码。你需要做的是写一份统计代码,然后在所有页面的viewWillAppear:中不停的进行复制、粘贴。

第二种:利用继承

创建基类,所有页面都继承自基类。这样的话只需要在基类的viewDidAppear:中添加一次统计功能。这样修改代码还是很多,如果所有页面不是一开始继承自定的基类,那么就需要把所有页面的继承关系修改一下,同样会造成很多重复代码,和极大的工作量。

第三种:利用分类 + Method Swizzling

我们可以利用Category的特性来实现这个功能。如果一个类的分类重写了这个类的方法之后,那么该类的方法就会失效,起作用的将会是分类中重写的方法。

这样的话,我们可以为UIViewController建立一个Category,在分类中重写viewWillAppear:,在其中添加统计代码,然后在所有的控制器中引入Category。但是这样的话,所有继承自UIViewContory自身的viewWillAppear:就会失效,不会被调用。

这时候就需要 ·Method Swizzling· 来实现了。步骤如下:

  1. 在分类中实现一个自定义的xxx_viewWillAppear:方法;
  2. 利用 ·Method Swizzling· 将viewWillAppear:和自定义的xxx_viewWillAppear:进行方法交换;
  3. 然后在xxx_viewWillAppear:中添加统计代码和调用xxx_viewWillAppear:实现;由于两个方法发生了交换,所以最后实质是调用了viewWillAppear:方法。

代码实现:

#import "UIViewController+Swizzling.h"
#import 

@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);
        }
    
    });
}

- (void)xxx_viewWillAppear:(BOOL)animated {
    
    if (![self isMemberOfClass:[UIViewController class]] ||
        ![self isMemberOfClass:[UINavigationController class]] ||
        ![self isMemberOfClass:[UITabBarController class]]) {
        
        ///添加统计代码
        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 

#define XXX_UISCREEN_WIDTH 375

@implementation UIFont (AdjustSwizzling)

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        // 类方法存储在元类对象中,我们通过 object_getClass 获取 self.isa 所指向的元类
        Class class = object_getClass((id)self);
        
        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;
}

注意:这种方式只适用于纯代码的情况,关于XIB字体根据屏幕尺寸适配,可以参考这篇文章:iOS xib文件根据屏幕等比例缩放的适配


4.3、处理按钮重复点击

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

这个需求也算是比较常见的需求了,同样的我们先来思考一下有几种做法。

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

这种方法很直观,也很简单。但是工作量大,需要在所有有按钮的地方添加代码。

- (void)viewDidLoad {
    [super viewDidLoad];

    UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    btn.backgroundColor = [UIColor redColor];
    [btn addTarget:self action:@selector(btnFunc:) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:btn];
}

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

- (void)changeBtnStatus:(UIButton *)sender {
    sender.enabled = YES;
}

第二种:利用分类 + Method Swizzling

  1. UIContorlUIButton建立一个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 

@interface UIButton ()

///重复点击间隔
@property (nonatomic, assign) NSTimeInterval xxx_acceptEventInterval;
///上一次点击时间戳
@property (nonatomic, assign) NSTimeInterval xxx_acceptEventTime;

@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 重复点击;同样的在本次分类里面我们使用了关联对象的概念,这里大家可以参考这篇文章关联对象 AssociatedObject 完全解析


4.4、TableView、CollectionView 异常加载占位图

在项目中遇到网络异常,或者其他各种原因造成 ·TableView、CollectionView· 数据为空的时候,通常需要加载占位图显示。那么加载占位图有什么好的方法和技巧吗?

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

这应该是通常的做法。当返回数据,刷新 ·UITableView、CollectionView· 的时候,进行判断,如果数据为空,则加载占位图。如果数据不为空,则移除占位图,显示数据。

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

TableView为例:

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

代码实现:

#import "UITableView+ReloadDataSwizzling.h"
#import 

@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  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);
    UIView *view = [[UIView alloc] initWithFrame:self.bounds];
    self.placeholderView = view;
    
    UIButton *btn = [[UIButton alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
    btn.backgroundColor = [UIColor redColor];
    btn.center = view.center;
    [btn addTarget:self action:@selector(reloadAction) forControlEvents:UIControlEventTouchUpInside];
    [view addSubview:btn];
}

//点击刷洗按钮
- (void)reloadAction {
    self.reloadBlock();
}

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

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

- (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_RETAIN_NONATOMIC);
}

@end

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

  1. 通过 ·Method Swizzling· 替换NSURLConnection, NSURLSession相关的原始是实现(例如NSURLConnection的构造方法和start方法),在实现中加入网络性能埋点行为,然后调用原始实现。从而监听网络。
  2. 防止程序崩溃,可以通过 ·Method Swizzling· 拦截容易造成崩溃的系统方法,然后在替换方法捕获异常类型NSException,再对异常进行处理。最常见的就是拦截arrayWithObjects:count:方法避免数组越界。
  • 一些防止程序崩溃的开源项目:

    • chenfanfang/AvoidCrash
    • ValiantCat/XXShield
  • 一些利用 ·Method Swizzling· 特性进行 APM(应用性能管理)的例子:

    • New Relic:https://newrelic.com
    • 听云 APM:https://www.tingyun.com
    • NetEaseAPM:http://apm.netease.com/
    • ONE APM:https://www.oneapm.com/

总结:

这篇文章作为一篇笔记型的记录存在,之前也阅读过笔者的文章,但是过一段时间之后,发现记忆并不深刻;所以详细的摘抄一遍,在摘抄的过程中,起到了复习的作用。笔者关于 ·Runtime· 的文章确实非常的详细。有时候仅仅是阅读,并不能吃透文章中设计的知识点,花时间手动写一遍,会发现一些新的内容。
在此非常感谢 行走少年郎 对于Runtime知识的整理,帮助我系统性的过了一遍我记忆中的知识点,把之前散乱的知识点系统化了。

你可能感兴趣的:(iOS底层探索 --- Runtime(二)Method Swizzling)