iOS-Runtime

一、runtime简介

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

Runtime 在项目中常用到的几种作用在接下来会一一介绍并附上完整demo地址,而且有详细的注释

二、runtime作用

1.发送消息
  • 方法调用的本质,就是让对象发送消息。
  • objc_msgSend,只有对象才能发送消息,因此以objc开头.
  • 使用消息机制前提,必须导入 #import < objc/message.h>
    这里写图片描述
  • 消息机制简单使用
//发送消息
/**

 使用Runtime首先:
 1.导入头文件: #import <objc/message.h>
 2.项目target中 -> Build Setting -> 搜索msg -> 设置属性为No

 本质让对象发送消息  objc_msgSend(<#id self#>, <#SEL op, ...#>) :第一个参数:谁发送这个消息,第二个参数:发送哪个消息

 */
-(void)sendMessage{
    Car *car = [[Car alloc] init];

    //普通-调用对象方法
    [car run];
    //Runtime-调用对象方法
    objc_msgSend(car, @selector(run));

    //普通-调用带参数对象方法
    [car run:100];
    //Runtime-调用带参数对象方法
    objc_msgSend(car, @selector(run:),1000);

    //普通-调用类方法
    [Car run];
    //Runtime-调用类方法   类名调用类方法,本质类名转换成类对象
    objc_msgSend([Car class], @selector(run));

    //普通-调用带参数类方法
    [Car run:200];
    //Runtime-调用带参数类方法
    objc_msgSend([Car class], @selector(run:),2000);
}
2.交换方法
  • 开发使用场景:系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。
  • 方式一:继承系统的类,重写方法.
  • 方式二:使用runtime,交换方法.
@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);


}

// 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super.

// 既能加载图片又能打印
+ (instancetype)imageWithName:(NSString *)name
{

    // 这里调用imageWithName,相当于调用imageName
    UIImage *image = [self imageWithName:name];

    if (image == nil) {
        NSLog(@"加载空的图片");
    }

    return image;
}


@end
3.动态添加方法
  • 开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。
  • 经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。
**
 *  Runtime 动态添加方法
 */
-(void)dynamicMethod{
    Car *car = [[Car alloc] init];
    //通过performSelector调用未声明的,但是会报错。
    //动态添加方法就不会报错
    [car performSelector:@selector(eat:) withObject:@"馒头"];
}

/**
 *  默认一个方法都有两个参数,self,_cmd,隐式参数
 *  self:方法调用者
 *  _cmd:调用方法的编号
 */
void dynamicMethod(id self, SEL _cmd, id param){
    NSLog(@"动态添加的方法 eat 的:%@",param);
}

// 动态添加方法,首先实现这个resolveInstanceMethod
// resolveInstanceMethod调用:当调用了没有实现的方法没有实现就会调用resolveInstanceMethod
// resolveInstanceMethod作用:就知道哪些方法没有实现,从而动态添加方法
// sel:没有实现对象方法
+(BOOL)resolveInstanceMethod:(SEL)sel{

    if (sel == @selector(eat:)) {

        //动态添加eat方法
        /**
         *  class_addMethod(<#__unsafe_unretained Class cls#>, <#SEL name#>, <#IMP imp#>, <#const char *types#>)
         *  cls:给哪个类添加方法
         *  name:添加的方法的方法编号是什么
         *  imp:方法实现,函数入口,函数名
         *  types:方法类型 函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
         */
        class_addMethod(self, sel, (IMP)dynamicMethod, "v@:@");

        return YES;
    }

    return [super resolveInstanceMethod:sel];
}
4.给分类添加属性
  • 原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
/**
 *  Runtime 分类动态添加属性
 *  属性的含义即将其指针指到特定的一块存储空间

 如果在分类中仅仅声明一个属性而不实现,在使用这个属性会报错:
 '-[Car setName:]: unrecognized selector sent to instance 0x7fde4d940c30'

 所以必须实现自定义的这个属性的get方法和set方法

 一种方法是使用 static 定义全局变量,实现其 get / set 方法:

 #import "Car+Cool.h"

 @implementation Car (Cool)

 static NSString *_name;

 -(void)setName:(NSString *)name{
 _name = name;
 }

 -(NSString *)name{
 return _name;
 }

 @end

 另外即使用Runtime给分类动态添加属性

 */
-(void)categoryAddPropery{
    Car *car = [[Car alloc] init];
    car.name = @"BMW";
    NSLog(@"CAR 的名称: %@",car.name);
}
#import "Car+Cool.h"
#import <objc/message.h>

@implementation Car (Cool)

//全局存在,影响内存 - 不推荐
//static NSString *_name;

