Runtime
2017-08-15 本文已影响14人
AlanGe
一、runtime简介
- RunTime简称运行时。OC就是运行时机制,也就是在运行时候的一些机制,其中最主要的是消息机制。
- 对于C语言,函数的调用在编译的时候会决定调用哪个函数。
- 对于OC的函数,属于动态调用过程,在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
- 事实证明:
- 在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。
- 在编译阶段,C语言调用未实现的函数就会报错。
二、runtime作用
1.发送消息
- 方法调用的本质,就是让对象发送消息。
- objc_msgSend,只有对象才能发送消息,因此以objc开头.
- 使用消息机制前提,必须导入#import <objc/message.h>
- 消息机制简单使用
// Person.h
// Runtime(消息机制)
#import <Foundation/Foundation.h>
@interface Person : NSObject
+ (void)eat;
- (void)run:(int)age;
- (void)eat;
@end
// Person.m
// Runtime(消息机制)
#import "Person.h"
@implementation Person
- (void)eat
{
NSLog(@"对象方法-吃东西");
}
+ (void)eat
{
NSLog(@"类方法-吃东西");
}
- (void)run:(int)age
{
NSLog(@"%d",age);
}
@end
// ViewController.m
// Runtime(消息机制)
#import "ViewController.h"
#import "Person.h"
// 运行时使用运行时的步骤
// 第一步:导入<objc/message.h>
// 第二步:Build Setting -> 搜索msg -> 设置属性为No
#import <objc/message.h>
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建person对象
Person *p = [[Person alloc] init];
// 调用对象方法
[p eat];
// OC:运行时机制,消息机制是运行时机制最重要的机制
// 消息机制:任何方法调用,本质都是发送消息
// SEL:方法编号,根据方法编号就可以找到对应方法实现
// performSelector:动态添加方法
[p performSelector:@selector(eat)];
// 运行时,发送消息,谁做事情就那谁
// xcode5之后,苹果不建议使用底层方法
// xcode5之后,使用运行时.
// 让p发送消息
// 不带参数
objc_msgSend(p, @selector(eat));
// 带参数
objc_msgSend(p, @selector(run:),10);
// 调用类方法的方式:两种
// 第一种通过类名调用,类名调用类方法,本质类名转换成类对象
[Person eat];
// 第二种通过类对象调用
[[Person class] eat];
// 获取类对象
Class personClass = [Person class];
[personClass performSelector:@selector(eat)];
// 运行时
// 用类名调用类方法,底层会自动把类名转换成类对象调用
// 本质:让类对象发送消息
objc_msgSend(personClass, @selector(eat));
}
@end
-
消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现
2.交换方法
- 开发使用场景:系统自带的方法功能不够,给系统自带的方法扩展一些功能,并且保持原有的功能。
- 方式一:继承系统的类,重写方法.
- 方式二:使用runtime,交换方法.
// UIImage+Image.h
// Runtime(交换方法)
#import <UIKit/UIKit.h>
@interface UIImage (Image)
+ (__kindof UIImage *)ge_imageNamed:(NSString *)imageName;
@end
// UIImage+Image.m
// Runtime(交换方法)
#import "UIImage+Image.h"
#import <objc/message.h>
@implementation UIImage (Image)
// 不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super.
// 在分类里面不能调用super,分类木有父类
//+ (UIImage *)imageNamed:(NSString *)name
//{
// [super im]
//}
// 利用运行时
// 先写一个其他方法,实现这个功能
// 既能加载图片又能打印
+ (UIImage *)ge_imageNamed:(NSString *)imageName
{
// 1.加载图片
UIImage *image = [UIImage ge_imageNamed:imageName];
// 2.判断功能
if (image == nil) {
NSLog(@"加载image为空");
}
return image;
}
// 加载这个分类的时候调用
+ (void)load
{
// 交换方法实现,方法都是定义在类里面
// class_getMethodImplementation:获取方法实现
// class_getInstanceMethod:获取对象
// class_getClassMethod:获取类方法
// IMP:方法实现
// imageNamed
// Class:获取哪个类方法
// SEL:获取方法编号,根据SEL就能去对应的类找方法
Method imageNameMethod = class_getClassMethod([UIImage class], @selector(imageNamed:));
// ge_imageNamed
Method ge_imageNamedMethod = class_getClassMethod([UIImage class], @selector(ge_imageNamed:));
// 交换方法实现
method_exchangeImplementations(imageNameMethod, ge_imageNamedMethod);
}
@end
// ViewController.m
// Runtime(交换方法)
#import "ViewController.h"
//#import "UIImage+Image.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 需求:给imageNamed方法提供功能,每次加载图片就判断下图片是否加载成功。
// 步骤一:先搞个分类,定义一个能加载图片并且能打印的方法+ (instancetype)imageWithName:(NSString *)name;
// 步骤二:交换imageNamed和imageWithName的实现,就能调用imageWithName,间接调用imageWithName的实现。
// UIImage *image = [UIImage imageNamed:@"123"];
// 不好的地方
// 1.每次使用,都需要导入头文件
// 2.当一个项目开发太久,使用这个方式不靠谱
// imageNamed:
// 实现方法:底层调用ge_imageNamed
// 本质:交换两个方法的实现imageNamed和ge_imageNamed方法
// 调用imageNamed其实就是调用ge_imageNamed
// 系统imageNamed加载图片,并不知道图片是否加载成功
// 交换以后调用imageNamed的时候,就知道图片是否加载
[UIImage imageNamed:@"123"];
}
@end
- 交换原理:
- 交换之前:
- 交换之前:
3.动态添加方法
- 开发使用场景:如果一个类方法非常多,加载类到内存的时候也比较耗费资源,需要给每个方法生成映射表,可以使用动态给某个类,添加方法解决。
- 经典面试题:有没有使用performSelector,其实主要想问你有没有动态添加过方法。
- 简单使用
// Person.h
// Runtime(动态添加方法)
#import <Foundation/Foundation.h>
@interface Person : NSObject
@end
// Person.m
// Runtime(动态添加方法)
#import "Person.h"
#import <objc/message.h>
@implementation Person
// 动态添加方法,首先实现这个resolveInstanceMethod
// resolveInstanceMethod调用:当调用了没有实现的方法没有实现就会调用resolveInstanceMethod
// resolveInstanceMethod作用:就知道哪些方法没有实现,从而动态添加方法
// sel:没有实现方法
// 当一个对象调用未实现的方法,会调用这个方法处理,并且会把对应的方法列表传过来.
// 刚好可以用来判断,未实现的方法是不是我们想要动态添加的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// NSLog(@"%@",NSStringFromSelector(sel));
// 动态添加eat方法
if (sel == @selector(eat:)) {
// 第一个参数:cls:给哪个类添加方法
// 第二个参数:SEL:添加方法的方法编号是什么
// 第三个参数:IMP:方法实现,函数入口,函数名
// 第四个参数:types:方法类型
// v 表示 void
// @ 表示对象
// : 表示SEL
class_addMethod(self, sel, (IMP)aaaa, "v@:@");
// 处理完
return YES;
}
// 返回系统的方法,因为上面改了,尽量不要修改系统的方法
return [super resolveInstanceMethod:sel];
}
// 默认一个方法都有两个参数,self,_cmd,属于隐式参数
// self:方法调用者
// _cmd:调用方法的编号
// 定义函数
// 没有返回值,参数(id,SEL)
// void(id,SEL)
void aaaa(id self, SEL _cmd, id param1)
{
NSLog(@"调用eat %@ %@ %@",self,NSStringFromSelector(_cmd),param1);
}
@end
// ViewController.m
// Runtime(动态添加方法)
#import "ViewController.h"
#import "Person.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// performSelector:动态添加方法
Person *p = [[Person alloc] init];
// 默认person,没有实现eat方法,可以通过performSelector调用,但是会报错。
// 通过运行时后,动态添加方法就不会报错
// 动态添加方法
// 不带参数
[p performSelector:@selector(eat)];
// 带参数
[p performSelector:@selector(eat:) withObject:@111];
}
@end
4.给分类添加属性
- 原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
// NSObject+Objc.h
// Runtime(分类添加属性)
#import <Foundation/Foundation.h>
@interface NSObject (Objc)
// @property:只会生成set方法的声明,不会实现
@property (nonatomic, strong) NSString *name;
@end
// NSObject+Objc.m
// Runtime(分类添加属性)
#import "NSObject+Objc.h"
#import <objc/message.h>
@implementation NSObject (Objc)
// 定义关联的key
//static NSString *_name;
// set方法
- (void)setName:(NSString *)name {
// 添加属性,跟对象
// 给某个对象产生关联,添加属性
// 第一个参数:object:给哪个对象添加属性
// 第二个参数:key:属性名,根据key去获取关联的对象 ,void * == id
// 第三个参数:value:关联的值
// 第四个参数:policy:缓存策略
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
// get方法
- (NSString *)name {
// 根据关联的key,获取关联的值。
return objc_getAssociatedObject(self, @"name");
}
@end
// ViewController.m
// Runtime(分类添加属性)
#import "ViewController.h"
#import "NSObject+Objc.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view, typically from a nib.
NSObject *objc = [[NSObject alloc] init];
objc.name = @"123";
NSLog(@"%@",objc.name);
}
@end
5.字典转模型
- 设计模型:字典转模型的第一步
- 模型属性,通常需要跟字典中的key一一对应
- 问题:一个一个的生成模型属性,很慢?
- 需求:能不能自动根据一个字典,生成对应的属性。
- 解决:提供一个分类,专门根据字典生成对应的属性字符串。
// NSObject+Property.h
// 自动生成属性代码
// 通过解析字典自动生成属性代码
#import <Foundation/Foundation.h>
@interface NSObject (Property)
/// 通过解析字典自动生成属性代码
+ (void)createPropertyCodeWithDict:(NSDictionary *)dict;
@end
// NSObject+Property.m
// 自动生成属性代码
// 通过解析字典自动生成属性代码
#import "NSObject+Property.h"
@implementation NSObject (Property)
/// 通过解析字典自动生成属性代码
+ (void)createPropertyCodeWithDict:(NSDictionary *)dict {
// 拼接属性字符串代码
NSMutableString *strM = [NSMutableString string];
/*********************** 方法1 ***************************/
// 遍历字典
[dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull propertyName, id _Nonnull value, BOOL * _Nonnull stop) {
// NSLog(@"%@ %@",propertyName,[value class]);
// 类型经常变,抽出来
NSString *code;
if ([value isKindOfClass:NSClassFromString(@"__NSCFString")]) {
code = [NSString stringWithFormat:@"@property (nonatomic, copy) NSString *%@;",propertyName]
;
}else if ([value isKindOfClass:NSClassFromString(@"__NSCFNumber")]){
code = [NSString stringWithFormat:@"@property (nonatomic, assign) int %@;",propertyName]
;
}else if ([value isKindOfClass:NSClassFromString(@"__NSCFArray")]){
code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSArray *%@;",propertyName]
;
}else if ([value isKindOfClass:NSClassFromString(@"__NSCFDictionary")]){
code = [NSString stringWithFormat:@"@property (nonatomic, strong) NSDictionary *%@;",propertyName]
;
}else if ([value isKindOfClass:NSClassFromString(@"__NSCFBoolean")]){
code = [NSString stringWithFormat:@"@property (nonatomic, assign) BOOL %@;",propertyName]
;
}
// 每生成属性字符串,就自动换行。
[strM appendFormat:@"\n%@\n",code];
}];
/*********************** 方法2 ***************************/
// 1.遍历字典,把字典中的所有key取出来,生成对应的属性代码
// [dict enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
// // 类型经常变,抽出来
// NSString *type;
// if ([obj isKindOfClass:NSClassFromString(@"__NSCFString")]) {
// type = @"NSString";
// }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFArray")]){
// type = @"NSArray";
// }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFNumber")]){
// type = @"int";
// }else if ([obj isKindOfClass:NSClassFromString(@"__NSCFDictionary")]){
// type = @"NSDictionary";
// }
// // 属性字符串
// NSString *str;
// if ([type containsString:@"NS"]) {
// str = [NSString stringWithFormat:@"@property (nonatomic, strong) %@ *%@;",type,key];
// }else{
// str = [NSString stringWithFormat:@"@property (nonatomic, assign) %@ %@;",type,key];
// }
// // 每生成属性字符串,就自动换行。
// [strM appendFormat:@"\n%@\n",str];
// }];
// 把拼接好的字符串打印出来,就好了。
NSLog(@"strM = %@",strM);
}
@end
// ViewController.m
// Runtime(自动生成属性代码)
#import "ViewController.h"
#import "NSObject+Property.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 解析Plist
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSArray *dictArr = dict[@"statuses"];
// 设计模型属性代码,生成打印后赋值粘贴到模型即可
[NSObject createPropertyCodeWithDict:dictArr[0]];
}
@end
- 字典转模型的方式一:KVC
// Status.m
// 字典转模型KVC实现
#import "Status.h"
@implementation Status
// 字典转模型 - 模型的属性名跟字典一一对应
+ (Status *)statusWithDict:(NSDictionary *)dict {
Status *status = [[self alloc] init];
// KVC
[status setValuesForKeysWithDictionary:dict];
return status;
}
@end
- KVC字典转模型弊端:必须保证,模型中的属性和字典中的key一一对应。
- 如果不一致,就会调用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:] 报key找不到的错。
- 分析:模型中的属性和字典的key不一一对应,系统就会调用setValue:forUndefinedKey:报错。
- 解决:重写对象的setValue:forUndefinedKey:,把系统的方法覆盖, 就能继续使用KVC,字典转模型了。
// Status.m
// 转模型KVC实现
#import "Status.h"
@implementation Status
// 字典转模型 - 模型的属性名跟字典一一对应
+ (Status *)statusWithDict:(NSDictionary *)dict
{
Status *status = [[self alloc] init];
// KVC
[status setValuesForKeysWithDictionary:dict];
return status;
}
// 解决KVC报错
- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
if ([key isEqualToString:@"id"]) {
_ID = [value integerValue];
}
// key:没有找到key
// value:没有找到key对应的值
NSLog(@"没有找到key = %@,没有找到key对应的值 = %@",key,value);
}
@end
// ViewController.m
// Runtime(字典转模型KVC实现)
#import "ViewController.h"
#import "NSObject+Property.h"
#import "Status.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 解析Plist
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSArray *dictArr = dict[@"statuses"];
// 设计模型属性代码,生成打印后赋值粘贴到模型即可
// [NSObject createPropertyCodeWithDict:dictArr[0]];
NSMutableArray *statuses = [NSMutableArray array];
for (NSDictionary *dict in dictArr) {
// 字典转模型
Status *status = [Status statusWithDict:dict];
[statuses addObject:status];
}
NSLog(@"%@",statuses);
}
@end
- 字典转模型的方式二:Runtime
- 思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。
- 步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类转。
// User.h
// Runtime(字典转模型)
#import <Foundation/Foundation.h>
@interface User : NSObject
@property (nonatomic, copy) NSString *profile_image_url;
@property (nonatomic, assign) BOOL vip;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) int mbrank;
@property (nonatomic, assign) int mbtype;
@end
// User.m
// Runtime(字典转模型)
#import "User.h"
@implementation User
@end
// Status.h
// Runtime(字典转模型)
#import <Foundation/Foundation.h>
@class User;
@interface Status : NSObject
// 写一段程序自动生成属性代码
@property (nonatomic, assign) NSInteger ID;
// 解析字典自动生成属性代码
@property (nonatomic, strong) NSString *source;
@property (nonatomic, assign) NSInteger reposts_count;
@property (nonatomic, strong) NSArray *pic_urls;
@property (nonatomic, strong) NSString *created_at;
@property (nonatomic, assign) int attitudes_count;
@property (nonatomic, strong) NSString *idstr;
@property (nonatomic, strong) NSString *text;
@property (nonatomic, assign) int comments_count;
@property (nonatomic, strong) User *user;
@property (nonatomic, strong) NSDictionary *retweeted_status;
@end
// Status.m
// Runtime(字典转模型)
#import "Status.h"
@implementation Status
@end
// NSObject+Model.h
// Runtime(字典转模型)
// Runtime字典转模型分类
#import <Foundation/Foundation.h>
@interface NSObject (Model)
+ (instancetype)modelWithDict:(NSDictionary *)dict;
@end
// NSObject+Model.m
// Runtime(字典转模型)
// Runtime字典转模型分类
#import "NSObject+Model.h"
#import <objc/message.h>
/*
Ivar ivar1;
Ivar ivar2;
Ivar ivar3;
Ivar a[] = {ivar3,ivar1,ivar2};
Ivar *ivar = &a;
*/
@implementation NSObject (Model)
+ (instancetype)modelWithDict:(NSDictionary *)dict{
// 1.创建对应类的对象
id objc = [[self alloc] init];
// 2.利用runtime给对象中的成员属性赋值
// runtime:遍历模型中所有成员属性,去字典中查找
// 属性定义在哪,定义在类,类里面有个属性列表(数组)
// 遍历模型所有成员属性
// ivar:成员属性
// class_copyIvarList:把成员属性列表复制一份给你
// Ivar *:指向Ivar指针
// Ivar *:指向一个成员变量数组
// class:获取哪个类的成员属性列表
// count:成员属性总数
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(self, &count);
for (int i = 0 ; i < count; i++) {
// 获取成员属性
Ivar ivar = ivarList[i];
// 获取成员名
NSString *propertyName = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 获取key
NSString *key = [propertyName substringFromIndex:1];
// user value:字典
// 获取字典的value
id value = dict[key];
// 给模型的属性赋值
// value:字典的值
// key:属性名
// 获取成员属性类型
NSString *propertyType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];
// user:NSDictionary
// 二级转换
// 值是字典,成员属性的类型不是字典,才需要转换成模型
if ([value isKindOfClass:[NSDictionary class]] && ![propertyType containsString:@"NS"]) {
// 字典转模型
// 获取模型的类对象,调用modelWithDict
// 模型的类名已知,就是成员属性的类型
// 需要字典转换成模型
// 转换成哪个类型
// NSLog(@"转换成哪个类型 = %@",propertyType);
/*********** *********** 字符串截取 *********** ***********/
// 字符串截取
// 生成的是这种@"@\"User\"" 类型 -> @"User" 在OC字符串中 \" -> ",\是转义的意思,不占用字符
// @"@\"User\"" User
// \":算一个字符
NSRange range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringFromIndex:range.location + range.length];
// User\"";
// 裁剪到哪个角标,不包括当前角标
range = [propertyType rangeOfString:@"\""];
propertyType = [propertyType substringToIndex:range.location];
/*********** *********** 字符串截取 *********** ***********/
// 获取需要转换类的类对象
// 根据字符串类名生成类对象
Class modelClass = NSClassFromString(propertyType);
// 有对应的模型才需要转
if (modelClass) {
// 字典转模型
value = [modelClass modelWithDict:value];
}
}
// // 三级转换:NSArray中也是字典,把数组中的字典转换成模型.
// // 判断值是否是数组
// if ([value isKindOfClass:[NSArray class]]) {
// // 判断对应类有没有实现字典数组转模型数组的协议
// if ([self respondsToSelector:@selector(arrayContainModelClass)]) {
// // 转换成id类型,就能调用任何对象的方法
// id idSelf = self;
// // 获取数组中字典对应的模型
// NSString *type = [idSelf arrayContainModelClass][key];
// // 生成模型
// Class classModel = NSClassFromString(type);
// NSMutableArray *arrM = [NSMutableArray array];
// // 遍历字典数组,生成模型数组
// for (NSDictionary *dict in value) {
// // 字典转模型
// id model = [classModel modelWithDict:dict];
// [arrM addObject:model];
// }
// // 把模型数组赋值给value
// value = arrM;
// }
// }
// 有值,才需要给模型的属性赋值
if (value) {
// KVC赋值:不能传空
[objc setValue:value forKey:key];
}
}
return objc;
}
@end
// ViewController.m
// Runtime(字典转模型)
#import "ViewController.h"
#import "Status.h"
#import "User.h"
#import "NSObject+Model.h"
@interface ViewController ()
@end
@implementation ViewController
/*
KVC:遍历字典中所有key,去模型中查找有没有对应的属性名,没有对应的key就会报错
runtime:遍历模型中所有属性名,去字典中查找,这样不会像KVC那样报错
*/
- (void)viewDidLoad {
[super viewDidLoad];
// 解析Plist
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"status.plist" ofType:nil];
NSDictionary *dict = [NSDictionary dictionaryWithContentsOfFile:filePath];
NSArray *dictArr = dict[@"statuses"];
NSMutableArray *statuses = [NSMutableArray array];
// 遍历字典数组
for (NSDictionary *dict in dictArr) {
// Runtime(字典转模型)
Status *status = [Status modelWithDict:dict];
[statuses addObject:status];
User *user = status.user;
NSString *name = user.name;
NSLog(@"name = %@",name);
}
// NSLog(@"%@",statuses);
}
@end