IOS 黑魔法(方法交换) -- MethodSwizzle

前言

  • MethodSwizzle顾名思义是方法交换,也就是交换方法IMP实现。一般能做很多面向切面的事,但是如果使用不当,就会踩到不少坑。
  • 一般是在 + load 中执行方法交换的。因为load方法加载时机较早,基本能确保方法已交换。
  • 需要确保交换的方法是本类的方法,而不是父类的。直接交换父类方法,会影响其它子类。
  • 方法交换时还需要特别注意类簇,确保交换的是正确的类。
  • 实例方法存储在类对象中,类方法存储在元类对象中。
  • 经典isa流程图:


    isa流程图.png

开始玩一下

一、首先简单实现一下方法交换

/**
 方法交换
 @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

输出结果:


image.png
  • 在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

输出结果:


image.png
  • 结论

由于子类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中测试,其代码不变,输出结果:


    image.png
  • 结论

由此看出,此时交换的本类的方法,不会影响其它子类调用方法。但是还有问题,当父类方法只声明了,没有实现的话,而你在交换的方法中又需要调用原方法的时候,会产生死递归。

  • 上述描述问题重现
    在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

输出结果:


image.png

三、再次修改一下方法交换的实现

  • 判断原方法是否有实现,没有实现添加一个空实现
/**
 方法交换
 @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);
    }
}

输出结果:


image.png

四、交换类方法

  • 类方法交换基本和实例方法交换差不多
  • 需要注意的是类方法其实是元类的实例方法,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

输出结果:


image.png
  • 断点检测是否有调用方法交换


    image.png
  • 结论

方法交换确实调用了,那就是已经把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
  • 再次测试,输出结果


    image.png

六、结论

MethodSwizzle虽然好用,但是一不小心,可能让你找半天都不知道问题出在哪。特别注意多人开发,同时使用了方法交换相同的方法,会有很多意想不到的问题。

你可能感兴趣的:(IOS 黑魔法(方法交换) -- MethodSwizzle)