Objective-C Runtime Method Swizzling 实践

Posted sym3099@gmail

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Objective-C Runtime Method Swizzling 实践相关的知识,希望对你有一定的参考价值。

直接上代码

1  交换实例方法:

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
    //交换的是实例方法
        Class class = [self class];
        SEL originalSelector = @selector(originMethod);//originMethod 已经存在的方法
        SEL swizzleSelector = @selector(swizzleMethod);//swizzleMethod 新添加的方法

        Method originalMethod = class_getInstanceMethod(class, originalSelector);
        Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);

        BOOL success = class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));

        if (success) {
            class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
        } else {
            method_exchangeImplementations(originalMethod, swizzledMethod);
        }
                
    });
}

  

2. 交换类方法

上面的交换的实例方法,交换类方法该怎么写呢?
1)  Class class = [self class]; 换为 Class class = object_getClass((id)self);
注意是object_getClass,不是objc_getClass
2)   上面的class_getInstanceMethod改为class_getClassMethod方法,其他不变,注意Class class = [self class];和class_getInstanceMethod对应;
Class class = object_getClass((id)self)和class_getClassMethod对应

3. 方法交换写完了,不知道你有没有一下疑问:


a 为什么上面的为什么写在+ (void)load ,只能写在load吗?
+ (void)load 这个方法比较特别,这个方法只要启动APP就会调用,不管你启动页面的相关代码里面有没有用到所属的类,都会调用这个方法;这个方法是当类或分类被添加到 Objective-C runtime 时被调用的,重载这个方法可以让我们完成一下初始化的操作,这个方法只会调用一次,在这个方法中千万不要进行耗时的操作,比如文件的读写。笔者当年就出过这个错误,导致APP启动变慢。这个方法是在main()方法执行前就执行了。

另外 +load 方法还有一个和其他方法不同的地方,那就是子类、父类和分类中的 +load 方法的实现是被区别对待的。也就是说如果子类没有实现 +load 方法,那么当它被加载时 runtime 是不会去调用父类的 +load 方法的。同理,当一个类和它的分类都实现了 +load 方法时,两个方法都会被调用。

以上特别决定了Method Swizzling写在+load 方法比较合适。
b 重新父类的load方法,为什么不调用[super load]?
上面已经说了,子类、父类和分类中的 +load 方法的实现是被区别对待的,系统会自动调用分别调用子类和父类中的+load 方法,所以子类一定不能使用[super load]

c Method Swizzling 为什么要加 dispatch_once 
加dispatch_once就是防止多次调用,确保代码只被执行一次,上面都说了+load 方法在类加载的时候会被 runtime 自动调用一次,而且只调用一次,为什么还加dispatch_once?为了防止程序员的手动调用。

d 为什么要调用class_addMethod,直接交换(如下)不就行了吗?

SEL originalSelector = @selector(originMethod);//originMethod 已经存在的方法
SEL swizzleSelector = @selector(swizzleMethod);//swizzleMethod 新添加的方法

Method originalMethod = class_getInstanceMethod(class, originalSelector);
Method swizzledMethod = class_getInstanceMethod(class, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);

  

要回到这个问题首首先说一下class_addMethod这个方法的做作用,
class_addMethod就是给类添加新的方法,如果类本身已经存在这个方法,就会添加失败返回NO,这里的类本身的方法,意思是不包括从父类继承过来的方法,什么意思?比如father 类有一个eat方法,son类继承自father类,但是son类没有重写父类(也就是father类)的eat方法,那么使用 eat 类使用class_addMethod添加eat方法,就能添加成功。

那么问题来了,首先我们要使用Method Swizzling,肯定是因为当前类已经存在了这个方法,如果类不存在这个方法直接使用category添加方法就行了,
当前类已经存在了这个方法,有两种情况:
a 对类本身存在方法交换方法,这里的类本身存在方法是指不是从父类继承过来的,这时直接交换即可,如下图:

技术分享图片
技术分享图片

