iOS RunTime 学习记录5_Method Swizzling

前言:我是参考 南峰子 的博客加上自己理解写的,原著专辑大家自己可看:http://southpeak.github.io/categories/objectivec/

1,Method Swizzling啥意思

Method Swizzling 直译是"方法调和",理解起来我们可以想成是方法调剂,方法调整。这样就是方法执行的不合适,我们把它调整调整,哈哈!

通过这个功能,能帮助我们更好的理解Runtime的运行机制,即是在运行时绑定方法。其实一般用在想要不修改原有方法(包括系统方法)实现的情况下,替换掉原方法的实现。其实这个功能真的还是蛮吊的,都说是黑魔法。

2,Method Swizzling常用的网络举栗子

举个例子:
实现功能就是全局在在应用中viewDidAppear插入一个统计代码,这个一般我们不用Runtime中的方法就一个一个添加,或者是所有的ViewContoller都继承一个父类,在父类中添加。但是使用RunTime的Method Swizzling就可以轻松实现这个需求

@implementation UIViewController (Swizzling)
+(void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        SEL originalSelector = @selector(viewDidAppear:);
        SEL diySelector = @selector(diyViewDidAppear:);
        
        Method originalMethod = class_getInstanceMethod([self class], originalSelector);
        Method diyMethod = class_getInstanceMethod([self class], diySelector);
        
        //这个添加成功后 就会 用swizzingSEL的IMP指针 指向 originalSEL的 MP指针
        BOOL didAddMethod = class_addMethod([self class], originalSelector, method_getImplementation(diyMethod), method_getTypeEncoding(diyMethod));
        
        
        //下面就是 交换exchange 两个SEL 的 IMP指针
        if (didAddMethod) {
            
            //该函数的行为可以分为两种:如果类中不存在name指定的方法,则类似于class_addMethod函数一样会添加方法;如果类中已存在name指定的方法,则类似于method_setImplementation一样替代原方法的实现
            
            /*如果添加成功
             * 添加成功会发生  1,在self中 没有 originalSEL的实现(即使是父类中实现originalSEL方法也不行)
             *              2, 在self中 有 swizzingSEL的实现(即使父类中实现swizzingSEL也不行)
             * 添加成功后 用originalSEL的IMP指针 指向 swizzingSEL的 MP指针
             */
            
            class_replaceMethod([self class], diySelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        }else{
            
            /***没有添加成功,那么就是说originalSEL 和 swizzingSEL 实现都存在
             *  直接使用method_exchangeImplementations 交换IMP指针就好
             **/
            method_exchangeImplementations(originalMethod, diyMethod);
        }
    });
}

-(void)diyViewDidAppear:(BOOL)anmiate{
    NSLog(@"ViewDidAppear重新指向");
    [self diyViewDidAppear:anmiate];
}

具体代码就是获取ViewController原生的ViewDidAppear 的Method和自定义diyViewDidAppear的Method 指针,然后交换它两 的IMP(方法实现的标准C调用首地址),通俗的讲就是交换他俩的具体实现。

在这个方法中,会发现最后又调用了自身

-(void)diyViewDidAppear:(BOOL)anmiate{
    NSLog(@"ViewDidAppear重新指向");
    [self diyViewDidAppear:anmiate];
}

是不是会循环调用了,其实不会,因为它俩的实现被交换了,所以调用的diyViewDidAppear:其实会调用它原生的viewDidAppear :。我们来验证一下,在ViewController中,我们这样写:

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
   
//    Student *s = [[Student alloc]init];
//    [s performSelector:@selector(introduceSelf)];
//    [s performSelector:@selector(swizzingIntroduceSelf)];
//    
//    NSArray *testArray = @[@"A",@"B",@"C"];
//    NSString *tStr = [testArray objectAtIndex:4];
    
    
}

-(void)viewDidAppear:(BOOL)animated{
    [super viewDidAppear:animated];
   
    NSLog(@"我是原生方法");
}

运行一下,打印结果:

2017-01-12 14:42:04.689 MethodSwizzlingTest[4892:436048] ViewDidAppear重新指向
2017-01-12 14:42:04.690 MethodSwizzlingTest[4892:436048] ViewDidAppear原生方法

