一、runtime简介
- RunTime简称运行时。OC就是
运行时机制
,也就是在运行时候的一些机制,其中最主要的是消息机制。 - 对于C语言,
函数的调用在编译的时候会决定调用哪个函数
。 - 对于OC的函数,属于
动态调用过程
,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。 - 事实证明:
- 在编译阶段,OC可以
调用任何函数
,即使这个函数并未实现,只要声明过就不会报错。 - 在编译阶段,C语言调用
未实现的函数
就会报错。
- 在编译阶段,OC可以
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的知识点,可以参考苹果开发文档进行更深一步得学习: