Runtime简介以及常见的使用场景(此内容非原创,为转载内容)

Runtime简称运行时,是一套比较底层的纯C语言的API, 作为OC的核心,运行时是一种面向对象的编程语言的运行环境,其中最主要的是消息机制,Objective-C 就是基于运行时的。

所谓运行时,是指尽可能地把决定从编译期推迟到运行期,就是尽可能地做到动态.只是在运行的时候才会去确定对象的类型和方法的.因此利用Runtime机制可以在程序运行时动态地修改类和对象中的所有属性和方法。

对于C语言,函数的调用在编译的时候会决定调用哪个函数。对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

 

Objective-C 从三种不同的层级上与 Runtime 系统进行交互,分别是

①通过 Objective-C 源代码;

②通过 Foundation 框架的NSObject类定义的方法;

③通过对 Runtime 函数的直接调用(需要导入#import <objc/runtime.h>);

大部分情况下只管写OC代码就行,因为OC底层默认实现Runtime,每一个OC的方法,底层必然有一个与之对应的Runtime方法。。

 

以下是Runtime的一些使用场景:

1.发送消息

方法调用的本质,就是让对象发送消息。objc_msgSend,只有对象才能发送消息

举个简单的例子:如下

1).调用对象方法:
// 创建Dog对象
Dog *dog = [[Dog alloc] init];

// 调用对象方法
[dog run];
// 调用对象本质:让对象发送消息
objc_msgSend(dog, @selector(run));

//(2).调用类方法方式:
//有两种
// 第一种通过类名调用
[Dog run];
// 第二种通过类对象调用
[[Dog class] run];
// 用类名调用类方法,底层会自动把类名转换成类对象调用
// 调用类方法本质:让类对象发送消息
objc_msgSend([Dog class], @selector(run));

消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现

2.动态添加方法

开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。简单使用

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    Person *p = [[Person alloc] init];
    
    // 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。
    // 动态添加方法就不会报错
    [p performSelector:@selector(eat)];
    
}


@end

在person类.m中:

@implementation Person
// void(*)()
// 默认方法都有两个隐式参数,
void eat(id self,SEL sel)
{
    NSLog(@"%@ %@",self,NSStringFromSelector(sel));
}

// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
    
    if (sel == @selector(eat)) {
        // 动态添加eat方法
        
        // 第一个参数:给哪个类添加方法
        // 第二个参数:添加方法的方法编号
        // 第三个参数:添加方法的函数实现(函数地址)
        // 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
        class_addMethod(self, @selector(eat), eat, "v@:");
        
    }
    
    return [super resolveInstanceMethod:sel];
}
@end

注意:

+ (BOOL) resolveInstanceMethod:(SEL)aSEL 这个函数与forwardingTargetForSelector类似,都会在对象不能接受某个selector时触发,执行起来略有差别。前者的目的主要在于给客户一个机会来向该对象添加所需的selector,后者的目的在于允许用户将selector转发给另一个对象。另外触发时机也不完全一 样,该函数是个类函数,在程序刚启动,界面尚未显示出时,就会被调用。

在类不能处理某个selector的情况下,如果类重载了该函数,并使用class_addMethod添加了相应的selector,并返回YES,那么后面forwardingTargetForSelector 就不会被调用,如果在该函数中没有添加相应的selector,那么不管返回什么,后面都会继续调用 forwardingTargetForSelector,如果在forwardingTargetForSelector并未返回能接受该 selector的对象,那么resolveInstanceMethod会再次被触发,这一次,如果仍然不添加selector,程序就会报异常

3.运行时关联对象提高效率,给分类添加属性。

使用的时候与懒加载的特点相似,从`关联对象`中获取对象属性,如果有,直接返回。

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.

    // 给系统NSObject类动态添加属性name
    NSObject *objc = [[NSObject alloc] init];
    objc.name = @"旺财";
    NSLog(@"%@",objc.name);
}

@end
// 定义关联的key
static const char *key = "name";

@implementation NSObject (Property)

