本文为L_Ares个人写作,以任何形式转载请表明原文出处。
Method_Swizzling
是iOS
开发者常见的一种方法,那么关于Method_Swizzling
到底是什么,有一些什么坑在里面,本节将会通过自己的视角来阐述。
首先,虽然很熟悉Method_Swizzling
了,但是还是要系统的介绍一下。
一、Method_Swizzling是什么
Method_Swizzling
是一种在运行时,将方法编号(sel)
对应的方法实现(imp)
进行交换的手段。
通俗的说,方法在类中是以method_list_t
的形式存储着的,也就是之前在说类的结构中的class_ro_t* ro
中的baseMethodList
对象中存储。而method_list_t
中存储的方法是以method_t
结构体结构存储的方法,而method_t
拥有着方法编号(sel) --- 方法实现(imp)
属性,Method_Swizzling
就是要将方法编号(sel)
对应的方法实现(imp)
进行交换。
如下图 :
method_swizzling.png二、Method_Swizzling要用到的API
即然是方法交换,那么必然就需要一些和类、方法、实现相关的API
。
1. 通过sel获取Method
-
获取实例方法 :
class_getInstanceMethod(Class _Nullable cls, SEL _Nonnull name)
-
cls
: 要获取哪个类的实例方法 -
name
: 方法编号的名称
-
-
获取类方法 :
class_getClassMethod(Class _Nullable cls, SEL _Nonnull name)
-
cls
: 要获取哪个类的类方法 -
name
: 方法编号的名称
-
2. 方法的实现Imp
- 获取一个方法的实现 :
method_getImplementation(Method _Nonnull m)
-
m
: 哪个方法的IMP
实现
-
- 设置一个方法的实现 :
method_setImplementation(Method _Nonnull m, IMP _Nonnull imp)
-
m
: 要给哪个方法设置实现。 -
imp
: 实现IMP
-
3. 编码类型获取
- 获取一个方法的编码类型 :
method_getTypeEncoding(Method _Nonnull m)
-
m
: 要获取编码类型的方法。
-
4. 方法相关
-
添加一个方法 :
class_addMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
-
cls
: 给哪个类添加方法 -
name
: 指定要添加的方法名称的选择器 -
imp
: 一个新方法的实现函数。该函数必须使用至少两个参数—self和_cmd。 -
types
: 描述方法参数类型的字符数组。
-
-
替换给定类的方法实现 :
class_replaceMethod(Class _Nullable cls, SEL _Nonnull name, IMP _Nonnull imp, const char * _Nullable types)
-
cls
: 你想要修改的类。 -
name
: 你想要替换方法实现的方法的方法编号。 -
imp
: 你想要给上面的name
修改成为的方法实现。 -
types
: 描述方法参数类型的字符数组。
-
-
交换两个方法的实现 :
method_exchangeImplementations(Method _Nonnull m1, Method _Nonnull m2)
-
m1
和m2
: 你想要交换哪两个方法的方法实现。
-
三、Method_Swizzling中可能存在的一些问题
准备 : 随意创建一个
iOS
的Project
--->App
,然后定义两个类。继承于NSObject
的JDPerson
类,和继承与JDPerson
的JDStudent
类。类名自拟。创建NSArray
的分类NSArray+JD
。
1. 数组越界和类蔟
什么意思呢,就是说在进行Method_Swizzling
的时候,我们是可以进行数组越界的一个处理的,防止进入越界的crash
中。但是数组是类蔟,所以有一些特殊。
举个例子 :
我们会利用NSArray
的- (ObjectType)objectAtIndex:(NSUInteger)index;
方法获取数组中index
对应的元素。但是一旦index
的数字大于array.count - 1
,就会造成越界,程序就会进入crash
流程。所以我们可以对它进行相应的处理,让越界不进入crash
流程。
先在NSArray
的分类NSArray+JD.m
中实现如下交换代码(错误示范
) :
#import "NSArray+JD.h"
#import <objc/runtime.h>
@implementation NSArray (JD)
+ (void)load
{
//获取NSArray的 objectAtIndex: 的Method
Method originalMethod = class_getInstanceMethod([self class], @selector(objectAtIndex:));
//获取下面自定义的,用来替换objectAtIndex实现的Method
Method swizzlingMethod = class_getInstanceMethod([self class], @selector(jd_objectAtIndex:));
//交换两个方法的实现
method_exchangeImplementations(originalMethod, swizzlingMethod);
}
- (id)jd_objectAtIndex:(NSUInteger)index
{
//判断如果index比数组拥有的元素数量还多
if (self.count - 1 < index) {
//你可以进行其他的处理,这里作为举例,只返回nil,只进行NSLog,方便查看控制台
NSLog(@"数组越界了");
return nil;
}
//这里再调用jd_objectAtIndex就已经在+(void)load中就被换成了objectAtIndex的实现
return [self jd_objectAtIndex:index];
}
@end
然后,在ViewController
里面随意定义一个数组属性,进行数组初始化,然后打印超过数组元素数量的index
的值。代码如下 :
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) NSArray *tempArray;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.tempArray = @[@"name",@"sex",@"age",@"work"];
NSLog(@"%@",[self.tempArray objectAtIndex:4]);
// Do any additional setup after loading the view.
}
@end
但是这么运行起来,依然是会报错数组越界的。如下图 :
图3.1.0.png原因 : 数组是一个类蔟,获取NSArray
的objectAtIndex
方法的类应该是__NSArrayI
类蔟 :
类蔟是一种设计模式,类蔟中的类利用相同的接口却可以有不同的实现。
具体的介绍大家可以直接进入这里。直接把一些常见的类蔟写一下。
类名 | 实际名称 |
---|---|
NSArray | __NSArrayI |
NSMutableArray | __NSArrayM |
NSDictionary | __NSDictionaryI |
NSMutableDictionary | __NSDictionaryM |
正确的Method_Swizzling :
#import "NSArray+JD.h"
#import <objc/runtime.h>
@implementation NSArray (JD)
+ (void)load
{
//获取NSArray的 objectAtIndex: 的Method
// Method originalMethod = class_getInstanceMethod([self class], @selector(objectAtIndex:));
Method originalMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
//获取下面自定义的,用来替换objectAtIndex实现的Method
// Method swizzlingMethod = class_getInstanceMethod([self class], @selector(jd_objectAtIndex:));
Method swizzlingMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(jd_objectAtIndex:));
//交换两个方法的实现
method_exchangeImplementations(originalMethod, swizzlingMethod);
}
- (id)jd_objectAtIndex:(NSUInteger)index
{
//判断如果index比数组拥有的元素数量还多
if (index > (self.count - 1)) {
//你可以进行其他的处理,这里作为举例,只返回nil,只进行NSLog,方便查看控制台
NSLog(@"数组越界了");
return nil;
}
//这里再调用jd_objectAtIndex就已经在+(void)load中就被换成了objectAtIndex的实现
return [self jd_objectAtIndex:index];
}
@end
执行结果 :
图3.1.2.png当然,最常用的数组取值还是以self.tempArray[4]
这种居多,所以可以再在load
中添加objectAtIndexedSubscript
的Method_Swizzling
。
+ (void)load
{
//获取NSArray的 objectAtIndex: 的Method
// Method originalMethod = class_getInstanceMethod([self class], @selector(objectAtIndex:));
Method originalMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
//获取下面自定义的,用来替换objectAtIndex实现的Method
// Method swizzlingMethod = class_getInstanceMethod([self class], @selector(jd_objectAtIndex:));
Method swizzlingMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(jd_objectAtIndex:));
//交换两个方法的实现
method_exchangeImplementations(originalMethod, swizzlingMethod);
//针对self.tempArray[4]取值进行防止越界crash
Method oriM = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
Method swiM = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(jd_objectAtIndexedSubscript:));
method_exchangeImplementations(oriM, swiM);
}
- (id)jd_objectAtIndex:(NSUInteger)index
{
//判断如果index比数组拥有的元素数量还多
if (index > (self.count - 1)) {
//你可以进行其他的处理,这里作为举例,只返回nil,只进行NSLog,方便查看控制台
NSLog(@"数组越界了");
return nil;
}
//这里再调用jd_objectAtIndex就已经在+(void)load中就被换成了objectAtIndex的实现
return [self jd_objectAtIndex:index];
}
- (id)jd_objectAtIndexedSubscript:(NSUInteger)idx
{
if ((unsigned long)index > (self.count - 1)) {
NSLog(@"数组越界了");
return nil;
}
return [self jd_objectAtIndexedSubscript:(unsigned long)index];
}
2. 多次执行交换问题
上面的代码的确解决了数组越界造成的crash
,但是如果在有人不知情的情况下,在代码中又调用了[NSArray load]
,就会出现又一个问题,sel
对应的imp
被多次交换,可能继续造成数组越界的crash
。所以还可以优化一下。
利用单例的设计,只让Method_Swizzling
出现一次。
+ (void)load
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
Method swizzlingMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(jd_objectAtIndex:));
method_exchangeImplementations(originalMethod, swizzlingMethod);
Method oriM = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndexedSubscript:));
Method swiM = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(jd_objectAtIndexedSubscript:));
method_exchangeImplementations(oriM, swiM);
});
}
3. 子类交换父类的实现
-
再创建一个
JDStudent(子类)
的分类JDStudent+JD
,进行Method_Swizzling
。 -
在
准备
中的JDPerson
类中创建一个实例方法- (void)personInstanceMethod;
,并且实现。子类则没有方法。 -
把
Method_Swizzling
封装成一个工具类RuntimeTools
,方便调用,也方便修改。
/**
RuntimeTools.h
*/
#import <Foundation/Foundation.h>
@interface RuntimeTools : NSObject
/**
交换方法
@param cls 交换对象
@param oriSEL 原始方法编号
@param swizzledSEL 交换的方法编号
*/
+ (void)jd_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swiSEL;
@end
/**
RuntimeTools.m
*/
#import "RuntimeTools.h"
#import <objc/runtime.h>
@implementation RuntimeTools
+ (void)jd_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swiSEL
{
if (!cls) NSLog(@"传入的交换类不能为空");
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swiSEL);
method_exchangeImplementations(oriMethod, swiMethod);
}
@end
因为子类会继承父类的实例方法,有的时候直接会用子类调用父类的实例方法,然后进行了imp
的交换,比如JDStudent+JD.m
中会有 :
#import "JDStudent+JD.h"
#import "RuntimeTools.h"
#import <objc/runtime.h>
@implementation JDStudent (JD)
+ (void)load
{
static dispatch_once_t jdOnceToken;
dispatch_once(&jdOnceToken, ^{
[RuntimeTools jd_methodSwizzlingWithClass:self oriSEL:@selector(personInstanceMethod) swizzledSEL:@selector(jd_studentInstanceMethod)];
});
}
- (void)jd_studentInstanceMethod
{
NSLog(@"JDStudent(子类)的方法 : %s",__func__);
[self jd_studentInstanceMethod];
}
@end
然后在ViewController
的- (void)viewDidLoad
:
#import "ViewController.h"
#import "JDPerson.h"
#import "JDStudent.h"
#import "JDStudent+JD.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
JDStudent *student = [[JDStudent alloc] init];
[student personInstanceMethod];
}
@end
执行结果是没有问题的,因为子类把父类本身的实现交换成了自己分类JDStudent+JD.m
中的jd_studentInstanceMethod
。如图 :
但是,如果这个时候,父类JDPerson
再调用自己的personInstanceMethod
就会出现问题。在ViewController
的- (void)viewDidLoad
中添加代码 :
JDPerson *person = [[JDPerson alloc] init];
[person personInstanceMethod];
再次执行,报错。因为它的imp
被JDStudent
交换了,可是它是父类,是找不到子类的实现的。就会出现如下图错误 :
这也会引发错误,所以还可以对RunTimeTools
中的Method_Swizzling
方法进行改进。
改进的思路是给子类进行方法的添加,然后让子类交换添加后的,自己的personInstanceMethod
,这就不会影响父类自己的实现。
改进后RunTimeTools
代码 :
#import "RuntimeTools.h"
#import <objc/runtime.h>
@implementation RuntimeTools
+ (void)jd_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swiSEL
{
if (!cls) NSLog(@"传入的交换类不能为空");
//还是先拿好这两个方法
//因为如果子类本身就有`personInstanceMethod`方法,就不需要再进行添加
//直接交换也无所谓,因为交换的是子类自己的`personInstanceMethod`方法
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swiSEL);
//用来判断是否可以给子类添加`personInstanceMethod`方法
//可以添加则证明子类没有这个方法
//不能添加则证明子类有这个方法,不需要再添加,直接交换即可,不会影响父类
BOOL canAdd = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (canAdd)
{
//子类没有这个方法的实现,现在刚刚加进去
//但是子类的personInstanceMethod--->jd_studentInstanceMethod的实现
//而jd_studentInstanceMethod又调用了自己,如果不把jd_studentInstanceMethod修改
//成父类的实现,那么就会一直递归jd_studentInstanceMethod
//所以把子类的jd_studentInstanceMethod的实现,替换成父类的实现
class_replaceMethod(cls, swiSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}
else
{
//子类本身就有这个方法的实现,那么直接交换
method_exchangeImplementations(oriMethod, swiMethod);
}
}
@end
再执行,就不会发生上面的错误了。执行结果 :
图3.3.2.png4. 父类也没有实现,子类交换父类的方法
就是说如果父类的方法也没实现,子类也没有一个方法的实现,但是子类还是交换了父类的方法,就会出现一个不停递归的问题。
就上面的代码,把personInstanceMethod
的实现从JDPerson.m
里面去掉。再执行。就会出现如下图的问题 :
原因 :
图3.4.1.png没有实现就没办法替换imp
,所以jd_studentInstanceMethod
还是自己的实现,就造成了死循环。
解决 :
-
先给子类添加上它调用的方法,也就是和上面一样,利用
class_addMethod
-
子类有了方法后,只要给
sel : jd_studentInstanceMethod
替换一个存在的IMP
就行。 -
所以要给
swiMethod
也添加实现。
#import "RuntimeTools.h"
#import <objc/runtime.h>
@implementation RuntimeTools
+ (void)jd_methodSwizzlingWithClass:(Class)cls oriSEL:(SEL)oriSEL swizzledSEL:(SEL)swiSEL
{
if (!cls) NSLog(@"传入的交换类不能为空");
//还是先拿好这两个方法
//因为如果子类本身就有`personInstanceMethod`方法,就不需要再进行添加
//直接交换也无所谓,因为交换的是子类自己的`personInstanceMethod`方法
Method oriMethod = class_getInstanceMethod(cls, oriSEL);
Method swiMethod = class_getInstanceMethod(cls, swiSEL);
//如果父类也没有方法实现
if (!oriMethod) {
class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
method_setImplementation(swiMethod, imp_implementationWithBlock(^(id self, SEL _cmd){ }));
}
//用来判断是否可以给子类添加`personInstanceMethod`方法
//可以添加则证明子类没有这个方法
//不能添加则证明子类有这个方法,不需要再添加,直接交换即可,不会影响父类
BOOL canAdd = class_addMethod(cls, oriSEL, method_getImplementation(swiMethod), method_getTypeEncoding(swiMethod));
if (canAdd)
{
//子类没有这个方法的实现,现在刚刚加进去
//但是子类的personInstanceMethod--->jd_studentInstanceMethod的实现
//而jd_studentInstanceMethod又调用了自己,如果不把jd_studentInstanceMethod修改
//成父类的实现,那么就会一直递归jd_studentInstanceMethod
//所以把子类的jd_studentInstanceMethod的实现,替换成父类的实现
class_replaceMethod(cls, swiSEL, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
}
else
{
//子类本身就有这个方法的实现,那么直接交换
method_exchangeImplementations(oriMethod, swiMethod);
}
}
@end
在ViewController.m
的- (void)viewDidLoad
中让子类JDStudent
调用方法personInstanceMethod
就不会出现死循环。
效果图 :
图3.4.2.png
网友评论