3,自写类说明Method Swizzling的方法替换

为了更好的说明,这个关系,我们用一个普通自定义的类来做说明,一个是Person类 一个继承Person的Student类:
其中Person实现如下:

@implementation Person

-(void)introduceSelf{
    NSLog(@"person:原有实现");
}

-(void)swizzingIntroduceSelf{
    NSLog(@"person:调剂方法");
}

@end

Student类的实现

@implementation Student

+(void)load{
    
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        
        SEL originalSEL = @selector(introduceSelf);
        SEL swizzingSEL = @selector(swizzingIntroduceSelf);
        
        Method originalMethod = class_getInstanceMethod([self class], originalSEL);
        Method swizzingMethod = class_getInstanceMethod([self class], swizzingSEL);
        
        /******交换指针*******/
        //这个添加成功后 就会 用swizzingSEL的IMP指针 指向 originalSEL的 MP指针
        BOOL didAddMethod = class_addMethod([self class],
                                            originalSEL,
                                            method_getImplementation(swizzingMethod),
                                            method_getTypeEncoding(swizzingMethod));
        
        //下面就是 交换exchange 两个SEL 的 IMP指针
        if (didAddMethod) {
            /*如果添加成功
             * 添加成功会发生  1,在self中 没有 originalSEL的实现(即使是父类中实现originalSEL方法也不行)
             *              2, 在self中 有 swizzingSEL的实现(即使父类中实现swizzingSEL也不行)
             * 添加成功后 用originalSEL的IMP指针 指向 swizzingSEL的 MP指针
             */
            class_replaceMethod([self class],
                                swizzingSEL,
                                method_getImplementation(originalMethod),
                                method_getTypeEncoding(originalMethod));
        } else {
            /***没有添加,那么就是说originalSEL 和 swizzingSEL 实现都存在
             *  直接使用method_exchangeImplementations 交换IMP指针就好
             **/
            method_exchangeImplementations(originalMethod, swizzingMethod);
        }
        
        //其实上面这个判断可以归纳为,只要我实现了交换方法swizzingMethod,我都进行 Method Swizzling
//        if (!class_addMethod([self class], swizzingSEL, method_getImplementation(swizzingMethod), method_getTypeEncoding(swizzingMethod))) {
//            method_exchangeImplementations(originalMethod, swizzingMethod);
//        }
    });
}

-(void)introduceSelf{
    NSLog(@"Student:原有实现");
}

-(void)swizzingIntroduceSelf{
    NSLog(@"Student:调剂方法");
    [self swizzingIntroduceSelf];
    
}

@end

这里我们实现了introduceSelf 和 swizzingIntroduceSelf的方法实现交换,我们在VC中这样调用:

    Student *s = [[Student alloc]init];
    [s performSelector:@selector(introduceSelf)];

打印结果:

2017-01-12 15:07:24.333 MethodSwizzlingTest[5232:525878] Student:调剂方法
2017-01-12 15:07:24.334 MethodSwizzlingTest[5232:525878] Student:原有实现

可以看出我们调用的是introduceSelf,确打印swizzingIntroduceSelf的实现,最后我们在swizzingIntroduceSelf中调用自身,会发现它执行的确实'introduceSelf'的实现。我么可以屏蔽最后调用自身的这段代码如下:

-(void)swizzingIntroduceSelf{
    NSLog(@"Student:调剂方法");
//    [self swizzingIntroduceSelf];
}

打印输出:

2017-01-12 15:14:00.866 MethodSwizzlingTest[5323:547121] Student:调剂方法

会发现,完全实现了替换。

我们这样调用

    Student *s = [[Student alloc]init];
    [s performSelector:@selector(swizzingIntroduceSelf)];

结果:

2017-01-12 16:17:47.172 MethodSwizzlingTest[6058:706641] Student:原有实现

会发现方法两个实现方法被完全替换。

4,关于didAddMethod 的判断

这篇文章解释的非常好http://www.jianshu.com/p/f6dad8e1b848,可以看看。

交换之前做了class_addMethod判断

