目录
Method Swizzling(动态方法交换)简介
-
什么是 Method Swizzling
在 Objective-C 中,
Method
(方法)对应的是struct objc_method
,而struct objc_method
中包含了:方法名称(SEL method_name
)、方法类型(char * method_types
)、方法实现(IMP method_imp
)// 一个不透明的类型,用于表示 实例对象或者类对象 中的方法 typedef struct objc_method *Method; // 用于描述 实例对象或者类对象 的单个方法 struct objc_method { SEL _Nonnull method_name; // 方法名称 char * _Nullable method_types; // 方法类型 IMP _Nonnull method_imp; // 方法实现 }; // 用于描述 实例对象或者类对象 的方法列表 struct objc_method_list { struct objc_method_list * _Nullable obsolete; // 指向下一个方法列表(已废弃) int method_count; // 本方法列表中方法的总个数 struct objc_method method_list[1]; // 可变长度的结构体数组 };
其中
Method
(方法)、SEL
(方法名称)、IMP
(方法实现)这三者的关系可以这样来表示:在 RunTime 中,Class
(类)维护了一个method list
(方法列表)来确定消息的正确发送。method list
(方法列表)存放的元素就是Method
(方法)。Method
(方法)中映射了一对键值对:SEL
(方法名称):IMP
(方法实现)Method Swizzling 用于改变一个已经存在的方法的实现。在程序运行时,可以通过改变
Method
(方法)中SEL
(方法名称):IMP
(方法实现)的映射关系从而改变方法的调用流程。其本质就是交换两个方法的IMP
(方法实现)Method Swizzling 修改了
method list
(方法列表),使得不同Method
(方法)中的键值对发生了交换。比如,交换前两个Method
(方法)的键值对分别为SEL A : IMP A
、SEL B : IMP B
,交换后两个Method
(方法)的键值对就变成了SEL A : IMP B
、SEL B : IMP A
,如图所示:
-
Method Swizzling 简单使用
ViewController
中有两个方法:-originalDoSomething
和-swizzledDoSomething
。下面的代码交换了两个方法的实现,从而达到:
调用-originalDoSomething
方法实际上调用的是-swizzledDoSomething
方法
调用-swizzledDoSomething
方法实际上调用的是-originalDoSomething
方法#import "ViewController.h" #import <objc/runtime.h> @interface ViewController () @end @implementation ViewController -(void)viewDidLoad { [super viewDidLoad]; [self doMethodSwizzling]; [self originalDoSomething]; [self swizzledDoSomething]; } -(void)doMethodSwizzling { // 获取:当前类对象 Class cls = [self class]; // 获取:原始方法名 和 替换方法名 SEL originalSelector = @selector(originalDoSomething); SEL swizzledSelector = @selector(swizzledDoSomething); // 获取:原始方法结构体 和 替换方法结构体 Method originalMethod = class_getInstanceMethod(cls, originalSelector); Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); // 交换 原始方法 和 替换方法 的实现 method_exchangeImplementations(originalMethod, swizzledMethod); } // 原始方法 -(void)originalDoSomething { NSLog(@"originalDoSomething"); } // 替换方法 -(void)swizzledDoSomething { NSLog(@"swizzledDoSomething"); } @end
从打印结果可以看出两个方法成功进行了交换:
2021-04-17 17:56:47.776516+0800 MethodSwizzlingDemo[6677:740790] swizzledDoSomething 2021-04-17 17:56:47.776786+0800 MethodSwizzlingDemo[6677:740790] originalDoSomething
-
Method Swizzling 相关函数介绍
①
SEL
相关/* 返回一个标识两个 selector 是否相等的布尔值 @param lhs 要与 rhs 进行比较的 selector @param rhs 要与 lhs 进行比较的 selector @return 如果 lhs 与 rhs 相等则返回 YES,否则返回 NO @note 该函数相当于操作符 == */ BOOL sel_isEqual(SEL _Nonnull lhs, SEL _Nonnull rhs); /* 标识一个 selector 为有效或者无效 @param sel 要识别的 selector @return 如果 selector 有效并且具有函数实现则返回 YES,否则返回 NO @warning 在某些平台上,一个无效的引用(指向无效的内存地址)可能会引起崩溃 */ BOOL sel_isMapped(SEL _Nonnull sel); /* 返回由给定的 selector 所指定的方法的名称 @param sel 一个指向 SEL 类型的指针。传递希望确定其名称的 selector @return 一个用于标识 selector 名称的 C 字符串 */ const char * _Nonnull sel_getName(SEL _Nonnull sel); /* 向 Objective-C 运行时系统注册一个方法名称 @param str 一个指向 C 字符串的指针。传递希望注册的方法的名称 @return 一个 SEL 类型的指针,用于标识指定方法名称的 selector @note 该方法的实现与 sel_registerName() 的实现完全相同 @note 在 OSX10.0 版本之前,此方法尝试查找已映射给定名称的 selector,如果找不到相应的 selector 则返回 NULL 为了安全此函数已经做了更改,因为它观察到很多调用者没有检查返回值是否为 NULL */ SEL _Nonnull sel_getUid(const char * _Nonnull str); /* 向 Objective-C 运行时系统注册一个方法名称,将方法名称映射为一个 selector,并且返回 selector 的值 @param str 一个指向 C 字符串的指针。传递希望注册的方法的名称 @return 一个 SEL 类型的指针,用于标识指定方法名称的 selector @note 再将方法添加到类之前,必须向 Objective-C 运行时系统注册一个方法名称,以获取方法的 selector 如果该方法名称已经注册,则该函数简单地返回该方法名称对应的 selector */ SEL _Nonnull sel_registerName(const char * _Nonnull str); // 编译器指令,用于向 Objective-C 运行时系统注册一个方法名称,等效于 sel_registerName() 函数 SEL selector0 = @selector(methodName)
②
Type
相关// 通过编译器指令获取类型编码 char* types0 = @encode(Person); // 通过查编码表直接填入类型编码 char* types1 = "v@:";
③
IMP
相关/* 返回使用 imp_implementationWithBlock() 函数创建的与该 IMP 关联的 block @param anImp 用于调用此 block 的 IMP @return anImp 调用的那个 block */ id _Nullable imp_getBlock(IMP _Nonnull anImp); /* 解除 block 与使用 imp_implementationWithBlock() 函数创建的 IMP 的关联,并释放所创建的 block 的副本 @param anImp 使用 imp_implementationWithBlock() 函数创建的 IMP @return 如果 block 成功释放则返回 YES,否则返回 NO(例如,该 block 之前可能没有用于创建 IMP) */ BOOL imp_removeBlock(IMP _Nonnull anImp); /* 创建一个指向 block 的函数指针 @param block 实现此方法的 block。它的签名应该是:method_return_type ^(id self, method_args...) 不能作为此 block 的参数使用 该 block 将使用 Block_copy() 进行复制 @return 用于调用此 block 的 IMP,必须使用 imp_removeBlock() 函数释放 */ IMP _Nonnull imp_implementationWithBlock(id _Nonnull block); /* 获取指定类中指定方法的实现 @param cls 需要获取方法实现的类 @param name 方法实现对应的 selector @return 方法实现 */ IMP _Nullable class_getMethodImplementation(Class _Nullable cls, SEL _Nonnull name); /* 获取指定类中指定方法的实现 @param cls 需要获取方法实现的类 @param name 方法实现对应的 selector @return 方法实现 */ IMP _Nullable class_getMethodImplementation_stret(Class _Nullable cls, SEL _Nonnull name);
④
Method
相关/* 设置一个方法的实现 @param m 要设置实现的方法 @param imp 要设置到方法的实现 @return 该方法的先前实现 */ IMP _Nonnull method_setImplementation(Method _Nonnull m, IMP _Nonnull imp); /* 返回一个方法的实现 @param m 要返回实现的方法 @return 一个 IMP 类型的函数指针 */ IMP _Nonnull method_getImplementation(Method _Nonnull m); /* 交换两个方法的实现 @param m1 要与 m2 交换实现的方法 @param m2 要与 m1 交换实现的方法 本函数是以下内容的原子版本: IMP imp1 = method_getImplementation(m1); IMP imp2 = method_getImplementation(m2); method_setImplementation(m1, imp2); method_setImplementation(m2, imp1); */ void method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2); /* 返回一个方法的名称 @param m 要返回名称的方法 @return 一个 SEL 类型的指针 @note 如果要获取方法名称的 C 字符串,调用 sel_getName(method_getName(method)) */ SEL _Nonnull method_getName(Method _Nonnull m); /* 返回一个描述方法的参数和返回类型的字符串编码 @param m 要返回描述字符串的方法 @return 一个 C 字符串,该字符串可能为空 */ const char * _Nullable method_getTypeEncoding(Method _Nonnull m); struct objc_method_description * _Nonnull method_getDescription(Method _Nonnull m); /* 返回一个描述方法的返回值类型的字符串编码 @param m 要获取返回值类型的方法 @return 一个用于描述方法返回值类型的 C 字符串。你必须要通过 C 函数 free() 来释放该字符串 */ char * _Nonnull method_copyReturnType(Method _Nonnull m); /* 通过引用来返回描述方法的返回值类型的字符串编码 @param m 要获取返回值类型的方法 @param dst 一个指向方法返回值类型的字符指针 @param dst_len 参数 dst 中可以存储的最大的字符个数 @note 该方法的返回值类型的字符串编码,将被复制到 dst 中。dst 通过 strncpy(dst, parameter_type, dst_len) 函数填充 */ void method_getReturnType(Method _Nonnull m, char * _Nonnull dst, size_t dst_len); /* 通过引用来返回方法中单个参数的类型的字符串编码 @param m 要获取单个参数类型的方法 @param index 要获取类型的参数的索引 @param dst 一个指向参数类型编码的字符指针 @param dst_len 参数 dst 中可以存储的最大的字符个数 @note 参数类型的字符串编码将被复制到 dst 中。dst 通过 strncpy(dst, parameter_type, dst_len) 函数填充 如果该方法不包含具有 index 索引的参数,dst 通过 strncpy(dst, "", dst_len) 函数填充 */ void method_getArgumentType(Method _Nonnull m, unsigned int index, char * _Nullable dst, size_t dst_len); /* 返回一个方法所接受的参数的个数 @param m 要获取参数个数的方法 @return 一个用于表示方法所接受参数个数的整数 */ unsigned int method_getNumberOfArguments(Method _Nonnull m); /* 返回一个用于描述方法中单个参数的类型的字符串编码 @param m 要获取单个参数类型的方法 @param index 要获取类型的参数的索引 @return 一个用于描述 index 处参数类型的 C 字符串,如果方法在 index 处没有参数则为 NULL 你必须要通过 C 函数 free() 来释放该字符串 */ char * _Nullable method_copyArgumentType(Method _Nonnull m, unsigned int index);
Method Swizzling 使用方法(5 种方案)
前面简单演示了在当前类中如何进行 Method Swizzling 操作。但在日常开发中,一般并不是直接在原有类中进行 Method Swizzling 操作。更多的是为当前类添加一个分类,然后在分类中进行 Method Swizzling 操作。另外,真正使用时,会比上面写的考虑的东西多一点,代码要复杂一些
在日常使用 Method Swizzling 的过程中,有 4 种很常用的方案,具体情况如下:
-
① Method Swizzling:在该类的分类中添加 Method Swizzling 交换方法,使用 Objective-C 方法替换
ViewController.m
:#import "ViewController.h" @interface ViewController () @end @implementation ViewController -(void)viewDidLoad { [super viewDidLoad]; [self originalDoSomething]; } // 原始方法 -(void)originalDoSomething{ NSLog(@"originalDoSomething"); } @end
ViewController+UseMethod.m
:#import "ViewController+UseMethod.h" #import <objc/runtime.h> @implementation ViewController (UseMethod) // 交换 原始方法 和 替换方法 的方法实现 +(void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 获取:当前类对象 Class cls = [self class]; // 获取:原始方法名 和 替换方法名 SEL originalSelector = @selector(originalDoSomething); SEL swizzledSelector = @selector(swizzledDoSomething); // 获取:原始方法结构体 和 替换方法结构体 Method originalMethod = class_getInstanceMethod(cls, originalSelector); Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); // 如果当前类中没有 原始方法 的 IMP,则说明 原始方法 是从父类继承过来的 // 为了保证 Method Swizzling 代码结构的清晰,需要在当前类中重建 原始方法 // 并且为了达到方法交换的效果,需要使用 替换方法 的 IMP 重建 原始方法 bool isSuccess = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (isSuccess) { // 如果方法添加成功,则说明 原始方法 是从父类继承过来的。此时,修改 替换方法 的 IMP 为 原始方法 的 IMP class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { // 如果方法添加失败,则说明 原始方法 是当前类本身的。此时,交换 原始方法 与 替换方法 的方法实现 method_exchangeImplementations(originalMethod, swizzledMethod); } }); } // 替换方法 -(void)swizzledDoSomething { NSLog(@"swizzledDoSomething"); // 执行自定义逻辑 // ... // 调用 原始方法 的实现 [self swizzledDoSomething]; } @end
输出结果:
2021-04-17 19:00:03.388650+0800 MethodSwizzlingDemo[6948:764393] swizzledDoSomething 2021-04-17 19:00:03.388833+0800 MethodSwizzlingDemo[6948:764393] originalDoSomething
-
② Method Swizzling:在该类的分类中添加 Method Swizzling 交换方法,使用 C 函数替换
ViewController.m
:#import "ViewController.h" @interface ViewController () @end @implementation ViewController -(void)viewDidLoad { [super viewDidLoad]; [self originalDoSomething]; } // 原始方法 -(void)originalDoSomething { NSLog(@"originalDoSomething"); } @end
ViewController+UseFunction.m
:#import "ViewController+UseFunction.h" #import <objc/runtime.h> @implementation ViewController (UseFunction) // 用于保存 原始方法实现 的函数指针 static void (*originalIMP)(id recevier, SEL selector); // 交换 原始方法 和 替换方法 的方法实现 +(void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 获取:当前类对象 Class cls = [self class]; // 获取:原始方法 SEL originalSelector = @selector(originalDoSomething); Method originalMethod = class_getInstanceMethod(cls, originalSelector); const char * originalTypes = method_getTypeEncoding(originalMethod); originalIMP = (void *)method_getImplementation(originalMethod); // 获取:替换方法 IMP swizzledIMP = (IMP)swizzledFunction; const char * swizzledTypes = "v@:"; // 如果当前类中没有 原始方法 的 IMP,则说明 原始方法 是从父类继承过来的 // 为了保证 Method Swizzling 代码结构的清晰,需要在当前类中重建 原始方法 // 并且为了达到方法交换的效果,需要使用 替换方法 的 IMP 重建 原始方法 bool isSuccess = class_addMethod(cls, originalSelector, swizzledIMP, swizzledTypes); if (isSuccess) { // 如果方法添加成功,则说明 原始方法 是从父类继承过来的。因为前面已经获得 原始方法 的 IMP,所以这里不做任何处理 } else { // 如果方法添加失败,则说明 原始方法 是当前类本身的。此时,替换原始方法的实现。注意:前面已经获得 原始方法 的 IMP class_replaceMethod(cls, originalSelector, swizzledIMP, swizzledTypes); } }); } // 替换函数 static void swizzledFunction(id recevier, SEL selector) { NSLog(@"swizzledFunction"); // 执行自定义逻辑 // ... // 调用 原始方法 的实现 originalIMP(recevier, selector); } @end
输出结果:
2021-04-17 21:41:03.401058+0800 MethodSwizzlingDemo[7918:820638] swizzledFunction 2021-04-17 21:41:03.401297+0800 MethodSwizzlingDemo[7918:820638] originalDoSomething
-
③ Method Swizzling:在其他类中添加 Method Swizzling 交换方法,使用 Objective-C 方法替换
ViewController.m
:#import "ViewController.h" @interface ViewController () @end @implementation ViewController -(void)viewDidLoad { [super viewDidLoad]; [self originalDoSomething]; } // 原始方法 -(void)originalDoSomething { NSLog(@"originalDoSomething"); } @end
SwizzlingToolUseMethod.m
:#import "SwizzlingToolUseMethod.h" #import <objc/runtime.h> @implementation SwizzlingToolUseMethod // 交换 原始方法 和 替换方法 的方法实现 +(void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 获取:ViewController 类对象 Class cls = NSClassFromString(@"ViewController"); // 获取:原始方法名 和 替换方法名 SEL originalSelector = @selector(originalDoSomething); SEL swizzledSelector = @selector(swizzledDoSomething); // 获取:原始方法结构体 和 替换方法结构体 Method originalMethod = class_getInstanceMethod(cls, originalSelector); Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector); // 将 替换方法 添加到 ViewController 类对象中 class_addMethod(cls, swizzledSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); // 获取位于 ViewController 类对象中的 替换方法 swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); // 如果当前类中没有 原始方法 的 IMP,则说明 原始方法 是从父类继承过来的 // 为了保证 Method Swizzling 代码结构的清晰,需要在当前类中重建 原始方法 // 并且为了达到方法交换的效果,需要使用 替换方法 的 IMP 重建 原始方法 bool isSuccess = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (isSuccess) { // 如果方法添加成功,则说明 原始方法 是从父类继承过来的。此时,修改 替换方法 的 IMP 为 原始方法 的 IMP class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { // 如果方法添加失败,则说明 原始方法 是当前类本身的。此时,交换 原始方法 与 替换方法 的方法实现 method_exchangeImplementations(originalMethod, swizzledMethod); } }); } // 替换方法 -(void)swizzledDoSomething { NSLog(@"swizzledDoSomething"); // 执行自定义逻辑 // ... // 调用 原始方法 的实现 [self swizzledDoSomething]; } @end
输出结果:
2021-04-17 21:45:06.845324+0800 MethodSwizzlingDemo[9024:882471] swizzledDoSomething 2021-04-17 21:45:06.845562+0800 MethodSwizzlingDemo[9024:882471] originalDoSomething
-
④ Method Swizzling:在其他类中添加 Method Swizzling 交换方法,使用 C 函数替换
ViewController.m
:#import "ViewController.h" @interface ViewController () @end @implementation ViewController -(void)viewDidLoad { [super viewDidLoad]; [self originalDoSomething]; } // 原始方法 -(void)originalDoSomething { NSLog(@"originalDoSomething"); } @end
SwizzlingToolUseFunction.m
:#import "SwizzlingToolUseFunction.h" #import <objc/runtime.h> @implementation SwizzlingToolUseFunction // 用于保存 原始方法实现 的函数指针 static void (*originalIMP)(id recevier, SEL selector); // 交换 原始方法 和 替换方法 的方法实现 +(void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 获取:ViewController 类对象 Class cls = NSClassFromString(@"ViewController"); // 获取:原始方法 SEL originalSelector = @selector(originalDoSomething); Method originalMethod = class_getInstanceMethod(cls, originalSelector); const char * originalTypes = method_getTypeEncoding(originalMethod); originalIMP = (void *)method_getImplementation(originalMethod); // 获取:替换方法 IMP swizzledIMP = (IMP)swizzledFunction; const char * swizzledTypes = "v@:"; // 如果当前类中没有 原始方法 的 IMP,则说明 原始方法 是从父类继承过来的 // 为了保证 Method Swizzling 代码结构的清晰,需要在当前类中重建 原始方法 // 并且为了达到方法交换的效果,需要使用 替换方法 的 IMP 重建 原始方法 bool isSuccess = class_addMethod(cls, originalSelector, swizzledIMP, swizzledTypes); if (isSuccess) { // 如果方法添加成功,则说明 原始方法 是从父类继承过来的。因为前面已经获得 原始方法 的 IMP,所以这里不做任何处理 } else { // 如果方法添加失败,则说明 原始方法 是当前类本身的。此时,替换原始方法的实现。注意:前面已经获得 原始方法 的 IMP class_replaceMethod(cls, originalSelector, swizzledIMP, swizzledTypes); } }); } // 替换函数 static void swizzledFunction(id recevier, SEL selector) { NSLog(@"swizzledFunction"); // 执行自定义逻辑 // ... // 调用 原始方法 的实现 originalIMP(recevier, selector); } @end
输出结果:
2021-04-17 21:55:01.409893+0800 MethodSwizzlingDemo[9144:893089] swizzledFunction 2021-04-17 21:55:01.410061+0800 MethodSwizzlingDemo[9144:893089] originalDoSomething
-
⑤ Method Swizzling:使用优秀的第三方框架
JRSwizzle 和 RSSwizzle 都是封装了 Method Swizzling 的优秀的第三方框架
- JRSwizzle 尝试解决在不同平台和系统版本上的 Method Swizzling 与类继承关系的冲突。对各平台低版本系统兼容性较强。JRSwizzle 核心是用到了
method_exchangeImplementations
函数。在健壮性上先做了class_addMethod
操作 - RSSwizzle 主要用到了
class_replaceMethod
方法,避免了子类的替换影响到父类。而且对交换方法过程加了锁,增强了线程安全。它用很复杂的方式解决了 What are the dangers of method swizzling in Objective-C? 中提到的问题。是一种更安全优雅的 Method Swizzling 解决方案
- JRSwizzle 尝试解决在不同平台和系统版本上的 Method Swizzling 与类继承关系的冲突。对各平台低版本系统兼容性较强。JRSwizzle 核心是用到了
Method Swizzling 使用注意
Method Swizzling 之所以被称为 RunTime 中的黑魔法,就是因为使用 Method Swizzling 进行方法交换是一个危险的操作。Stack Overflow 上,有人提出了使用 Method Swizzling 会造成的一些危险和缺陷,更是把 Method Swizzling 比作是厨房里一把锋利的刀:有些人会害怕刀过于锋利,会伤到自己,从而放弃了锋利的刀,或者使用了钝刀。但是事实却是:锋利的刀比钝刀反而更加安全,前提是你有足够的经验(Stack Overflow 相关问题链接:What are the dangers of method swizzling in Objective-C ?)
Method Swizzling 可用于编写更好、更高效、更易维护的代码。但也可能因为滥用而导致可怕的错误。所以在使用 Method Swizzling 的时候,需要注意一些事项,以规避可能出现的危险。下面我们结合其他博主关于 Method Swizzling 的博文、 以及 Stack Overflow 上边提到的危险和缺陷、还有笔者的个人见解,来综合说明一下使用 Method Swizzling 需要注意的地方
-
① 应该只在 +load 中执行 Method Swizzling
程序在启动的时候,会先加载所有的类,这时会调用每个类的
+load
方法。而且在整个程序运行周期只会调用一次(不包括外部显示调用)。所以在+load
方法中进行 Method Swizzling 再好不过了那为什么不用
+initialize
方法呢?因为+initialize
方法的调用时机是:第一次向该类发送消息的时候。如果该类只是被引用,没有被调用,则不会执行+initialize
方法Method Swizzling 影响的是全局状态,
+load
方法能保证在加载类的时候就进行交换,保证交换结果。而使用+initialize
方法则不能保证这一点,有可能在使用的时候起不到交换方法的作用 -
② Method Swizzling 在 +load 中执行时,不要调用 [super load]
在第 ① 点中我们说了:程序在启动的时候,会先加载所有的类。如果在
+load
方法中调用[super load]
方法,则会导致父类的 Method Swizzling 被重复执行两次,即方法交换被执行了两次,相当于互换了一次方法实现之后,第二次又换回去了,从而使得父类的 Method Swizzling 失效 -
③ Method Swizzling 应该总是在 dispatch_once 中执行
Method Swizzling 不是原子操作,
dispatch_once
可以确保即使在不同的线程中也能保证 Method Swizzling 代码只执行一次。所以,我们应该总是在dispatch_once
中执行 Method Swizzling 操作,保证方法交换只被执行一次 -
④ 使用 Method Swizzling 后,记得要在 替换方法 中调用 原始方法 的实现
使用 Method Swizzling 交换方法实现后,记得要在(替换方法)中调用(原始方法)的实现(除非你非常确定可以不用调用原始方法的实现)。API 提供了输入输出的规则,而在输入输出中间的方法实现是一个看不见的黑盒,我们很难知道在原始方法的实现中都调用了哪些函数,执行了哪些回调。如果不在(替换方法)中调用(原始方法)的实现,则可能会造成底层实现的崩溃
例如,你在一个类中重写一个方法,并且不调用
super
方法,则可能会出现问题。在大多数情况下,super
方法是期望被调用的(除非有特殊说明)。如果你是用同样的思想来进行 Method Swizzling,可能就会引起很多问题。如果你不调用原始的方法实现,那么你 Method Swizzling 改变的越多,代码就越不安全 -
⑤ 避免 替换方法 和 原始方法 命名冲突
避免方法命名冲突一个比较好的做法是为(替换方法)加个前缀以区别(原始方法)
避免方法命名冲突另一个更好的做法是替换方法使用函数指针(上面的 方案② 和 方案④)
-
⑥ 对于 Method Swizzling 来说,+load 方法的调用顺序很重要
+load
方法的调用规则为:- 先调用主类,按照编译顺序,顺序地根据继承关系由父类向子类调用
- 再调用分类,按照编译顺序,依次调用
- 除非主动调用,否则只会调用一次
这样的调用规则导致了
+load
方法的调用顺序并不一定确定。一个可能的顺序是:父类 -> 子类 -> 父类类别 -> 子类类别,也可能是:父类 -> 子类 -> 子类类别 -> 父类类别。如果 Method Swizzling 的顺序不能保证,那么就不能保证 Method Swizzling 后方法的调用顺序是正确的所以被用于 Method Swizzling 的方法必须是当前类自身的方法,如果把父类继承过来的
IMP
复制到自身上面可能会存在问题。如果+load
方法调用顺序为:父类 -> 子类 -> 父类类别 -> 子类类别,那么造成的影响就是调用子类的替换方法并不能正确调起父类分类的替换方法 -
⑦ 谨慎对待 Method Swizzling
一定要确保调用了(原始方法)的所有地方不会因为 Method Swizzling 交换了方法的实现而出现意料不到的结果
使用 Method Swizzling,会改变非自己拥有的代码,通常是更改一些系统框架的对象方法、类方法。Method Swizzling 改变的不只是一个对象实例,而是改变了项目中所有的该类的对象实例,以及所有子类的对象实例。所以,在使用 Method Swizzling 的时候,应该保持足够的谨慎
Method Swizzling 应用场景:全局页面统计功能
-
需求
在所有页面添加统计功能,用户每进入一次页面就统计一次
-
一般的实现方式
① 手动添加:直接在所有的控制器添加一次统计代码。你需要做的就是写一份统计代码,然后在所有控制器的
viewWillAppear:
中不断地进行复制、粘贴② 利用继承:创建一个控制器的基类,所有控制器都继承自该基类。这样的话只需要在基类的
viewWillAppear:
中添加一次统计功能。这样修改代码还是很多,如果所有控制器不是一开始就继承自该基类,那么就需要修改所有控制器的继承关系,同样会造成很多重复代码,和极大的工作量 -
利用 Method Swizzling + 分类
步骤如下:
- 为控制器添加一个分类,并在分类中实现一个自定义的
hcg_viewWillAppear:
方法 - 利用
Method Swizzling
将控制器的viewWillAppear:
和自定义的hcg_viewWillAppear:
进行方法交换 - 然后在
hcg_viewWillAppear:
中添加统计代码,并调用hcg_viewWillAppear:
(因为两个方法发生了交换,所以实际上是调用了控制器的viewWillAppear:
方法)
代码实现:
#import "UIViewController+StatisticsSwizzling.h" #import <objc/runtime.h> @implementation UIViewController (StatisticsSwizzling) +(void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 获取:当前类对象 Class cls = [self class]; // 获取:原始方法名 和 替换方法名 SEL originalSelector = @selector(viewWillAppear:); SEL swizzledSelector = @selector(hcg_viewWillAppear:); // 获取:原始方法结构体 和 替换方法结构体 Method originalMethod = class_getInstanceMethod(cls, originalSelector); Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); // 如果当前类中没有 原始方法 的 IMP,则说明 原始方法 是从父类继承过来的 // 为了保证 Method Swizzling 代码结构的清晰,需要在当前类中重建 原始方法 // 并且为了达到方法交换的效果,需要使用 替换方法 的 IMP 重建 原始方法 bool isSuccess = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (isSuccess) { // 如果方法添加成功,则说明 原始方法 是从父类继承过来的。此时,修改 替换方法 的 IMP 为 原始方法 的 IMP class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { // 如果方法添加失败,则说明 原始方法 是当前类本身的。此时,交换 原始方法 与 替换方法 的方法实现 method_exchangeImplementations(originalMethod, swizzledMethod); } }); } // 页面统计功能 -(void)hcg_viewWillAppear:(BOOL)animated { // 去除 UIViewController UINavigationController UITabBarController 等系统根控制器相关的统计影响 // 这个看需求,去除是因为一般项目中直接使用系统根控制器的场景不多 if (![self isMemberOfClass:[UIViewController class]] || ![self isMemberOfClass:[UINavigationController class]] || ![self isMemberOfClass:[UITabBarController class]]) { // 添加页面统计代码 NSLog(@"进入页面 : %@", [self class]); } // 调用 原始方法 的实现 [self hcg_viewWillAppear:animated]; } @end
- 为控制器添加一个分类,并在分类中实现一个自定义的
Method Swizzling 应用场景:字体根据屏幕尺寸适配
-
需求
所有控件的字体必须依据屏幕的尺寸等比缩放
-
一般的实现方式
① 手动修改:所有用到
UIFont
的地方,手动判断,添加适配代码。一想到那个工作量,不忍直视② 利用 PCH 文件:在 PCH 文件中定义一个计算缩放字体的方法。在设置字体时,先调用 PCH 文件中缩放字体的方法。但是这样同样需要修改所有用到
UIFont
的地方。工作量依旧很大static const CGFloat kStandardScreenWidth = 375.0f; // 标准屏幕尺寸 static const CGFloat kFontSizeScale = 1.0f; // 缩放比例 // 计算缩放字体的方法 static inline CGFloat calculateFontSize(CGFloat fontSize){ CGFloat currentScreenWidth = [UIScreen mainScreen].bounds.size.width; return fontSize * currentScreenWidth / kStandardScreenWidth * kFontSizeScale; }
-
利用 Method Swizzling + 分类
步骤如下:
- 为
UIFont
建立一个分类 - 在分类中实现一个自定义的
hcg_systemFontOfSize:
方法,在其中添加缩放字体的代码 - 利用 Method Swizzling 将
UIFont
的systemFontOfSize:
和分类的hcg_systemFontOfSize:
进行方法交换 - 注意,这种方式只适用于纯代码的情况,关于 XIB 字体根据屏幕尺寸适配,可以参考这篇博文(小生不怕:iOS xib 文件根据屏幕等比例缩放的适配)
代码实现:
#import "UIFont+ScaleSwizzling.h" #import <objc/runtime.h> static const CGFloat kStandarScreenWidth = 375.0f; static const CGFloat kFontSizeScale = 1.0f; @implementation UIFont (ScaleSwizzling) +(void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 获取:当前元类对象(类方法存储在元类对象中,通过 object_getClass 获取 self.isa 所指向的元类) Class cls = object_getClass(self); // 获取:原始方法名 和 替换方法名 SEL originalSelector = @selector(systemFontOfSize:); SEL swizzledSelector = @selector(hcg_systemFontOfSize:); // 获取:原始方法结构体 和 替换方法结构体 Method originalMethod = class_getInstanceMethod(cls, originalSelector); Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); // 如果当前类中没有 原始方法 的 IMP,则说明 原始方法 是从父类继承过来的 // 为了保证 Method Swizzling 代码结构的清晰,需要在当前类中重建 原始方法 // 并且为了达到方法交换的效果,需要使用 替换方法 的 IMP 重建 原始方法 bool isSuccess = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (isSuccess) { // 如果方法添加成功,则说明 原始方法 是从父类继承过来的。此时,修改 替换方法 的 IMP 为 原始方法 的 IMP class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { // 如果方法添加失败,则说明 原始方法 是当前类本身的。此时,交换 原始方法 与 替换方法 的方法实现 method_exchangeImplementations(originalMethod, swizzledMethod); } }); } +(UIFont *)hcg_systemFontOfSize:(CGFloat)fontSize { CGFloat currentScreenWidth = [UIScreen mainScreen].bounds.size.width; CGFloat fontSizeAfterSacle = fontSize * currentScreenWidth / kStandarScreenWidth * kFontSizeScale; return [UIFont hcg_systemFontOfSize:fontSizeAfterSacle]; } @end
- 为
Method Swizzling 应用场景:处理按钮重复点击
-
需求
避免一个按钮被快速多次点击
-
一般的实现方式
① 利用 Delay 延迟,和不可点击方法。这种方法很直观,也很简单。但就是工作量很大,需要在所有有按钮的地方添加代码
#import "ViewController.h" @interface ViewController () @end @implementation ViewController -(void)viewDidLoad { [super viewDidLoad]; [self setupSubview]; } -(void)setupSubview { CGRect btnFrame = CGRectMake(100, 100, 100, 100); UIButton* btn = [[UIButton alloc] initWithFrame:btnFrame]; btn.backgroundColor = [UIColor orangeColor]; [btn addTarget:self action:@selector(btnDidClick:) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:btn]; } -(void)btnDidClick:(UIButton *)sender { static const NSTimeInterval kClickInterval = 1.0f; sender.enabled = NO; [self performSelector:@selector(setButtonToEnabled:) withObject:sender afterDelay:kClickInterval]; NSLog(@"%s", __func__); } -(void)setButtonToEnabled:(UIButton *)sender { sender.enabled = YES; } @end
-
利用 Method Swizzling + 分类
步骤如下:
- 为
UIControl
或UIButton
建立一个分类 - 在分类中实现一个自定义的
hcg_sendAction:to:forEvent:
方法,在其中添加限定点击间隔的相应代码 - 利用 Method Swizzling 将
UIButton
的sendAction:to:forEvent:
和分类的hcg_sendAction:to:forEvent:
进行方法交换
代码实现:
#import "UIButton+ClickIntervalSwizzling.h" #import <objc/runtime.h> static const char * kClickIntervalKey = "UIButton+ClickIntervalSwizzling.hcg_clickInterval"; static const char * kLastClickTimeKey = "UIButton+ClickIntervalSwizzling.hcg_lastClickTime"; static const NSTimeInterval kDefaultInterval = 1.0f; @interface UIButton (ClickIntervalSwizzling) @property (nonatomic, assign) NSTimeInterval hcg_clickInterval; // 点击的时间间隔 @property (nonatomic, assign) NSTimeInterval hcg_lastClickTime; // 上一次点击时间 @end @implementation UIButton (ClickIntervalSwizzling) +(void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 获取:当前类对象 Class cls = [self class]; // 获取:原始方法名 和 替换方法名 SEL originalSelector = @selector(sendAction:to:forEvent:); SEL swizzledSelector = @selector(hcg_sendAction:to:forEvent:); // 获取:原始方法结构体 和 替换方法结构体 Method originalMethod = class_getInstanceMethod(cls, originalSelector); Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); // 如果当前类中没有 原始方法 的 IMP,则说明 原始方法 是从父类继承过来的 // 为了保证 Method Swizzling 代码结构的清晰,需要在当前类中重建 原始方法 // 并且为了达到方法交换的效果,需要使用 替换方法 的 IMP 重建 原始方法 bool isSuccess = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (isSuccess) { // 如果方法添加成功,则说明 原始方法 是从父类继承过来的。此时,修改 替换方法 的 IMP 为 原始方法 的 IMP class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { // 如果方法添加失败,则说明 原始方法 是当前类本身的。此时,交换 原始方法 与 替换方法 的方法实现 method_exchangeImplementations(originalMethod, swizzledMethod); } }); } -(void)hcg_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event { // 判断是否需要发送点击事件 NSTimeInterval currentClickTime = NSDate.date.timeIntervalSince1970; bool needSendAction = (currentClickTime - self.hcg_lastClickTime >= self.hcg_clickInterval); if (needSendAction) { self.hcg_lastClickTime = currentClickTime; [self hcg_sendAction:action to:target forEvent:event];; } } #pragma mark - Association Object -(NSTimeInterval)hcg_clickInterval { // 设置默认时间间隔 NSTimeInterval interval = [objc_getAssociatedObject(self, &kClickIntervalKey) doubleValue]; if (interval <= 0) { interval = kDefaultInterval; [self setHcg_clickInterval:interval]; } return interval; } -(void)setHcg_clickInterval:(NSTimeInterval)hcg_clickInterval { // 设置默认时间间隔 if (hcg_clickInterval <= 0) { hcg_clickInterval = kDefaultInterval; } objc_setAssociatedObject(self, &kClickIntervalKey, @(hcg_clickInterval), OBJC_ASSOCIATION_ASSIGN); } -(NSTimeInterval)hcg_lastClickTime { return [objc_getAssociatedObject(self, &kLastClickTimeKey) doubleValue]; } -(void)setHcg_lastClickTime:(NSTimeInterval)hcg_lastClickTime { objc_setAssociatedObject(self, &kLastClickTimeKey, @(hcg_lastClickTime), OBJC_ASSOCIATION_ASSIGN); } @end
- 为
Method Swizzling 应用场景:TableView、CollectionView 异常加载占位图
-
需求
在项目中遇到网络异常,或者其他各种原因造成
TableView
、CollectionView
数据为空的时候,通常需要加载占位图显示。那么加载占位图有没有什么好的方法或技巧? -
一般的实现方式
① 刷新数据后进行判断:这应该是通常的做法。当返回数据,刷新
TableView
、CollectionView
的时候,进行判断,如果数据为空,则显示占位图;如果数据不为空,则隐藏占位图,显示数据 -
利用 Method Swizzling + 分类
步骤如下(以
TableView
为例):- 为
TableView
建立一个分类 - 在分类中实现一个自定义的
hcg_reloadData
方法,在其中添加判断数据源是否为空,以及显示占位图、隐藏占位图的相关代码 - 利用 Method Swizzling 将
TableView
的reloadData
和分类的xxx_reloadData
进行方法交换
代码实现:
#import <UIKit/UIKit.h> NS_ASSUME_NONNULL_BEGIN @interface UITableView (ReloadDataSwizzling) @property (nonatomic, assign) bool hcg_firstReload; @property (nonatomic, strong) UIView* hcg_placeholderView; @end NS_ASSUME_NONNULL_END
#import "UITableView+ReloadDataSwizzling.h" #import "HCGPlaceholderView.h" #import <objc/runtime.h> static const char * kFirstReloadKey = "UITableView+ReloadDataSwizzling.firstReload"; static const char * kPlaceholderViewKey = "UITableView+ReloadDataSwizzling.placeholderView"; @implementation UITableView (ReloadDataSwizzling) +(void)load { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 获取:当前类对象 Class cls = [self class]; // 获取:原始方法名 和 替换方法名 SEL originalSelector = @selector(reloadData); SEL swizzledSelector = @selector(hcg_reloadData); // 获取:原始方法结构体 和 替换方法结构体 Method originalMethod = class_getInstanceMethod(cls, originalSelector); Method swizzledMethod = class_getInstanceMethod(cls, swizzledSelector); // 如果当前类中没有 原始方法 的 IMP,则说明 原始方法 是从父类继承过来的 // 为了保证 Method Swizzling 代码结构的清晰,需要在当前类中重建 原始方法 // 并且为了达到方法交换的效果,需要使用 替换方法 的 IMP 重建 原始方法 bool isSuccess = class_addMethod(cls, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (isSuccess) { // 如果方法添加成功,则说明 原始方法 是从父类继承过来的。此时,修改 替换方法 的 IMP 为 原始方法 的 IMP class_replaceMethod(cls, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { // 如果方法添加失败,则说明 原始方法 是当前类本身的。此时,交换 原始方法 与 替换方法 的方法实现 method_exchangeImplementations(originalMethod, swizzledMethod); } }); } -(void)hcg_reloadData { if (!self.hcg_firstReload) { [self checkDataSourceEmpty]; } self.hcg_firstReload = NO; [self hcg_reloadData]; } #pragma mark - Helper -(void)checkDataSourceEmpty { // 通过获取 DataSource 的 Section 和 Row,判断 TableView 的 DataSource 是否为空 bool isEmpty = YES; id<UITableViewDataSource> dataSource = self.dataSource; if ([dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)]) { NSInteger sectionNum = [dataSource numberOfSectionsInTableView:self]; if ([dataSource respondsToSelector:@selector(tableView:numberOfRowsInSection:)]) { for (NSInteger i = 0; i <= sectionNum - 1; i++) { NSInteger rowNum = [dataSource tableView:self numberOfRowsInSection:i]; if (0 != rowNum) { isEmpty = NO; } } } } // 若 DataSource 为空则加载占位图;若 DataSource 不为空则隐藏占位图 if (isEmpty) { self.hcg_placeholderView.hidden = NO; } else { self.hcg_placeholderView.hidden = YES; } } -(UIView *)makeDefaultPlaceholderView { HCGPlaceholderView* placeholderView = [[HCGPlaceholderView alloc] initWithFrame:self.bounds]; // 设置 placeholderView // ... return placeholderView; } #pragma mark - Association Object -(bool)hcg_firstReload { return [objc_getAssociatedObject(self, &kFirstReloadKey) boolValue]; } -(void)setHcg_firstReload:(bool)hcg_firstReload { objc_setAssociatedObject(self, &kFirstReloadKey, @(hcg_firstReload), OBJC_ASSOCIATION_ASSIGN); } -(UIView *)hcg_placeholderView { UIView* placeholderView = objc_getAssociatedObject(self, &kPlaceholderViewKey); if (!placeholderView) { placeholderView = [self makeDefaultPlaceholderView]; [self setHcg_placeholderView:placeholderView]; } return placeholderView; } -(void)setHcg_placeholderView:(UIView *)hcg_placeholderView { UIView* placeholderView = objc_getAssociatedObject(self, &kPlaceholderViewKey); if (placeholderView != hcg_placeholderView) { [placeholderView removeFromSuperview]; [self addSubview:hcg_placeholderView]; objc_setAssociatedObject(self, &kPlaceholderViewKey, hcg_placeholderView, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } } @end
- 为
Method Swizzling 应用场景:应用性能管理(APM)、防止程序崩溃
-
一些利用 Method Swizzling 特性进行应用性能管理(Application Performance Management)的例子:
① New Relic
② 听云 APM
③ NetEaseAPM
④ ONE APM -
防止程序崩溃的开源项目:
① GitHub:chenfanfang / AvoidCrash
② GitHub:ValiantCat / XXShield -
应用举例:
① 通过 Method Swizzling 替换
NSURLConnection
、NSURLSession
相关的原始实现(例如NSURLConnection
的构造方法和start
方法),在实现中加入网络性能埋点行为,然后调用原始实现,从而达到监控网络的目的② 防止程序崩溃,可以通过 Method Swizzling 拦截容易造成崩溃的系统方法,然后在替换方法捕获异常类型
NSException
,再对异常进行处理。最常见的例子就是拦截arrayWithObjects:count:
方法避免数组越界,这种例子网上很多,就不再展示代码了