美文网首页编写高质量代码的52个有效方法
52个有效方法(13) - 用“方法调配技术”调试“黑盒方法”

52个有效方法(13) - 用“方法调配技术”调试“黑盒方法”

作者: SkyMing一C | 来源:发表于2018-09-03 16:34 被阅读8次

    方法调配技术也就是方法交换技术。

    //获取类的实例方法 返回一个Method对象
    class_getInstanceMethod(Class obj, SEL cmd)
    //获取类的类方法 返回一个Method对象
    class_getClassMethod(Class obj, SEL cmd)
    //添加一个新的方法和该方法的具体实现
    OBJC_EXPORT BOOL class_addMethod(Class cls, SEL name, IMP imp,  const char *types) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0);
    //Class cls 你要添加新方法的那个类
    //SEL name 要添加的方法
    //IMP imp 指向实现方法的指针   就是要添加的方法的实现部分
    //const char *types 我们要添加的方法的返回值和参数 method_getTypeEncoding(Method _Nonnull m) OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);
    
    //替换方法的实现
    method_exchangeImplementations(method1, method2)
    
    交换对象方法
    //.h
    #import <UIKit/UIKit.h>
    
    @interface UIActionSheet(Swizzling)
    
    @end
    #import "UIActionSheet+Swizzling.h"
    
    @implementation UIActionSheet(Swizzling)
    
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            //获取class
            Class class = [self class];
            // 获取原有SEL
            SEL originSelector = @selector(dismissWithClickedButtonIndex:animated:);
            // 获取交换SEL
            SEL newSelector = @selector(swizzleDismissWithClickedButtonIndex:animated:);
            // 获取原有方法
           Method oriMethod = class_getInstanceMethod(class, originSelector);
            // 获取交换方法
            Method newMethod = class_getInstanceMethod(class, newSelector);
            // 注意:不能直接交换方法实现,需要判断原有方法是否存在,存在才能交换
            // 如何判断?添加原有方法,如果成功,表示原有方法不存在,失败,表示原有方法存在
            // 原有方法可能没有实现,所以这里添加方法实现,用自己方法实现
            // 这样做的好处:方法不存在,直接把自己方法的实现作为原有方法的实现,调用原有方法,就会来到当前方法的实现
            BOOL isAddedMethod = class_addMethod(class, originSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
            if (isAddedMethod) {
            // 如果 class_addMethod 成功了,说明之前 class 里并不存在 originSelector,所以要用一个空的方法代替它,以避免 class_replaceMethod 后,后续这个方法被调用时可能会 crash。
                class_replaceMethod(class, newSelector, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
            } else {
                method_exchangeImplementations(oriMethod, newMethod);
            }
        });
    }
    
    -(void)swizzleDismissWithClickedButtonIndex:(NSInteger)buttonIndex animated:(BOOL)animated{
        //这个方法是准备和`dismissWithClickedButtonIndex:animated:`这儿方法互换的。在运行期,`swizzleDismissWithClickedButtonIndex:animated: `选择子实际上对应于原有的`dismissWithClickedButtonIndex:animated:`方法的实现
        [self swizzleDismissWithClickedButtonIndex:buttonIndex animated:animated];
        //修复keyWindow 不正确的问题
        [[UIApplication sharedApplication].delegate.window makeKeyWindow];
    }
    @end
    
    • 一般情况下,类别里的方法会重写掉主类里相同命名的方法。如果有两个类别实现了相同命名的方法,只有一个方法会被调用。但 +load:是个特例,当一个类被读到内存的时候, runtime 会给这个类及它的每一个类别都发送一个+load:消息。使用GCD的dispatch_once可以保证交换两个实例方法的实现只进行一次。
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
           //do something...
        });
    }
    
    • 不能直接交换方法实现,需要判断原有方法是否存在,存在才能交换。
    • 如何判断?添加原有方法,如果成功,表示原有方法不存在,失败,表示原有方法存在。
    • 原有方法可能没有实现,所以这里添加方法实现,用自己方法实现。
    • 这样做的好处:方法不存在,直接把自己方法的实现作为原有方法的实现,调用原有方法,就会来到当前方法的实现。
            //获取class
            Class class = [self class];
            // 获取原有SEL
            SEL originSelector = @selector(dismissWithClickedButtonIndex:animated:);
            // 获取交换SEL
            SEL newSelector = @selector(swizzleDismissWithClickedButtonIndex:animated:);
            // 获取原有方法
           Method oriMethod = class_getInstanceMethod(class, originSelector);
            // 获取交换方法
            Method newMethod = class_getInstanceMethod(class, newSelector);
            BOOL isAddedMethod = class_addMethod(class, originSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
            if (isAddedMethod) {
                // 如果 class_addMethod 成功了,说明之前 class 里并不存在 originSelector,所以要用一个空的方法代替它,以避免 class_replaceMethod 后,后续这个方法被调用时可能会 crash。
                class_replaceMethod(class, newSelector, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
            } else {
                method_exchangeImplementations(oriMethod, newMethod);
            }
    
    • swizzleDismissWithClickedButtonIndex:animated:是准备和dismissWithClickedButtonIndex:animated:这儿方法互换的。
    • 在运行期,swizzleDismissWithClickedButtonIndex:animated:选择子实际上对应于原有的dismissWithClickedButtonIndex:animated:方法的实现。
    -(void)swizzleDismissWithClickedButtonIndex:(NSInteger)buttonIndex animated:(BOOL)animated{
        //这个方法是准备和`dismissWithClickedButtonIndex:animated:`这儿方法互换的。在运行期,`swizzleDismissWithClickedButtonIndex:animated: `选择子实际上对应于原有的`dismissWithClickedButtonIndex:animated:`方法的实现
        [self swizzleDismissWithClickedButtonIndex:buttonIndex animated:animated];
        //修复keyWindow 不正确的问题
        [[UIApplication sharedApplication].delegate.window makeKeyWindow];
    }
    
    交换类方法
    //.h
    #import <Foundation/Foundation.h>
    
    @interface UIFont (DefaultSize)
    
    @end
    //.m
    #import "UIFont+DefaultSize.h"
    #import <objc/runtime.h>
    @implementation UIFont (DefaultSize)
    
    + (void)load {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            //获取class
            Class class = object_getClass((id)self);
            // 获取原有SEL
            SEL originSelector = @selector(preferredFontForTextStyle:);
            // 获取交换SEL
            SEL newSelector = @selector(swizzlePreferredFontForTextStyle:);
            // 获取原有方法
            Method oriMethod = class_getClassMethod(class, originSelector);
            // 获取交换方法
            Method newMethod = class_getClassMethod(class, newSelector);
            // 注意:不能直接交换方法实现,需要判断原有方法是否存在,存在才能交换
            // 如何判断?添加原有方法,如果成功,表示原有方法不存在,失败,表示原有方法存在
            // 原有方法可能没有实现,所以这里添加方法实现,用自己方法实现
            // 这样做的好处:方法不存在,直接把自己方法的实现作为原有方法的实现,调用原有方法,就会来到当前方法的实现
            BOOL isAddedMethod = class_addMethod(class, originSelector, method_getImplementation(newMethod), method_getTypeEncoding(newMethod));
            if (isAddedMethod) {
                // 如果 class_addMethod 成功了,说明之前 class 里并不存在 originSelector,所以要用一个空的方法代替它,以避免 class_replaceMethod 后,后续这个方法被调用时可能会 crash。
                class_replaceMethod(class, newSelector, method_getImplementation(oriMethod), method_getTypeEncoding(oriMethod));
            } else {
                method_exchangeImplementations(oriMethod, newMethod);
            }
        });
    }
    
    + (UIFont *)swizzlePreferredFontForTextStyle:(UIFontTextStyle)style{
        return [UIFont systemFontOfSize:17];
    }
    
    @end
    
    • 类方法获取class实例对象方法为Class class = object_getClass((id)self);,通过object_getClass获取到的是类的元类。对象方法获取class实例对象方法为Class class = [self class];
      创建对象的类本身也是对象,称为类对象,类对象中存放的是描述实例相关的信息,例如实例的成员变量,实例方法。类对象里的isa指针指向Subclass(meta),Subclass(meta)也是一个对象,是原类对象,原类对象中存放的是描述类相关的信息,例如类方法。

    • 类方法获取方法为class_getClassMethod对象方法获取方法为class_getInstanceMethod

    要点
    1. 在运行期,可以向类中新增或替换选择子所对应的方法实现。

    2. 使用另一份实现来替换原有的方法实现,这道工序叫做“方法调配”,开发者常用此技术向原有实现中添加新功能。

    3. 一般来说,只有调试程序的时候才需要在运行期修改方法实现,这种做法不宜滥用。

    相关文章

      网友评论

        本文标题:52个有效方法(13) - 用“方法调配技术”调试“黑盒方法”

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