前言
- MethodSwizzle顾名思义是方法交换,也就是交换方法IMP实现。一般能做很多面向切面的事,但是如果使用不当,就会踩到不少坑。
- 一般是在 + load 中执行方法交换的。因为load方法加载时机较早,基本能确保方法已交换。
- 需要确保交换的方法是本类的方法,而不是父类的。直接交换父类方法,会影响其它子类。
- 方法交换时还需要特别注意类簇,确保交换的是正确的类。
- 实例方法存储在类对象中,类方法存储在元类对象中。
-
经典isa流程图:
开始玩一下
一、首先简单实现一下方法交换
/**
方法交换
@param origSel 原方法名
@param newSel 新方法名
*/
+(void) methodSwizzleWithOrigSel:(SEL)origSel newSel:(SEL)newSel
{
//类对象(实例方法存储在类对象中) -- 由于此方法是类方法,所以self是类对象
Class mClass = [self class];
//方法
Method origMethod = class_getInstanceMethod(mClass, origSel);
Method newMethod = class_getInstanceMethod(mClass, newSel);
//imp
IMP origIMP = method_getImplementation(origMethod);
IMP newIMP = method_getImplementation(newMethod);
method_setImplementation(origMethod, newIMP);
method_setImplementation(newMethod, origIMP);
}
- 创建类Person及其子类Student、Student2
Person:
#import
NS_ASSUME_NONNULL_BEGIN
@interface Person : NSObject
//名称
@property (nonatomic,copy) NSString *name;
//年龄
@property (nonatomic,assign) NSInteger age;
-(NSString *)name;
@end
NS_ASSUME_NONNULL_END
#import "Person.h"
@implementation Person
-(NSString *)name
{
return @"person";
}
@end
Student:
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Student : Person
//学科
@property (nonatomic,copy) NSString *subject;
@end
NS_ASSUME_NONNULL_END
Student2:
#import "Person.h"
NS_ASSUME_NONNULL_BEGIN
@interface Student2 : Person
//别名
@property (nonatomic,copy) NSString *nickName;
@end
NS_ASSUME_NONNULL_END
- 我们来hook get方法(在Student类中hook name方法)
#import "Student.h"
#import "NSObject+MethodSwizzle.h"
@implementation Student
+(void) load
{
[self methodSwizzleWithOrigSel:@selector(name) newSel:@selector(myName)];
}
-(NSString *) myName
{
return @"学生1";
}
@end
- 在ViewController中测试
#import "ViewController.h"
#import "Student.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Student *stu = [[Student alloc] init];
NSLog(@"%@",stu.name);
}
@end
输出结果:
- 在ViewController中增加Student2的name输出
#import "ViewController.h"
#import "Student.h"
#import "Student2.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
Student *stu = [[Student alloc] init];
NSLog(@"%@",stu.name);
Student2 *stu2 = [[Student2 alloc] init];
NSLog(@"%@",stu2.name);
}
@end
输出结果:
- 结论
由于子类Student交换的是父类Person的name方法,所以影响了其它子类调用父类的name方法,都会变成调用Student的myName方法。
二、修改一下方法交换的实现
- 判断子类是否有实现需要交换的方法,没有实现则添加
/**
方法交换
@param origSel 原方法名
@param newSel 新方法名
*/
+(void) methodSwizzleWithOrigSel:(SEL)origSel newSel:(SEL)newSel
{
//类对象(实例方法存储在类对象中) -- 由于此方法是类方法,所以self是类对象
Class mClass = [self class];
//方法
Method origMethod = class_getInstanceMethod(mClass, origSel);
Method newMethod = class_getInstanceMethod(mClass, newSel);
//imp
IMP origIMP = method_getImplementation(origMethod);
IMP newIMP = method_getImplementation(newMethod);
//方法添加成功代表target中不包含原方法,可能是其父类包含(交换父类方法可能有意想不到的问题)
if(class_addMethod(mClass, origSel, origIMP, method_getTypeEncoding(origMethod))){
//直接替换新添加的方法
class_replaceMethod(mClass, origSel, newIMP, method_getTypeEncoding(newMethod));
}else{
method_setImplementation(origMethod, newIMP);
method_setImplementation(newMethod, origIMP);
}
}
-
再次在ViewController中测试,其代码不变,输出结果:
结论
由此看出,此时交换的本类的方法,不会影响其它子类调用方法。但是还有问题,当父类方法只声明了,没有实现的话,而你在交换的方法中又需要调用原方法的时候,会产生死递归。
- 上述描述问题重现
在Person类增加say方法但不实现,Student类中交换say方法
@interface Person : NSObject
//名称
@property (nonatomic,copy) NSString *name;
//年龄
@property (nonatomic,assign) NSInteger age;
-(NSString *)name;
-(void) say;
@end
@implementation Student
+(void) load
{
[self methodSwizzleWithOrigSel:@selector(say) newSel:@selector(mySay)];
}
-(NSString *) myName
{
return @"学生1";
}
-(void) mySay
{
NSLog(@"%@",@"学生1说话");
//调用父类方法
[self mySay];
}
@end
输出结果:
三、再次修改一下方法交换的实现
- 判断原方法是否有实现,没有实现添加一个空实现
/**
方法交换
@param origSel 原方法名
@param newSel 新方法名
*/
+(void) methodSwizzleWithOrigSel:(SEL)origSel newSel:(SEL)newSel
{
//类对象(实例方法存储在类对象中) -- 由于此方法是类方法,所以self是类对象
Class mClass = [self class];
//方法
Method origMethod = class_getInstanceMethod(mClass, origSel);
Method newMethod = class_getInstanceMethod(mClass, newSel);
if (!origMethod) {//原方法没实现
class_addMethod(mClass, origSel, imp_implementationWithBlock(^(id self, SEL _cmd){}), "v16@0:8");
origMethod = class_getInstanceMethod(mClass, origSel);
}
//imp
IMP origIMP = method_getImplementation(origMethod);
IMP newIMP = method_getImplementation(newMethod);
//方法添加成功代表target中不包含原方法,可能是其父类包含(交换父类方法可能有意想不到的问题)
if(class_addMethod(mClass, origSel, origIMP, method_getTypeEncoding(origMethod))){
//直接替换新添加的方法
class_replaceMethod(mClass, origSel, newIMP, method_getTypeEncoding(newMethod));
}else{
method_setImplementation(origMethod, newIMP);
method_setImplementation(newMethod, origIMP);
}
}
输出结果:
四、交换类方法
- 类方法交换基本和实例方法交换差不多
- 需要注意的是类方法其实是元类的实例方法,class_getClassMethod实际上内部还是调用class_getInstanceMethod。
/***********************************************************************
* class_getClassMethod. Return the class method for the specified
* class and selector.
**********************************************************************/
Method class_getClassMethod(Class cls, SEL sel)
{
if (!cls || !sel) return nil;
return class_getInstanceMethod(cls->getMeta(), sel);
}
- 所以只要确保class_getInstanceMethod方法中的第一个参数是元类对象,我们就可以直接调用class_getInstanceMethod来获取类方法,从而减少调用class_getClassMethod时需要的元类判断。
/**
类方法交换
@param origSel 原类方法名
@param newSel 新类方法名
*/
+(void) methodSwizzleWithOrigClassSel:(SEL)origSel newClassSel:(SEL)newSel
{
//元类(类方法存储在元类对象中)
Class metaClass = object_getClass([self class]);
//方法
Method origMethod = class_getInstanceMethod(metaClass, origSel);
Method newMethod = class_getInstanceMethod(metaClass, newSel);
if (!origMethod) {//原方法没实现
class_addMethod(metaClass, origSel, imp_implementationWithBlock(^(id self, SEL _cmd){}), "v16@0:8");
origMethod = class_getInstanceMethod(metaClass, origSel);
}
//imp
IMP origIMP = method_getImplementation(origMethod);
IMP newIMP = method_getImplementation(newMethod);
//方法添加成功代表target中不包含原方法,可能是其父类包含(交换父类方法可能有意想不到的问题)
if(class_addMethod(metaClass, origSel, origIMP, method_getTypeEncoding(origMethod))){
//直接替换新添加的方法
class_replaceMethod(metaClass, origSel, newIMP, method_getTypeEncoding(newMethod));
}else{
method_setImplementation(origMethod, newIMP);
method_setImplementation(newMethod, origIMP);
}
}
五、交换类簇的方法
1、问题
- 创建NSArray的分类,检测数组越界
#import "NSArray+CheckSize.h"
#import "NSObject+MethodSwizzle.h"
@implementation NSArray (CheckSize)
+(void) load
{
[self methodSwizzleWithOrigSel:@selector(objectAtIndex:) newSel:@selector(myObjectAtIndex:)];
}
- (id)myObjectAtIndex:(NSUInteger)index
{
if (index > [self count] - 1) {
NSLog(@"数组越界了");
return nil;
}
return [self myObjectAtIndex:index];
}
@end
- 在ViewController中测试
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSArray *array = @[@"1",@"2",@"3"];
for (int i = 0 ; i < 4; i++) {
NSLog(@"%d-->%@",i,[array objectAtIndex:i]);
}
}
@end
输出结果:
-
断点检测是否有调用方法交换
结论
方法交换确实调用了,那就是已经把NSArray的objectAtIndex方法改成分类中的myObjectAtIndex。但是并不管用,一样奔溃,原因就是因为类簇,可以从崩溃信息中看到实际调用的类是__NSArrayI。
2、解决
- 写一个新的方法交换方法,支持设置OrigTarget
/**
方法交换
@param origTarget 被交换方法的类
@param origSel 原方法名
@param newSel 新方法名
*/
+(void) methodSwizzleWithOrigTarget:(Class)origTarget OrigSel:(SEL)origSel newSel:(SEL)newSel
{
//类对象(实例方法存储在类对象中)
Class origClass = origTarget;
if ([origTarget isKindOfClass:[origTarget class]]) {//成立则origTarget为实例对象
origClass = object_getClass(origTarget);
}
//方法
Method origMethod = class_getInstanceMethod(origClass, origSel);
Method newMethod = class_getInstanceMethod(origClass, newSel);
if (!origMethod) {//原方法没实现
class_addMethod(origClass, origSel, imp_implementationWithBlock(^(id self, SEL _cmd){}), "v16@0:8");
origMethod = class_getInstanceMethod(origClass, origSel);
}
//imp
IMP origIMP = method_getImplementation(origMethod);
IMP newIMP = method_getImplementation(newMethod);
//方法添加成功代表target中不包含原方法,可能是其父类包含(交换父类方法可能有意想不到的问题)
if(class_addMethod(origClass, origSel, origIMP, method_getTypeEncoding(origMethod))){
//直接替换新添加的方法
class_replaceMethod(origClass, origSel, newIMP, method_getTypeEncoding(newMethod));
}else{
method_setImplementation(origMethod, newIMP);
method_setImplementation(newMethod, origIMP);
}
}
- 修改NSArray分类中的交换方法
#import "NSArray+CheckSize.h"
#import "NSObject+MethodSwizzle.h"
@implementation NSArray (CheckSize)
+(void) load
{
[self methodSwizzleWithOrigTarget:NSClassFromString(@"__NSArrayI") OrigSel:@selector(objectAtIndex:) newSel:@selector(myObjectAtIndex:)];
}
- (id)myObjectAtIndex:(NSUInteger)index
{
if (index > [self count] - 1) {
NSLog(@"数组越界了");
return nil;
}
return [self myObjectAtIndex:index];
}
@end
-
再次测试,输出结果
六、结论
MethodSwizzle虽然好用,但是一不小心,可能让你找半天都不知道问题出在哪。特别注意多人开发,同时使用了方法交换相同的方法,会有很多意想不到的问题。