iOS黑魔法(Method Swizzling)

刚开始学习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对系统方法进行替换,除非必要的情况下。

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    萌萌的小伟哥阅读 714评论 0 9
  • 场景需求:在没有一个类的实现源码的情况下,想改变其中一个方法(一般指系统的方法)的实现,除了继承它重写、和借助类别...
    船长_阅读 1,140评论 0 17
  • 前言: 今天我们再来了解另外一个体现OC动态特性的技术,向来有IOS黑魔法之称的Method Swizzling,...
    cxlhaha阅读 325评论 0 3
  • 目录 Objective-C Runtime到底是什么 Objective-C的元素认知 Runtime详解 应用...
    Ryan___阅读 919评论 1 3
  • 也许学习就是那样,学的越多就越觉得自己不会的多,而学习的目的也许就是:在某一天你能站在她的肩膀上看到更美的风景。(...
    极客汤米阅读 157评论 3 4