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 <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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,029评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,238评论 3 388
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,576评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,214评论 1 287
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,324评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,392评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,416评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,196评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,631评论 1 306
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,919评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,090评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,767评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,410评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,090评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,328评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,952评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,979评论 2 351

推荐阅读更多精彩内容