美文网首页
iOS Runtime实践

iOS Runtime实践

作者: 小小土豆dev | 来源:发表于2018-06-02 18:11 被阅读0次

    本文主要介绍Runtime四种使用情况:

    1、交换方法

    2、动态添加方法

    3、动态添加属性

    4、日志统计

    Objective-C 是面向运行时的语言(runtime oriented language),就是说它会尽可能的把编译和链接时要执行的逻辑延迟到运行时。这就给了你很大的灵活性,你可以按需要把消息重定向给合适的对象,你甚至可以交换方法的实现,等等。

    RunTime简称运行时。就是系统在运行的时候的一些机制,其中最主要的是消息机制。OC的函数调用成为消息发送。属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。而C语言在编译阶段就会报错)。OC只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

    Objective-C 的 Runtime 是一个运行时库(Runtime Library),它是一个主要使用 C 和汇编写的库,为 C 添加了面相对象的能力并创造了 Objective-C。这就是说它在类信息(Class information) 中被加载,完成所有的方法分发,方法转发,等等。Objective-C runtime 创建了所有需要的结构体,让 Objective-C 的面相对象编程变为可能。

    以下面的代码为例:

    [obj makeText];

    其中obj是一个对象,makeText是一个函数名称。对于这样一个简单的调用。在编译时RunTime会将上述代码转化成

    objc_msgSend(obj,@selector(makeText));

    首先,编译器将代码[obj makeText];转化为objc_msgSend(obj, @selector (makeText));,在objc_msgSend函数中。首先通过obj的isa指针找到obj对应的class。在Class中先去cache中通过SEL查找对应函数method(猜测cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),若cache中未找到。再去methodList中查找,若methodList中未找到,则去superClass中查找。若能找到,则将method加入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。

    Demo地址:runtime


    Method Swizzling

    Method Swizzling也称苹果的“黑魔法”,本质上就是对IMP和SEL进行交换。

    Method Swizzling原理

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

    我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP

    我们可以利用 class_replaceMethod 来修改类

    我们可以利用 method_setImplementation 来直接设置某个方法的IMP

    归根结底,都是偷换了selector的IMP

    而且Method Swizzling也是iOS中AOP(面相切面编程)的一种实现方式,我们可以利用苹果这一特性来实现AOP编程。


    RunTime实践

    1. 交换方法

    iOS中有很多可变容器,如:NSMutableArray。在调用可变数组的addObject方法时,如果我们加入了一个nil对象,就会crash。所以在添加数据之前,我们就需要对数据做判空处理。同样,在拿数据时,经常也会遇到角标越界的问题,所以在拿数据之前,需要对角标进行判断,这样太麻烦了,有了runtime我们就可以这样解决这个问题。

    Runtime的思想就是交换方法,把系统的方法和我们自定义的方法进行交换,然后我们在定义的方法里,对数据进行处理。

    1、先定义自己的方法

    给NSMutableArray新建一个类别,.m文件实现:

    #import "NSMutableArray+safe.h"

    #import <objc/runtime.h>

    @implementation NSMutableArray (safe)

    + (void)load {

    }

    @end

    代码看起来可能有点奇怪,像递归不是么。当然不会是递归,因为在 runtime 的时候,函数实现已经被交换了。我们会将safeAddObject方法和系统的addObject方法交换。当你调用addObject方法时,其实调用的是safeAddObject。当调用safeAddObject方法时,调用的是系统的addObject方法。

    - (void)safeAddObject:(id)anObject {

      if(anObject) {

        [self safeAddObject:anObject];

      }else{

        NSLog(@"obj is nil");

      }

    }

    2、使用Method Swizzing交换两个方法

    + (void)load {  

    static dispatch_once_t oneToken;  

    dispatch_once(&oneToken, ^{   

    id obj = [[self alloc] init];   

    [obj swizzleMethod:@selector(addObject:) withMethod:@selector(safeAddObject:)];   

    });

    }

    - (void)swizzleMethod:(SEL)origSelector withMethod:(SEL)newSelector {

      Class class = [self class];

      Method originalMethod = class_getInstanceMethod(class, origSelector);

      Methods wizzledMethod = class_getInstanceMethod(class, newSelector);

      BOOL didAddMethod = class_addMethod(class,

                                          origSelector

                                          method_getImplementation(swizzledMethod),

                                          method_getTypeEncoding(swizzledMethod));

      if(didAddMethod) {

        class_replaceMethod(class,

                            newSelector,

                            method_getImplementation(originalMethod),

                            method_getTypeEncoding(originalMethod));

      }else{

        method_exchangeImplementations(originalMethod, swizzledMethod);

      }

    }

    class_addMethod 。要先尝试添加原 selector 是为了做一层保护,因为如果这个类没有实现 originalSelector ,但其父类实现了,那 class_getInstanceMethod 会返回父类的方法。这样 method_exchangeImplementations 替换的是父类的那个方法,这当然不是你想要的。所以我们先尝试添加 orginalSelector ,如果已经存在,再用 method_exchangeImplementations 把原方法的实现跟新的方法实现给交换掉。

    3、调用方法

    - (void)viewDidLoad {

    NSMutableArray *array = [NSMutableArray array];

    [array addObject:nil]; //->obj is nil

    [array objectAtIndex:3];//->index is beyond bounds

    }


    2. 动态添加方法

    1、新建一个类,命名为Student,.m文件实现以下代码:

    #import "Student.h"

    #import <objc/runtime.h>

    @implementation Student

    void study(id self, SEL sel) {// 要添加的方法

        NSLog(@"%@ %@",self, NSStringFromSelector(sel));

    }

    // 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.

    // 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法

    +(BOOL)resolveInstanceMethod:(SEL)sel {

        if (sel == NSSelectorFromString(@"study")) {

            // 动态添加study方法

            // 第一个参数:给哪个类添加方法

            // 第二个参数:添加方法的方法编号

            // 第三个参数:添加方法的函数实现(函数地址)

            // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd

            class_addMethod(self, sel, (IMP)study,"v@:");

        }

        return [super resolveInstanceMethod:sel];

    }

    @end

    2、调用方法

    - (void)viewDidLoad {

      Student *p = [[Student alloc] init];

      [p performSelector:@selector(eat)];

    }


    3. 动态添加属性

    场景一:给系统类动态添加属性

    有这样一种情况,在一个页面有很多模块,每个模块里又有很多Button,当点击button时,我们想知道是点击了哪个模块里的哪个button。button有自带的tag属性,可以标示button的唯一性,但是这里我们还想知道是哪个模块里的button,此时我就想希望button还有个模块属性,类似tableview里的section和row的关系一样。不用苦恼,我们可以利用runtime给button动态添加一个我们自己定义的属性。(当然,我们也可以)

    第一步:给UIButton新建一个类别文件 .h文件 实现代码:

    @interfaceUIButton (property)

    // 在列别中定义属性,只有声明方法,没有实现方法,直接访问属性会报错

    @property (strong, nonatomic) NSString *section;

    @end

    .m文件 实现代码:

    #import "UIButton+property.h"

    #import <objc/runtime.h>

    // 定义关联的key

    static const char *key = "section";

    @implementationUIButton (property)

    - (NSString*)section{

      // 根据关联的key,获取关联的值。

      return objc_getAssociatedObject(self, key);

    }

    - (void)setSection:(NSString*)section{

      // 第一个参数:给哪个对象添加关联

      // 第二个参数:关联的key,通过这个key获取

      // 第三个参数:关联的value

      // 第四个参数:关联的策略

      objc_setAssociatedObject(self, key, section, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

    }

    @end

    第二步:调用

    UIButton *button = [[UIButton alloc] init];

    button.section =@"1";

    NSLog(@"%@", button.section);

    如果我们希望给系统所有的类都添加一个属性,可以给NSObject新建类别文件,实现动态添加属性。

    场景二:给自定义的类动态添加属性

    有这样的场景,我们使用别人的类,然后再页面中间,我们会传递这个类的实例,用来传值,此时根据情况,我们需要添加一个属性,但这个属性,知识临时用一下,我们没有必要去修改别人的代码。同样,我们也可以利用runtime来动态添加一个临时的属性。

    具体代码和上面的给系统类添加属性类似,可以参考具体代码:runtime实践


    4. 日志统计

    有时候市场同事会提出,他们想知道产品具体某个页面的的打开次数,这就要求,某个页面打开,我会要告知一下后台。

    方法一:让所有的Controller继承自一个BaseController。我们在BaseController的生命周期方法(viewDidLoad)里去写向后台请求的代码。这就要求所有的类必须继承一个Base,如果项目已经开发,并且你没有这么做,那么,你的改动就大了。

    方法二:使用AOP加runtime实现。

    1、给UIViewController新建一个类别并且实现 .m 文件:

    #import "UIViewController+track.h"

    #import <objc/runtime.h>

    @implementationUIViewController (track)

    /*

     创建一个Category来覆盖系统方法,系统会优先调用Category中的代码,然后在调用原类中的代码

     */

    + (void)load {

        [super load];

        // 通过class_getInstanceMethod()函数从当前对象中的method list获取method结构体,如果是类方法就使用class_getClassMethod()函数获取。

        Method fromMethod =class_getInstanceMethod([self class],@selector(viewDidLoad));

        Method toMethod =class_getInstanceMethod([self class],@selector(swizzlingViewDidLoad));

        /**

         *  我们在这里使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。

         *  而且self没有交换的方法实现,但是父类有这个方法,这样就会调用父类的方法,结果就不是我们想要的结果了。

         *  所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了。

         */

        if (!class_addMethod([self class], @selector(viewDidLoad), method_getImplementation(toMethod), method_getTypeEncoding(toMethod))) {

            method_exchangeImplementations(fromMethod, toMethod);

        }

    }

    // 我们自己实现的方法,也就是和self的viewDidLoad方法进行交换的方法。

    - (void)swizzlingViewDidLoad {

        NSString *str = [NSString stringWithFormat:@"%@", self.class];

        // 我们在这里加一个判断,将系统的UIViewController的对象剔除掉

        if(![str containsString:@"UI"]){

            NSLog(@"页面统计 : %@",self.class);

        }

        [self swizzlingViewDidLoad];

    }

    @end

    相关文章

      网友评论

          本文标题:iOS Runtime实践

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