- (NSString *)name
{
    // 根据关联的key,获取关联的值。
    NSString*name = objc_getAssociatedObject(self, key);
    if (name!= nil) {
        return name;
    }
}
- (void)setName:(NSString *)name
{
    // 第一个参数:给哪个对象添加关联
    // 第二个参数:关联的key,通过这个key获取
    // 第三个参数:关联的value
    // 第四个参数:关联的策略
    objc_setAssociatedObject(self, key, name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

@end

4.使用运行时字典转模型

大体思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类转。(所有字典转模型框架的核心算法)

创建NSObject的分类Runtime:

在.h中的类方法如下:
#import <Foundation/Foundation.h>

@interface NSObject (Runtime)

///  给定一个字典,创建 self 类对应的对象
///
///  @param dict 字典
///
///  @return 对象
+ (instancetype)hd_objWithDict:(NSDictionary *)dict;

///  获取类的属性列表数组
///
///  @return 类的属性列表数组
+ (NSArray *)hd_objProperties;

@end


在.m中的类方法如下:

// 所有字典转模型框架的核心算法
+ (instancetype)hd_objWithDict:(NSDictionary *)dict {
    // 实例化对象
    id object = [[self alloc] init];
    
    // 使用字典,设置对象信息
    // 1> 获得 self 的属性列表
    NSArray *proList = [self cz_objProperties];
    
    // 2> 遍历字典
    [dict enumerateKeysAndObjectsUsingBlock:^(id  _Nonnull key, id  _Nonnull obj, BOOL * _Nonnull stop) {
        
        NSLog(@"key %@ --- value %@", key, obj);
        // 3> 判断 key 是否在 proList 中
        if ([proList containsObject:key]) {
            //  说明属性存在,可以使用 `KVC` 设置数值
            [object setValue:obj forKey:key];
        }
    }];
    return object;
}

const char * kPropertiesListKey = "CZPropertiesListKey";

+ (NSArray *)hd_objProperties {
    
    // --- 1. 从`关联对象`中获取对象属性,如果有,直接返回!
    /**
     获取关联对象 - 动态添加的属性
     参数:
     1. 对象 self
     2. 动态属性的 key
     返回值
     动态添加的`属性值`
     */
    NSArray *ptyList = objc_getAssociatedObject(self, kPropertiesListKey);
    if (ptyList != nil) {
        return ptyList;
    }
    
    // 调用运行时方法,取得类的属性列表
    // Ivar 成员变量
    // Method 方法
    // Property 属性
    // Protocol 协议
    /**
     参数
     1. 要获取的类
     2. 类属性的个数指针
     
     返回值
     所有属性的`数组`,C 语言中,数组的名字,就是指向第一个元素的地址
     
     retain/create/copy 需要 release,最好 option + click
     */
    unsigned int count = 0;
    objc_property_t *proList = class_copyPropertyList([self class], &count);
    
    NSLog(@"属性的数量 %d", count);
    // 创建数组
    NSMutableArray *arrayM = [NSMutableArray array];
    
    // 遍历所有的属性
    for (unsigned int i = 0; i < count; i++) {
        
        // 1. 从数组中取得属性
        /**
         C 语言的结构体指针,通常不需要 `*`
         */
        objc_property_t pty = proList[i];
        
        // 2. 从 pty 中获得属性的名称
        const char *cName = property_getName(pty);
        
        NSString *name = [NSString stringWithCString:cName encoding:NSUTF8StringEncoding];
        
//        NSLog(@"%@", name);
        // 3. 属性名称添加到数组
        [arrayM addObject:name];
    }
    
    // 释放数组
    free(proList);
    
    // --- 2. 到此为止,对象的属性数组已经获取完毕,利用关联对象,动态添加属性
    /**
     参数
     
     1. 对象 self [OC 中 class 也是一个特殊的对象]
     2. 动态添加属性的 key,获取值的时候使用
     3. 动态添加的属性值
     4. 对象的引用关系
     */
    objc_setAssociatedObject(self, kPropertiesListKey, arrayM.copy, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
    
    return arrayM.copy;
}

注意:必须保证,模型中的属性和字典中的key一一对应。 如果不一致,就会调用[ setValue:forUndefinedKey:],报key找不到的错。 
分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,
就能继续使用KVC,字典转模型了。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{

}
通过运行时字典转模型的好处在于写在NSObject的分类中,和类的关联性不强对类解耦,以后再做字典转模型的时候只需要把这个分类往任何一个程序中一拖,程序中的对象就都具备了这个字典转模型的方法。

5.交叉方法(黑魔法)

开发使用场景:系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。方式一:继承系统的类,重写方法.方式二:使用runtime,交换方法.

RuntimeAFN中的使用细节:在AFNNSURLSessionMangerM方法里面第363行写了一个静态的内联函数,做了一个交叉方法,交叉的是af_resumeresume方法,这样的话,可以在发送网络之前发起一个通知,能接受到任何一个网络请求的事件的变化。

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    // 需求:给imageNamed方法提供功能,每次加载图片就判断下图片是否加载成功。
    // 步骤一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
    // 步骤二:交换imageNamed和imageWithName的实现,就能调用imageWithName,间接调用imageWithName的实现。
    UIImage *image = [UIImage imageNamed:@"123"];
}
@end

@implementation UIImage (Image)
// 加载分类到内存的时候调用
+ (void)load
{
    // 交换方法
    
    // 获取imageWithName方法地址
    Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));
    
    // 获取imageWithName方法地址
    Method imageName = class_getClassMethod(self, @selector(imageNamed:));
    
    // 交换方法地址,相当于交换实现方式
    method_exchangeImplementations(imageWithName, imageName);
    
}

// 既能加载图片又能打印
+ (instancetype)imageWithName:(NSString *)name
{
    
    // 这里调用imageWithName,相当于调用imageName
    UIImage *image = [self imageWithName:name];
    
    if (image == nil) {
        NSLog(@"加载空的图片");
    }
    
    return image;
}


@end

 

posted @ 2016-08-21 10:43  ReShadow  阅读(266)  评论(0编辑  收藏  举报