-(void)setName:(NSString *)name{
//    _name = name;


    /**
     *  跟所添加的属性产生关联
     *  objc_setAssociatedObject(<#id object#>, <#const void *key#>, <#id value#>, <#objc_AssociationPolicy policy#>)
     *
     *  @param object 给哪个对象添加属性
     *  @param key   属性名,根据key获取关联对象 void * == id
     *  @param value  属性名所关联的值
     *  @param policy  关联的策略
     */
    objc_setAssociatedObject(self, @"CarName", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

-(NSString *)name{
//    return _name;

    /**
     *  根据关联的key 获取所关联的值
     *  objc_getAssociatedObject(<#id object#>, <#const void *key#>)
     *
     *  @param object 给哪个对象所添加的属性
     *  @param key    属性名,可根据此key 值获取其之前所关联的值
     *
     */
    return objc_getAssociatedObject(self, @"CarName");
}
@end
5.字典转模型
  • 字典转模型的方式一:KVC
@implementation Status


+ (instancetype)statusWithDict:(NSDictionary *)dict
{
    Status *status = [[self alloc] init];

    [status setValuesForKeysWithDictionary:dict];

    return status;

}

@end
  • KVC字典转模型弊端:必须保证,模型中的属性和字典中的key一一对应。
    • 如果不一致,就会调用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]
      key找不到的错。
    • 分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
    • 解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖,
      就能继续使用KVC,字典转模型了。
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
    //在此处处理属性和字典中key不对应的情况
     if ([key isEqualToString:@"id"]) {
        _ID = [value integerValue];
    }
    // key:没有找到key
    // value:没有找到key对应的值
    NSLog(@"%@ %@",key,value);

}
  • 字典转模型的方式二:Runtime
    • 思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。
    • 步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类转。
      .h
#import <Foundation/Foundation.h>

@interface NSObject (Model)

/**
 *  根据字典转模型
 *
 *  @param dict 字典
 *
 *  @return 模型
 */
+(instancetype)modelWithDict:(NSDictionary *)dict;

@end

.m

#import "NSObject+Model.h"
#import <objc/message.h>
@implementation NSObject (Model)
+(instancetype)modelWithDict:(NSDictionary *)dict{
    //1.创建
    id objc = [[self alloc] init];

    //获取本类中所有属性列表
    /*
     * class_copyIvarList(<#__unsafe_unretained Class cls#>, <#unsigned int *outCount#>)
     * 遍历当前对象(模型)中所有成员属性
     * class_copyIvarList:把成员属性列表复制一份给你
     * Ivar *:指向一个成员变量数组,指向Ivar指针
     * Class cls :获取哪个类的成员属性列表
     * outCount :成员属性总数
     */
    unsigned int count = 0;
    Ivar *ivarList = class_copyIvarList(self, &count);
    //解析获取到的成员属性列表
    for (int i = 0; i < count; i++) {
        //每个成员属性
        Ivar oneProperty = ivarList[i];
        //成员属性名称
        NSString *onePropertyName = [NSString stringWithUTF8String:ivar_getName(oneProperty)];
        //成员属性类型
        NSString *onePropertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(oneProperty)];
        //获取字典的key,以便得到value给模型赋值
        NSString *key = [onePropertyName substringFromIndex:1];
        //当前可以所对应的value
        id value = dict[key];
        //二级转换,在解析过程中可能会遇到多层解析的情况,即字典中某一value值也为字典,此时需要进行模型的二级转换,同样利用Runtime的方式
        if ([value isKindOfClass:[NSDictionary class]] && ![onePropertyType containsString:@"NS"]) {
            //此时可确定模型中某一成员属性为另一模型而并非NSDictionary
            //使用递归算法,将其转化为另一自定义模型
            //此时打印的  onePropertyType 为 @"User" 需要进行截取 获得最终的类名
            onePropertyType = [onePropertyType substringFromIndex:2];
            onePropertyType = [onePropertyType substringToIndex:(onePropertyType.length-1)];
            Class customModel =  NSClassFromString(onePropertyType);
            if (customModel) {
                value = [customModel modelWithDict:value];
            }
        }
        //给模型当前属性赋值
        if (value) {
            [objc setValue:value forKey:key];
        }
//        NSLog(@"%@",objc);
    }

    return objc;
}
@end

解析方法:

/**
 *  利用runtime将字典转化为模型
 *  新建NSObject的分类 利用Runtime将字典转化为模型
 */
-(void)runtimeToModel{
    NSDictionary *fileDict = [NSDictionary dictionaryWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil]];
    NSArray *fileArr = [fileDict objectForKey:@"statuses"];
    NSLog(@"%@",fileArr);

    //将数据数组转化成模型数组
    __block NSMutableArray *modelArrM = [NSMutableArray array];

    [fileArr enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        NSDictionary *dict = (NSDictionary *)obj;
        Status *status = [Status modelWithDict:dict];
        [modelArrM addObject:status];
    }];

    NSLog(@"%@",modelArrM);
}

其中在使用Runtime进行字典转模型的过程中,可能会出现模型中某一属性也为模型,所以在解析字典时进行多级转化,类似于递归算法,层层解析.

整个关于Runtime介绍的完整demo下载地址:
https://github.com/qxuewei/XWRuntimeDemo

更多关于Runtime的知识点,可以参考苹果开发文档进行更深一步得学习:
这里写图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值