iOS runtime 详解

一、runtime 简介

OC是一门动态语言,所以它总想办法把一些决定工作从编译推迟到运行时。也就是说只有编译器是不够的,它还需要一个运行时系统来执行编译后的代码。这就是Runtime系统存在的意义,它是整个OC的一个基石。

Runtime基本是用C和汇编语言写的,可见苹果为动态系统的高效做出的努力。

Runtime库主要做下面几件事:

封装:在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。
找出方法的最终执行代码:当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。这将在后面详细介绍。
 
二、runtime相关概念
头文件 <objc/runtime>  <objc/message>(包含了runtime)
Method  :成员方法
Ivar    :  成员变量
 
三、runtime 方法调用流程「消息机制」

消息机制方法调用流程

怎么去调用类方法和实例方法,实例方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class)中方法列表)。
1.OC在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象对应的或其父类中查找方法。
2.注册方法编号(这里用方法编号的好处,可以快速查找)。
3.根据方法编号去查找对应方法。
4.找到只是最终函数实现地址,根据地址去方法区调用对应函数。

四、runtime使用场景
 
1、给类别添加属性

原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。

应用场景:给系统的类添加属性的时候,可以使用runtime动态添加属性方法。
注解:系统 NSObject 添加一个分类,我们知道在分类中是不能够添加成员属性的,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过runtime就可以做到给它方法的实现。

需求:给系统 NSObject 类动态添加属性 name 字符串。

 

@interface NSObject (Property)

// @property分类:只会生成get,set方法声明,不会生成实现,也不会生成下划线成员属性
@property NSString *name;
@property NSString *height;
@end

@implementation NSObject (Property)

- (void)setName:(NSString *)name {

    // objc_setAssociatedObject(将某个值跟某个对象关联起来,将某个值存储到某个对象中)
    // object:给哪个对象添加属性
    // key:属性名称
    // value:属性值
    // policy:保存策略
    objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

- (NSString *)name {
    return objc_getAssociatedObject(self, @"name");
}

// 调用
NSObject *objc = [[NSObject alloc] init];
objc.name = @"123";
NSLog(@"runtime动态添加属性name==%@",objc.name);

// 打印输出
2017-02-17 19:37:10.530 runtime[12761:543574] runtime动态添加属性--name == 123

总结:给属性赋值的本质其实就是让属性与一个对象产生关联,所以要个NSObject的分类的name属性赋值就是让name和NSObject产生关联,runtime可以做到这一点。

2、方法交换
应用场景:当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。
需求:加载一张图片直接用[UIImage imageNamed:@"image"];是无法知道到底有没有加载成功。给系统的imageNamed添加额外功能(是否加载图片成功)。
方案一:继承系统的类,重写方法.(弊端:每次使用都需要导入)
方案二:使用 runtime,交换方法.
实现步骤:
<1>给系统的方法添加分类
<2>自己实现一个带有扩展功能的方法
<3>交换方法,只需要交换一次
- (void)viewDidLoad {
    [super viewDidLoad];
    // 方案一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
    // 方案二:交换 imageNamed 和 ln_imageNamed 的实现,就能调用 imageNamed,间接调用 ln_imageNamed 的实现。
    UIImage *image = [UIImage imageNamed:@"123"];
}

#import <objc/message.h> 
@implementation UIImage (Image)
/**
 load方法: 把类加载进内存的时候调用,只会调用一次
 方法应先交换,再去调用
 */
+ (void)load {

    // 1.获取 imageNamed方法地址
    // class_getClassMethod(获取某个类的方法)
    Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
    // 2.获取 ln_imageNamed方法地址
    Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));

    // 3.交换方法地址,相当于交换实现方式;「method_exchangeImplementations 交换两个方法的实现」
    method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
}

/**
 看清楚下面是不会有死循环的
 调用 imageNamed => ln_imageNamed
 调用 ln_imageNamed => imageNamed
 */
// 加载图片 且 带判断是否加载成功
+ (UIImage *)ln_imageNamed:(NSString *)name {

    UIImage *image = [UIImage ln_imageNamed:name];
    if (image) {
        NSLog(@"runtime添加额外功能--加载成功");
    } else {
        NSLog(@"runtime添加额外功能--加载失败");
    }
    return image;
}

/**
 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super
 所以第二步,我们要 自己实现一个带有扩展功能的方法.
 + (UIImage *)imageNamed:(NSString *)name {

 }
 */
@end

// 打印输出
2017-02-17 17:52:14.693 runtime[12761:543574] runtime添加额外功能--加载成功 
  总结:我们交换两个方法地址指向,必须在系统的imageNamed:方法调用前,所以讲代码卸载分类的load方法中,最后当运行的时候系统的方法就会去找我们的方法的实现。
3、动态添加方法

应用场景:如果一个类的方法非常多,加载类到内存的时候比较耗资源,需要给每个方法生成映射表,可以使用动态给某个类添加方法解决。

注解:OC中使用的懒加载,当用到的时候才去加载它,实际上只要一个类实现了某个方法,就会被加载到内存。当我们不想加载那么多方法的时候,就可以使用runtime动态的添加方法。