关于class_addMethod,这个解释比较好:
class_addMethod为一个类添加方法(包括方法名称(SEL)和方法的实现(IMP)),返回值为BOOL类型,表示方法是否成功添加。需要注意的地方是class_addMethod会添加一个覆盖父类的实现,但不会取代原有类的实现。也就是说如果class_addMethod返回YES,说明子类中没有方法originalSelector,通过class_addMethod为其添加了方法originalSelector,并使其实现(IMP)为我们想要替换的实现。

//这个添加成功后 就会 用swizzingSEL的IMP指针 指向 originalSEL的 MP指针
        BOOL didAddMethod = class_addMethod([self class],
                                            originalSEL,
                                            method_getImplementation(swizzingMethod),
                                            method_getTypeEncoding(swizzingMethod));

这个是为什么了,其实就是为了防止本类中originalSEL没有实现,人家说是一种保护,我的注释已经写上,如下:

didAddMethod = YES
添加返回(didAddMethod ==YES),必须满足下面的条件:
  1,在self中 没有 originalSEL的实现(即使是父类中实现originalSEL方法也不行)
  2, 在self中 有 swizzingSEL的实现(即使父类中实现swizzingSEL也不行)

这时候swizzingSEL方法的实现就会赋给originalSEL,同时添加originalSEL方法。在添加成功后,我们还需要把originalSEL的实现替换给swizzingSEL,所有就调用了class_replaceMethod方法,这样就完成交换。

didAddMethod = NO

说明方法未添加成功,这说明自身中已经实现originalSEL方法,所以直接调用method_exchangeImplementations来完成方法实现的交换

5,Method Swizzling 额外知识(我只是个搬运工)

为什么方法交换调用在+load方法中?
在Objective-C runtime会自动调用两个类方法,分别为+load与+ initialize。+load 方法是在类被加载的时候调用的,也就是一定会被调用。而+initialize方法是在类或它的子类收到第一条消息之前被调用的,这里所指的消息包括实例方法和类方法的调用。也就是说+initialize方法是以懒加载的方式被调用的,如果程序一直没有给某个类或它的子类发送消息,那么这个类的+initialize方法是永远不会被调用的。此外+load方法还有一个非常重要的特性,那就是子类、父类和分类中的+load方法的实现是被区别对待的。换句话说在 Objective-C runtime自动调用+load方法时,分类中的+load方法并不会对主类中的+load方法造成覆盖。综上所述,+load 方法是实现 Method Swizzling 逻辑的最佳“场所”。如需更深入理解,可参考Objective-C 深入理解 +load 和 +initialize。

为什么方法交换要在dispatch_once中执行?
方法交换应该要线程安全,而且保证在任何情况下(多线程环境,或者被其他人手动再次调用+load方法)只交换一次,防止再次调用又将方法交换回来。除非只是临时交换使用,在使用完成后又交换回来。 最常用的解决方案是在+load方法中使用dispatch_once来保证交换是安全的。之前有读者反馈+load方法本身即为线程安全,为什么仍需添加dispatch_once,其原因就在于+load方法本身无法保证其中代码只被执行一次。

6,Method Swizzling 试用场景

实例一:替换ViewController生命周期方法
实例二:解决获取索引、添加、删除元素越界崩溃问题
实例三:防止按钮重复暴力点击
实例四:全局更换控件初始效果
实例五:App热修复
实例六:App异常加载占位图通用类封装
实例七:全局修改导航栏后退(返回)按钮

6.1第一个就是我们刚开始引入的时候举得例子。

6.2第二个我的Demo里面有,下有Demo下载地址

实现思路就是替换objectAtIndex:方法,在里面对索引进行判断

-(void)diyObjectAtIndex:(NSUInteger)index{
    
    if (index < self.count) {
        [self diyObjectAtIndex:index];
    }else{
        @try {
            [self diyObjectAtIndex:index];
        } @catch (NSException *exception) {
            NSLog(@"数组越界");
        } @finally {
            
        }
    }
}

搬运工补充知识:
这里没有使用self来调用,而是使用objc_getClass("__NSArrayM")来调用的。因为NSMutableArray的真实类只能通过后者来获取,而不能通过[self class]来获取,而method swizzling只对真实的类起作用。这里就涉及到一个小知识点:类簇。补充以上对象对应类簇表。


Paste_Image.png

6.3 来试试

你可能感兴趣的:(iOS RunTime 学习记录5_Method Swizzling)