b 对从父类继承过来的方法直接,交换方法,交换之后如下图:
技术分享图片
发现问题了吗?父类方法指向了子类的实现,如果用father 类调用father_A方法就会闪退。
实验
技术分享图片
技术分享图片
```
Class aClass = [self class];
SEL originalSel = @selector(father_A);
SEL swizzleSel = @selector(Swizz_A);

    Method originalMethod = class_getInstanceMethod(aClass, originalSel);
    Method swizzleMethod = class_getInstanceMethod(aClass, swizzleSel);

    method_exchangeImplementations(originalMethod, swizzleMethod);

Father *f = [Father new];
[f father_A];

2018-04-05 16:35:19.732717+0800 runtime 之Method Swizzling[65557:2828735] -[Father Swizz_A]: unrecognized selector sent to instance 0x1006348f0
2018-04-05 16:35:19.733565+0800 runtime 之Method Swizzling[65557:2828735] *** Terminating app due to uncaught exception ‘NSInvalidArgumentException‘, reason: ‘-[Father Swizz_A]: unrecognized selector sent to instance 0x1006348f0‘
```

c 如果先把父类的方法,利用class_addMethod添加到子类呢,也就是子类重写父类的方法,添加之后如下:
技术分享图片
没看明白是吗?让我们来分析一下添加的代码class_addMethod(class, originalSelector, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod))。
这里一共四个参数,这里的originalSelector就是father_A,method_getImplementation(swizzledMethod)就是Swizz_A的实现。这下明白了吧。

测试
技术分享图片

        Class aClass = [self class];
        SEL originalSel = @selector(father_A);
        SEL swizzleSel = @selector(Swizz_A);
        
        Method originalMethod = class_getInstanceMethod(aClass, originalSel);
        Method swizzleMethod = class_getInstanceMethod(aClass, swizzleSel);
        
        BOOL success = class_addMethod(aClass, originalSel, method_getImplementation(swizzleMethod), method_getTypeEncoding(swizzleMethod));
        
        if (success) {
            Method original = class_getInstanceMethod(aClass,originalSel);
            Method swizzle = class_getInstanceMethod(aClass, swizzleSel);
            IMP  originalIML = method_getImplementation(original);
            
            IMP  swizzleIML = method_getImplementation(swizzle);

可以看到originalIML和swizzleIML指向的是同一个实现

d 添加之后为什么不用method_exchangeImplementations方法呢?
通过上图可以看到由于father_A和Swizz_A指向的是同一个实现,所以交换也没有意义,

e 这里为什么还要调用class_replaceMethod方法呢,好像不调也可以,子类的father_A方法已经指向了Swizz_A,子类调用father_A,相当于调用Swizz_A,也就[son father_A]等同于[son Swizz_A];

在Swizz_A方法调用[super father_A],这个确实可以,就是写法和上面的写法不一样了,上面是
-(void)Swizz_A{
[self father_A];
NSLog(@"son分类里的Swizz_A");
}

-(void)Swizz_A{
[super father_A];
NSLog(@"son分类里的Swizz_A");
}
如果你能确定你要交换的方法是在父类实现的,也可以这么写。

f 调用class_replaceMethod方法,发生了什么呢?如下图;
技术分享图片

看到了吧,Swizz_A方法名指向了父类的father_A实现,为什么?
class_replaceMethod(class, swizzledSelector, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod))
这里的swizzledSelector就是Swizz_A,method_getImplementation(originalMethod)就是父类的father_A的实现。

上图不对?让我们实验一下
技术分享图片
可以看大指向的是同一个IMP

总结:判断class_addMethod这种写法是比较强壮的写法,如果你确定要交换的方法存在当前类中,可以直接交换。












































以上是关于Objective-C Runtime Method Swizzling 实践的主要内容,如果未能解决你的问题,请参考以下文章

Objective-C Runtime 文档翻译—Runtime版本和平台

Objective-C Runtime(转)

Objective-C Runtime 文档翻译—与Runtime的相互作用

Objective-C Runtime 运行时之三:方法与消息(转载)

Objective-C 中的Runtime的详细使用

Objective-C Runtime Programming Guide 中文翻译