本文用来介绍ios开发中【Runtime】中的黑魔法Method Swizzing。通过本文,您将了解到:
1.Method Swizzling(动态方法交换)简介
2.Method Swizzling 使用方法(四种方案)
3.Method Swizzling 使用注意
4.Method Swizzling 应用场景
4.1全局页面统计功能
4.2字体根据屏幕尺寸适配
4.3处理按钮重复点击
4.4TableView、CollectionView 异常加载占位图
4.5APM(应用性能管理)、防止崩溃
我们之前了解了运行机制(Runtime系统)的工作原理,包括消息发送以及转发机制的原理和流程。
从这一篇文章开始,我们来了解一下Runtime在实际开发过程中,具体的应用场景。
这一篇我们来学习一下被称为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 (方法实现)。
// objc_method 结构体
typedef 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(方法实现)。
Method swizzling 修改了method list(方法列表),使得不同Method(方法)中的键值对发生了交换。比如交换前两个键值对分别为SEL A : IMP A、SEL B : IMP B,交换之后就变为了SEL A : IMP B、SEL B :IMP A.如图所示:
2.Method Swizzling 使用方法
假如当前类中有两个方法:- (void)originalFunction;和- (void)swizzledFunction;.如果我们想要交换两个方法的实现,从而实现调用- (void)originalFunction;方法实际上调用的是- (void)swizzledFunction;方法,而调用B方法实际上是调用A方法的效果。那我们需要像下边代码一样来实现。
2.1 Method Swizzling 简单使用
在当前类的 + (void)load;方法中增加Method Swizzling 操作,交换 - (void)A 和- (void)B的方法实现。
#import "ViewController.h"
#import <objc/runtime.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self SwizzlingMethod];
[self A];
[self B];
}
// 交换 原方法 和 替换方法 的方法实现
- (void)SwizzlingMethod {
Class class = [self class];
SEL A = @selector(A);
SEL B = @selector(B);
// 原方法结构体 和替换方法结构体
Method A = class_getInstanceMethod(class, A);
Method B = class_getInstanceMethod(class, B);
// 调用交换两个方法的实现
method_exchangeImplementations(A, B);
}
// 原始方法
- (void)A {
NSLog(@"A");
}
- (void)B {
NSLog(@"B");
}
/*打印结果:
09:59:19.672349+0800 Runtime-MethodSwizzling[91009:30112833] B
09:59:20.414930+0800 Runtime-MethodSwizzling[91009:30112833] A
*/
刚才我们简单演示了如何当前类中如何进行Method Swizzling 操作。但一般日常开发中,并不是直接在原有类中进行Method Swizzling 操作。更多的是为当前类添加一个分类,然后在分类中进行Method Swizzling操作。另外真正使用会比上边写的考虑东西要多一点,要复杂一些。
在日常使用Method Swizzling 的过程中,有几种很常用的方案,具体情况如下。
2.3 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 ASelector = @selector(A);
SEL BSelector = @selector(B);
//原方法结构体 和 替换方法结构体
Method A = class_getInstanceMethod(class, ASelector);
Method B = class_getInstanceMethod(class, BSelector);
/*
如果当前类没有原方法的IMP。说明从父类继承过来的方法实现,
需要在当前类中添加一个 ASelector方法,
但是用 替换方法 B 去实现它
*/
BOOL didAddMethod = class_addMethod(class, ASelector, method_getImplementation(B),
method_getTypeEncoding(B));
if (didAddMethod) {
//. 原方法的 IMP 添加成功后, 修改 替换方法的 IMP 为原始方法的 IMP
class_replaceMethod(class, BSelector, method_getImplementation(A),
method_getTypeEncoding(A));
else {
// 添加失败 (说明已包含原方法的 IMP),调用交换两个方法的实现
method_exchangeImplementations(A, B);
}
}
});
}
// 原始方法
- (void)A {
NSLog(@"A");
}
- (void)B {
NSLog(@"B");
}
@end
2.2 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版本基本一致。
tatic 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)