美文网首页
IOS 黑魔法(方法交换) -- MethodSwizzle

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

作者: Devil_Chen | 来源:发表于2019-03-26 14:23 被阅读0次

    前言

    • 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 <Foundation/Foundation.h>
    
    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

          本文链接:https://www.haomeiwen.com/subject/jgwwvqtx.html