美文网首页iOS开发
iOS中Method swizzling的使用

iOS中Method swizzling的使用

作者: 小盟城主 | 来源:发表于2019-03-01 17:25 被阅读1次

    一、Method Swizzling简介

      Method Swizzling被业内称为黑魔法、黑科技。字面意思是方法交换,其中交换的是方法的实现
    具体点的来说,我们用@selector(方法选择器) 取出来的是一个方法的编号(指向方法的指针) ,用SEL类型表示;它所指向的是一个IMP(方法实现的指针) ,而我们交换的就是这个IMP,从而达到方法实现交换的效果。

    • @selector(方法名)

    @selector()方法名称的描述,只取类方法的编号不记录具体的方法,具体的方法是 IMP,取出的结果是SEL类型。

    (1)编译时,通过编译器指令@selector 来获取.

    //定义一个类方法的指针,selector查找是当前类(包含子类)的方法
    SEL aSelector = @selector(methodName);
    

    (2)运行时,通过字符串来获取一个方法名 NSSelectorFromString

    SEL aSelector = NSSelectorFromString(@"methodName");
    

    Objective-C 数据结构中,存在一个name - selector 的映射表如图:

    The Selector table

      方法以 selector 作为索引。selector的数据类型是SEL。虽然SEL 定义成 char*,我们可以把它想象成int。每个方法的名字对应一个唯一的 int 值。比如, 方法 addObject:可能 对应的是 12。 当寻找该方法时,使用的是 selector,而不是名字 @"addObject:"

    在编译的时候,只要有方法的调用,编译器都会通过 selector 来查找,所以 (假设addObjectselector12)

    [myObject addObject: param];
    

    将会编译变成:

    objc_msgSend(myObject, 12, param);
    

      这里,objc_msgSend()函数将会使用 myObjectisa 指针来找到 myObject 的类空间结构,并在类空间结构中查找 selector 12所对应的方法。如果没有找到,那么将使用指向父类的指针找到父类空间结构进行 selector 12 的查找。 如果仍然没有找到,就继续往父类的父类一 直找,直到找到为止, 如果到了根类NSObject中仍然找不到,将会抛出异常。

    • IMP:(Implementation缩写)

    一个函数指针,保存了方法地址。
    (1)它是指向一个方法具体实现的指针,每一个方法都有一个对应的IMP,所以,我们可以直接调用方法的IMP指针,来避免方法调用死循环的问题。

    (2)当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由IMP这个函数指针指向了这个方法实现的。
    IMP的声明为:

    typedef id(*IMP)(id, SEL,...);
    

    IMP是一个函数指针,这个被指向的函数包含一个接收消息的对象id(self指针),调用方法的选标SEL(方法名),以及不定个数的方法参数,并返回一个id。也就是说IMP是消息最终调用的执行代码,是方法真正的实现代码 。我们可以像在语言里面一样使用这个函数指针。

    //Method 是一个类实例,里面的结构体有一个方法选标 SEL – 表示该方法的名称,一个types – 表示该方法参数的类型,一个 IMP  - 指向该方法的具体实现的函数指针。
    typedef struct objc_method *Method;
    typedef struct objc_ method {
        SEL method_name;    //方法名
        char *method_types; //方法类型
        IMP method_imp;    //具体方法实施的指针
    };
    //获取了这个实例方法类Mehtod
    Method method = class_getInstanceMethod([self class], NSSelectorFromString(@"dealloc")); 
    //通过实例方法类获取对应的地址IMP
    IMP classResumeIMP = method_getImplementation(method); 
    

    二、Method Swizzling原理

    Method Swizzing是发生在运行时的,主要用于在运行时将两个Method进行交换,我们可以将Method Swizzling代码写到任何地方,但是只有在这段Method Swilzzling代码执行完毕之后互换才起作用。

    • 每一个OC实例对象都保存有isa指针和实例变量,其中isa指针所属类,类维护一个运行时可接收的方法列表(MethodLists);方法列表(MethodLists)中保存selector的方法名和方法实现(IMP,指向Method实现的指针)的映射关系。在运行时,通过selecter找到匹配的IMP,从而找到的具体的实现函数。
      MethodLists图
    • 开发中可以利用Objective-C的动态特性,在运行时替换selector对应的方法实现(IMP),达到给hook的目的。下图是利用Method Swizzling来替换selector对应IMP后的方法列表示意图。
      hook后的MethodLists示意图

    三、Method Swizzling的具体用途AOP(面向切面编程)

      举个例子,比如说:在所有页面添加统计功能,也就是用户进入这个页面就统计一次。

    方案:

    (1) 手动添加
    直接简单粗暴的在每个控制器中加入统计,复制、粘贴、复制、粘贴…
    上面这种方法太Low了,消耗时间而且以后非常难以维护,会让后面的开发人员骂死的。

    (2)继承
    我们可以使用继承的方式来解决这个问题。创建一个基类,在这个基类中添加统计方法,其他类都继承自这个基类。
    然而,这种方式修改还是很大,而且定制性很差。以后有新人加入之后,都要嘱咐其继承自这个基类,所以这种方式并不可取。

    (3)Category
    我们可以为UIViewController建一个Category,然后在所有控制器中引入这个Category。当然我们也可以添加一个PCH文件,然后将这个Category添加到PCH文件中。

    (4)Method Swizzling
    我们可以使用苹果的“黑魔法”Method SwizzlingMethod Swizzling本质上就是对IMPSEL进行交换。

    Method Swizzling的常见用途:

    1. 页面统计(AOP)、NSMutableArrayinsert等插入nilhook:給全局图片名称添加前缀,分类中为已有的属性或者方法添加钩子(增加一段代码)。
    2. 用于记录或者存储,比方说记录ViewController进入次数、Btn的点击事件、ViewController的停留时间等等。 可以通过Runtime获取到具体ViewControllerBtn信息,然后传给服务器。
    3. 添加需要而系统没提供的方法,比方说修改Statusbar颜色。用于轻量化、模块化处理。
    4. Method Swizzle动态给指定的方法添加代码,以解决Cross-cutting concern的编程方式叫做Aspect Oriented Programming,将逻辑处理和事件记录的代码解耦。
    5. AOP可以把琐碎的事务从主逻辑中分离出来,作为单独的模块,它是对面向对象编程模式的一种补充。
    6. 比较好的AOP库,封装了runtimeMethod Swizzling这些黑科技,该库只有两个API
    + (id<AspectToken>)aspect_hookSelector:(SEL)selector
                                      withOptions:(AspectOptions)options
                                       usingBlock:(id)block
                                            error:(NSError **)error;
          - (id<AspectToken>)aspect_hookSelector:(SEL)selector
                                      withOptions:(AspectOptions)options
                                       usingBlock:(id)block
                                            error:(NSError **)error;
    

    注意要点

    • swizzling 应该只在 +load 中完成。

    • swizzling 应该只在 dispatch_once 中完成。由于 swizzling 改变了全局的状态,所以我们需要确保每个预防措施在运行时都是可用的。原子操作就是这样一个用于确保代码只会被执行一次的预防措施,就算是在不同的线程中也能确保代码只执行一次。Grand Central Dispatchdispatch_once 满足了所需要的需求,并且应该被当做使用swizzling 的初始化单例方法的标准。

    • 尝试先调用class_addMethod方法,以保证即便originalSelector只在父类中实现,也能达到Method Swizzling的目的。

    • xxx_viewWillAppear:方法中[self xxx_viewWillAppear:animated];代码并不会造成死循环,因为Method Swizzling之后,调用[self xxx_viewWillAppear:animated];实际执行的代码已经是原来viewWillAppear中的代码了。

    (1) +load的执行时机:+load 方法会在加载类的时候就被调用,也就是iOS 应用启动的时候,就会加载所有的类,main函数之前,就会调用每个类的 +load 方法

    (2) 子类的+load方法会在它的所有父类(不包括父类分类中的+load)的+load方法执行之后执行
    分类的+load方法会在所有的主类的+load方法执行之后执行

    (3) 不同的类之间的+load方法的调用顺序是不确定的

    (4) + initialize:方法类似一个懒加载,initialize是在类或者其子类的第一个方法被调用前调用,且默认只加载一次;+ initialize的调用发生在+init 方法之前。

    +load+ initialize差异比较

    +(void)load +(void)initialize
    执行时机 在main函数之前执行 在类的方法被第一次调用时执行
    若自身未定义,是否沿用父类的方法?
    类别中的定义 全都执行,但后于类中的方法 覆盖类中的方法,只执行一次

    swizzling代码如下

    NSObject+Swizzling.m

    #import "NSObject+Swizzling.h"
    #import <objc/runtime.h>
    
    @implementation NSObject (Swizzling)
    
    /**
     交换两个对象方法的实现
     
     @param srcClass 被替换方法的类
     @param srcSel 被替换的方法编号
     @param swizzledSel 用于替换的方法编号
     */
    + (void)ff_swizzleInstanceMethodWithSrcClass:(Class)srcClass
                                          srcSel:(SEL)srcSel
                                     swizzledSel:(SEL)swizzledSel{
        
        Method srcMethod = class_getInstanceMethod(srcClass, srcSel);
        Method swizzledMethod = class_getInstanceMethod(srcClass, swizzledSel);
        if (!srcClass || !srcMethod || !swizzledMethod) return;
        
        //加一层保护措施,如果添加成功,则表示该方法不存在于本类,而是存在于父类中,不能交换父类的方法,否则父类的对象调用该方法会crash;添加失败则表示本类存在该方法
        BOOL addMethod = class_addMethod(srcClass, srcSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
        if (addMethod){
            //添加方法实现IMP成功后,再将原有的实现替换到swizzledMethod方法上,从而实现方法的交换,并且未影响到父类方法的实现
            class_replaceMethod(srcClass, swizzledSel, method_getImplementation(srcMethod), method_getTypeEncoding(srcMethod));
        }else{
            //添加失败,调用交互两个方法的实现
            method_exchangeImplementations(srcMethod, swizzledMethod);
        }
    }
    
    @end
    

    交换例子

    #import "UIViewController+swizzling.h"
    #import <objc/runtime.h>
    #import "NSObject+Swizzling.h"
    
    @implementation UIViewController (swizzling)
    
    + (void)load {
        [super load];
        
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            Class class = [self class];
            // 原方法名和替换方法名
            SEL originalSelector = @selector(viewDidAppear:);
            SEL swizzledSelector = @selector(swizzle_viewDidAppear:);
            
            [NSObject ff_swizzleInstanceMethodWithSrcClass:class srcSel:originalSelector swizzledSel:swizzledSelector];
        });
    }
    
    /**
     页面出现的时候会进入到这里实现,即使在子类重写了viewDidAppear:方法,
     那么在调用[super viewDidAppear:animated]的时候还是会进入这里。
     */
    - (void)swizzle_viewDidAppear:(BOOL)animated {
        //cmd在Objective-C的方法中表示当前方法的selector,正如同self表示当前方法调用的对象实例一样。
        NSLog(@"%@ --- %@ (IMP = UIViewController swizzle_viewDidAppear)",self, NSStringFromSelector(_cmd));
        //这里面调用自己在method swizzle交换了方法实现后就不会出现循环了
        //swizzle_viewDidAppear:方法的实现其实是调用UIViewController的viewDidAppear的原生实现方法
        [self swizzle_viewDidAppear:animated];
    }
    
    @end
    

    这里只是简单介绍了一下正确swizzle的思路,还有很多不完善的地方,推荐大家去看RSSwizzleswizzle思路也是动态查找父类的实现,但它的实现方式十分优雅,非常值得一看。最后,Method swizzling虽然能给我们带来很多便捷,但是调式困难,以及使用不当带来的麻烦也是难以排查,所以还是谨慎使用。

    相关文章

      网友评论

        本文标题:iOS中Method swizzling的使用

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