需求:runtime动态添加方法处理调用一个未实现的方法和去除报错。

- (void)viewDidLoad {
    [super viewDidLoad];   
    Person *p = [[Person alloc] init];
    // 默认person,没有实现run:方法,可以通过performSelector调用,但是会报错。
    // 动态添加方法就不会报错
    [p performSelector:@selector(run:) withObject:@10];
}

@implementation Person
// 没有返回值,1个参数
// void,(id,SEL)
void aaa(id self, SEL _cmd, NSNumber *meter) {
    NSLog(@"跑了%@米", meter);
}

// 任何方法默认都有两个隐式参数,self,_cmd(当前方法的方法编号)
// 什么时候调用:只要一个对象调用了一个未实现的方法就会调用这个方法,进行处理
// 作用:动态添加方法,处理未实现
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    // [NSStringFromSelector(sel) isEqualToString:@"run"];
    if (sel == NSSelectorFromString(@"run:")) {
        // 动态添加run方法
        // class: 给哪个类添加方法
        // SEL: 添加哪个方法,即添加方法的方法编号
        // IMP: 方法实现 => 函数 => 函数入口 => 函数名(添加方法的函数实现(函数地址))
        // type: 方法类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
        class_addMethod(self, sel, (IMP)aaa, "v@:@");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}
@end

// 打印输出
2017-02-17 19:05:03.917 runtime[12761:543574] runtime动态添加方法--跑了10米


4.字典转模型

字典转模型的方式:

  • 一个一个给模型属性赋值

  • 字典转模型KVC实现
    1、KVC字典转模型弊端:必须保证,模型中的属性和字典中的key一一对应
    2、如果不一致,就会调用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:] 报key找不到的错。
    3、分析:模型中的属性和字典中的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
    4、解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,就能继续使用KVC字典转模型。

  • 字典转模型Runtime实现
    思路:利用运行时,遍历模型中的所有属性,根据模型中的属性名,去字典中查找key,取出对应的值,给模型的属性赋值(注:字典中的取值,不一定会全部取出来)。

考虑情况:
1、当字典中的key 和模型的属性匹配不上。
2、模型中嵌套模型(模型属性是另一个模型对象)。
3、模型的属性是一个数组,数组中是一个个模型对象。

注解:字典中的key和模型的属性不对应的情况有两种,一种是字典的键值对大于模型的属性数量,这时候我们不需要任何处理,因为runtime是先遍历模型所有属性,再去字典中根据属性名找对应的值进行赋值,多余的键值对不需要去看;另外一种情况是模型属性数量大于字典中的键值对,这时候由于属性没有对应值会被赋值为nil,就会导致crash,只需加一个判断即可。

实现步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。

MJExtension字典转模型实现也是通过底层对runtime进行封装,才可以把模型中所有属性遍历出来。

字典转模型Runtime方式实现

1、runtime字典转为模型 -- 字典中的key和模型的属性不匹配(模型属性数量大于字典键值对),代码如下:

  
/ Runtime:根据模型中属性,去字典中取出对应的value给模型属性赋值
// 思路:遍历模型中所有属性->使用运行时
+ (instancetype)modelWithDict:(NSDictionary *)dict
{
    // 1.创建对应的对象
    id objc = [[self alloc] init];

    // 2.利用runtime给对象中的属性赋值
    /**
     class_copyIvarList: 获取类中的所有成员变量
     Ivar:成员变量
     第一个参数:表示获取哪个类中的成员变量
     第二个参数:表示这个类有多少成员变量,传入一个Int变量地址,会自动给这个变量赋值
     返回值Ivar *:指的是一个ivar数组,会把所有成员属性放在一个数组中,通过返回的数组就能全部获取到。
     count: 成员变量个数
     */
    unsigned int count = 0;
    // 获取类中的所有成员变量
    Ivar *ivarList = class_copyIvarList(self, &count);

    // 遍历所有成员变量
    for (int i = 0; i < count; i++) {
        // 根据角标,从数组取出对应的成员变量
        Ivar ivar = ivarList[i];

        // 获取成员变量名字
        NSString *ivarName = [NSString stringWithUTF8String:ivar_getName(ivar)];

        // 处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取)
        NSString *key = [ivarName substringFromIndex:1];

        // 根据成员属性名去字典中查找对应的value
        id value = dict[key];

        // 【如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil】
        // 而报错 (could not set nil as the value for the key age.)
        if (value) {
            // 给模型中属性赋值
            [objc setValue:value forKey:key];
        }

    }

    return objc;
}
这里在获取模型类中的所有属性名,是采取 class_copyIvarList 先获取成员变量(以下划线开头) ,然后再处理成员变量名->字典中的key(去掉 _ ,从第一个角标开始截取) 得到属性名。
原因:
Ivar:成员变量,以下划线开头Property 属性  
获取类里面属性 class_copyPropertyList  
获取类中的所有成员变量 class_copyIvarList

 
 
 
 
 
 
 
posted @ 2020-02-13 16:58  蜗牛叔叔  阅读(873)  评论(0编辑  收藏  举报