Write in the first【写在最前】
对于从事 iOS 开发人员来说,当提到 ** runtime时,我想都可以说出来 「runtime 运行时
」和基本使用的方法。相信很多开发者跟我当初一样,也许当你使用这个重要的模块完成一些工作任务之后(复制粘贴的^_^.
),还是不清楚 runtime** 知识体系和内在原理。
** runtime** 是 iOS 编程中比较难的模块,想要深入学习 OC,那 ** runtime** 是你必须要熟练掌握的东西,下面是我对 runtime
的整理,从零开始,由浅入深,并且带了几个 runtime
实际开发的应用场景。
本篇文章主要从【runtime 模块实用详解】学习总结。
在「时间和知识 」有限内,总结的文章难免有「未全、不足 」的地方,还望各位好友指出,以提高文章质量。
目录:
runtime 概念
runtime 消息机制
runtime 方法调用流程「消息机制」
runtime 运行时常见作用
runtime 常用开发应用场景「工作掌握」
1.runtime 交换方法
2.runtime 给分类动态添加属性
3.runtime 字典转模型(Runtime 考虑三种情况实现)runtime 运行时其它作用「面试熟悉」
1.动态添加方法
2.实现NSCoding的自动归档和解档
3.runtime 下Class的各项操作
4.runtime 几个参数概念什么是 method swizzling(俗称黑魔法)
最后一道面试题的注解
期待 & 后续
这篇文章较长一些,强烈建议先 ?? 收藏,在进行阅读 !
runtime 概念
Objective-C 是基于 C 的,它为 C 添加了面向对象的特性。它将很多静态语言在编译和链接时期做的事放到了 runtime 运行时来处理,可以说 runtime 是我们 Objective-C 幕后工作者。
runtime(
简称运行时
),是一套 纯C(C和汇编写的) 的API。而 OC 就是 运行时机制,也就是在运行时候的一些机制,其中最主要的是 消息机制。对于 C 语言,函数的调用在编译的时候会决定调用哪个函数。
OC的函数调用成为消息发送,属于 动态调用过程。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。
事实证明:在编译阶段,OC 可以 调用任何函数,即使这个函数并未实现,只要声明过就不会报错,只有当运行的时候才会报错,这是因为OC是运行时动态调用的。而 C 语言 调用未实现的函数 就会报错。
runtime 消息机制
我们写 OC 代码,它在运行的时候也是转换成了 runtime
方式运行的。任何方法调用本质:就是发送一个消息(用 runtime
发送消息,OC 底层实现通过 runtime
实现)。
消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。
每一个 OC 的方法,底层必然有一个与之对应的 runtime
方法。
简单示例:
验证:方法调用,是否真的是转换为消息机制?
必须要导入头文件
#import <objc/message.h>
注解1:我们导入系统的头文件,一般用尖括号。
注解2:OC 解决消息机制方法提示步骤【查找
build setting
-> 搜索msg
->objc_msgSend
(YES --> NO)】注解3:最终生成消息机制,编译器做的事情,最终代码,需要把当前代码重新编译,用xcode编译器,【
clang -rewrite-objc main.m
查看最终生成代码】,示例:cd main.m --> 输入前面指令,就会生成 .opp文件(C++代码)
注解4:这里一般不会直接导入
<objc/runtime.h>
示例代码:OC 方法-->runtime 方法
说明: eat(无参) 和 run(有参) 是 Person模型类中的私有方法「可以帮我调用私有方法」;// Person *p = [Person alloc];// 底层的实际写法Person *p = objc_msgSend(objc_getClass("Person"), sel_registerName("alloc"));// p = [p init];p = objc_msgSend(p, sel_registerName("init"));// 调用对象方法(本质:让对象发送消息)//[p eat];// 本质:让类对象发送消息objc_msgSend(p, @selector(eat)); objc_msgSend([Person class], @selector(run:),20);//--------------------------- <#我是分割线#> ------------------------------//// 也许下面这种好理解一点// id objc = [NSObject alloc];id objc = objc_msgSend([NSObject class], @selector(alloc));// objc = [objc init];objc = objc_msgSend(objc, @selector(init));
runtime 方法调用流程「消息机制」
面试:消息机制方法调用流程
怎么去调用
eat
方法,对象方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class
)中方法列表)。1.OC 在向一个对象发送消息时,
runtime
库会根据对象的isa
指针找到该对象对应的类或其父类中查找方法。。2.注册方法编号(这里用方法编号的好处,可以快速查找)。
3.根据方法编号去查找对应方法。
4.找到只是最终函数实现地址,根据地址去方法区调用对应函数。
补充:一个
objc
对象的isa
的指针指向什么?有什么作用?每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。
runtime 常见作用
动态交换两个方法的实现
动态添加属性
实现字典转模型的自动转换
发送消息
动态添加方法
拦截并替换方法
实现 NSCoding 的自动归档和解档
runtime 常用开发应用场景「工作掌握」
runtime 交换方法
应用场景:当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。
需求:加载一张图片直接用[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
方法里。最后当运行的时候系统的方法就会去找我们的方法的实现。
runtime 给分类动态添加属性
原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
应用场景:给系统的类添加属性的时候,可以使用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
可以做到这一点。
runtime 字典转模型
字典转模型的方式:
一个一个的给模型属性赋值(初学者)。
字典转模型KVC实现
KVC 字典转模型弊端:必须保证,模型中的属性和字典中的
key
一一对应。如果不一致,就会调用
[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]
报key
找不到的错。分析:模型中的属性和字典的
key
不一一对应,系统就会调用setValue:forUndefinedKey:
报错。解决:重写对象的
setValue:forUndefinedKey:
,把系统的方法覆盖,就能继续使用KVC,字典转模型了。字典转模型 Runtime 实现
思路:利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找
key
,取出对应的值,给模型的属性赋值(从提醒:字典中取值,不一定要全部取出来)。考虑情况:
1.当字典的
key
和模型的属性匹配不上。2.模型中嵌套模型(模型属性是另外一个模型对象)。
3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)。
注解:根据上面的三种特殊情况,先是字典的
key
和模型的属性不对应的情况。不对应有两种,一种是字典的键值大于模型属性数量,这时候我们不需要任何处理,因为runtime
是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也当然不会去看了;另外一种是模型属性数量大于字典的键值对,这时候由于属性没有对应值会被赋值为nil
,就会导致crash
,我们只需加一个判断即可。考虑三种情况下面一一注解;步骤:提供一个
NSObject
分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。MJExtension 字典转模型实现
底层也是对
runtime
的封装,才可以把一个模型中所有属性遍历出来。(你之所以看不懂,是 MJ 封装了很多层而已^_^.)。
这里针对字典转模型 KVC 实现,就不做详解了,如果你 对 KVC 详解使用或是实现原理 不是很清楚的,可以参考 实用「KVC编码 & KVO监听
字典转模型 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
{ int _a; // 成员变量}@property (nonatomic, assign) NSInteger attitudes_count; // 属性这里有成员变量,就不会漏掉属性;如果有属性,可能会漏掉成员变量;
使用runtime
字典转模型获取模型属性名的时候,最好获取成员属性名Ivar
因为可能会有个属性是没有setter
和``getter方法的。
2、runtime 字典转模型-->模型中嵌套模型「模型属性是另外一个模型对象」,这种情况处理如下:
+ (instancetype)modelWithDict2:(NSDictionary *)dict { // 1.创建对应的对象 id objc = [[self alloc] init]; // 2.利用runtime给对象中的属性赋值 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)]; // 获取成员变量类型 NSString *ivarType = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)]; // 替换: @\"User\" -> User ivarType = [ivarType stringByReplacingOccurrencesOfString:@"\"" withString:@""]; ivarType = [ivarType stringByReplacingOccurrencesOfString:@"@" withString:@""]; // 处理成员属性名->字典中的key(去掉 _ ,从第一个角标开始截取) NSString *key = [ivarName substringFromIndex:1]; // 根据成员属性名去字典中查找对应的value id value = dict[key]; //--------------------------- <#我是分割线#> ------------------------------// // // 二级转换:如果字典中还有字典,也需要把对应的字典转换成模型 // 判断下value是否是字典,并且是自定义对象才需要转换 if ([value isKindOfClass:[NSDictionary class]] && ![ivarType hasPrefix:@"NS"]) { // 字典转换成模型 userDict => User模型, 转换成哪个模型 // 根据字符串类名生成类对象 Class modelClass = NSClassFromString(ivarType); if (modelClass) { // 有对应的模型才需要转 // 把字典转模型 value = [modelClass modelWithDict2:value]; } } // 给模型中属性赋值 if (value) { [objc setValue:value forKey:key];
标签: iOS模式, runtime运行时, runtime面试, Runloop, iOS, OC, 程序员, 简书, 白开水ln
http://www.cnblogs.com/Plainboiledwater/p/jianshu-baikaishuiln.html