iOS Runtime实践

本文主要介绍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

@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

@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

// 定义关联的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

@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实践)