美文网首页
iOS黑魔法(Method Swizzling)

iOS黑魔法(Method Swizzling)

作者: 东了个尼 | 来源:发表于2017-08-17 23:43 被阅读0次

    刚开始学习IOS的时候,听说黑魔法很强大,正如它的名字一样,可以做很多不可思议的事情,一直到今天才彻底静下心去了解了Method Swizzling一下,所以写下这篇文章分享给大家。(由于最近希望写文章的时候,顺便可以学习下英文,所以准备在后续的每一篇文章中加入一句英文名言,希望可以给大家一些鼓励。
    本文内容参考 [南峰子的技术博客]
    (http://southpeak.github.io/2014/11/06/objective-c-runtime-4/)
    参考文献
    东了个尼github

    “Too many of us are not living our dreams because we are living our fears.”
    — Les Brown, Motivational Speaker

    Method Swizzling

    Method Swizzling 原理

    1.Method Swizzling 原理
    在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
    每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点>>类似函数指针,指向具体的Method实现。

    Method Swizzling 简介

    在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用objective-c的动态特性,可以实现在运行时偷换selector对应的方法实现。
    每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。
    我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP,
    我们可以利用 class_replaceMethod 来修改类,
    我们可以利用 method_setImplementation 来直接设置某个方法的IMP,
    通过上边的方法,可以把类的调度表(dispatch table)中选择器到最终函数间的映射关系 替换。就相当于把IMP 的只想替换了。

    swizzling 需要在 + (void)load{}中使用
    swizzling 需要保证只执行一次。 需要使用 dispatch_once;
    load 和initialize区别:load是只要类所在文件被引用就会被调用,而initialize是在类或者其子类的第一个方法被调用前调用。所以如果类没有被引用进项目,就不会有load调用;但即使类文件被引用进来,但是没有使用,那么initialize也不会被调用。

    Method Swizzling 调用

    在Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证–事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。

    Swizzling应该在dispatch_once中实现。

    还是因为swizzling会改变全局,我们需要在运行时采取所有可用的防范措施。保障原子性就是一个措施,它确保代码即使在多线程环境下也只会被执行一次。GCD中的diapatch_once就提供这些保障,它应该被当做swizzling的标准实践。

    Method Swizzling 实践(一)

    举个例子,假设我们想跟踪在一个iOS应用中每个视图控制器展现给用户的次数:
    我们可以给每个视图控制器对应的viewWillAppear:实现方法中增加相应的跟踪代码,但是这样做会产生大量重复的代码。子类化可能是另一个选择,但要求你将UIViewController、 UITableViewController、 UINavigationController 以及所有其他视图控制器类都子类化,这也会导致代码重复。

    幸好,还有另一个方法,在分类中进行method swizzling,下面来看怎么做:

    #import <objc/runtime.h>
    @implementation UIViewController (Tracking)
    
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class class = [self class];
    
            // When swizzling a class method, use the following:
            // Class class = object_getClass((id)self);
    
            SEL originalSelector = @selector(viewWillAppear:);
            SEL swizzledSelector = @selector(xxx_viewWillAppear:);
        // 通过class_getInstanceMethod()函数从当前对象中的methodlist获取   method结 构体,如果是类方法就使用class_getClassMethod()函数获取 
            Method originalMethod = class_getInstanceMethod(class, originalSelector);
            Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
        /**
         *  我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。
         *  而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。
         *  所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。
         */
            BOOL didAddMethod =
                class_addMethod(class,
                    originalSelector,
                    method_getImplementation(swizzledMethod),
                    method_getTypeEncoding(swizzledMethod));
            if (didAddMethod) {
                class_replaceMethod(class,
                    swizzledSelector,
                    method_getImplementation(originalMethod),
                    method_getTypeEncoding(originalMethod));
            } else {
              // 交换的方法。
                method_exchangeImplementations(originalMethod, swizzledMethod);
            }
        });
    }
    
    #pragma mark - Method Swizzling
    - (void)xxx_viewWillAppear:(BOOL)animated {
        [self xxx_viewWillAppear:animated];
        NSLog(@"viewWillAppear: %@", self);
    }
    

    Method Swizzling 实践(二)

    需要统计事件,或者需要输出Log的时候,比如在delloc中,输出log。告诉我们哪个类释放了以及一些类的信息。

    //封装方法
    + (void)swizzWithClass:(Class)class originSel:(SEL)originSel newSel:(SEL)newSel{
    
    Method originM = class_getInstanceMethod(class, originSel);
    Method newM = class_getInstanceMethod(class, newSel);
    
    IMP newImp =  method_getImplementation(newM);
    
    BOOL addMethodSucess = class_addMethod(class, newSel, newImp, method_getTypeEncoding(newM));
    
    if (addMethodSucess) {
        class_replaceMethod(class, originSel, newImp, method_getTypeEncoding(newM));
    }else{
        method_exchangeImplementations(originM, newM);
      }
    }
    

    代码如下

    //在类加载的时候调用 需要保证只执行一次,多次,容易出现不可预知的错误。
    + (void)load{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzWithClass:[self class] originSel:NSSelectorFromString(@"dealloc") newSel:@selector(swizz_dealloc)];
    });
    }
    
    //调用自己写的方法 里面可以自定义想要执行的内容
    - (void)swizz_dealloc{
    NSLog(@" ** %@ 释放了 %s",NSStringFromClass([self class]),__func__);
    [self swizz_dealloc];
    }
    

    Method Swizzling 实践(三)

    处理数组越界问题空值,导致程序崩溃的问题。
    小伙伴们都有遇到过数组越界导致程序崩溃的经历,我们可以给NSArray创建一个类目去解决
    代码如下:

    + (void)load{
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Method fromMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(objectAtIndex:));
            Method toMethod = class_getInstanceMethod(objc_getClass("__NSArrayI"), @selector(fd_objectAtIndex:));
            method_exchangeImplementations(fromMethod, toMethod);
        });
    }
    
    //写在 NSArray的类目里边
    - (id)fd_objectAtIndex:(NSUInteger)index {
        if (self.count-1 < index) {
            // 这里做一下异常处理,不然都不知道出错了。
            @try {
                return [self fd_objectAtIndex:index];
            }
            @catch (NSException *exception) {
                // 在崩溃后会打印崩溃信息,方便我们调试。
                NSLog(@"---------- %s Crash Because Method %s  ----------\n", class_getName(self.class), __func__);
                NSLog(@"%@", [exception callStackSymbols]);
                return nil;
            }
            @finally {}
        } else {
            return [self fd_objectAtIndex:index];
        }
    }
    

    实现效果如下:更多的报错信息

    效果图

    在这里我推荐Github上星最多的一个第三方-jrswizzle,很好用!

    最后提醒小伙伴们不要滥用Method Swizzling对系统方法进行替换,除非必要的情况下。

    相关文章

      网友评论

          本文标题:iOS黑魔法(Method Swizzling)

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