引导
runtime是运行时,对于从事iOS开发,想要深入学习OC的人,runtime是必须熟悉掌握的东西。
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:这里一般不会直接导入
示例代码:OC 方法–>runtime 方法
|
|
runtime 方法调用流程「消息机制」
消息机制方法调用流程
怎么去调用类方法和实例方法,实例方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class)中方法列表)。
1.OC在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象对应的类或其父类中查找方法。
2.注册方法编号(这里用方法编号的好处,可以快速查找)。
3.根据方法编号去查找对应方法。
4.找到只是最终函数实现地址,根据地址去方法区调用对应函数。
一个objc 对象的 isa 的指针指向什么?有什么作用?
每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。
runtime 常见作用
- 动态交换两个方法的实现
- 动态添加属性
- 实现字典转模型的自动转换
- 发送消息
- 动态添加方法
- 拦截并替换方法
- 实现 NSCoding 的自动归档和解档
runtime 常用开发应用场景「工作掌握」
runtime 交换方法
应用场景:当第三方框架 或者 系统原生方法功能不能满足我们的时候,我们可以在保持系统原有方法功能的基础上,添加额外的功能。
需求:加载一张图片直接用[UIImage imageNamed:@”image”];是无法知道到底有没有加载成功。给系统的imageNamed添加额外功能(是否加载图片成功)。
方案一:继承系统的类,重写方法.(弊端:每次使用都需要导入)
方案二:使用 runtime,交换方法.
实现步骤:
1.给系统的方法添加分类
2.自己实现一个带有扩展功能的方法
3.交换方法,只需要交换一次
|
|
总结:我们交换两个方法地址指向,必须在系统的imageNamed:方法调用前,所以讲代码卸载分类的load方法中,最后当运行的时候系统的方法就会去找我们的方法的实现。
runtime给分类动态添加属性
原理:给一个类声明属性,其实本质就是给这个类添加关联,并不是直接把这个值的内存空间添加到类存空间。
应用场景:给系统的类添加属性的时候,可以使用runtime动态添加属性方法。
注解:系统 NSObject 添加一个分类,我们知道在分类中是不能够添加成员属性的,虽然我们用了@property,但是仅仅会自动生成get和set方法的声明,并没有带下划线的属性和方法实现生成。但是我们可以通过runtime就可以做到给它方法的实现。
需求:给系统 NSObject 类动态添加属性 name 字符串。
案例代码:方法+调用+打印
|
|
总结:给属性赋值的本质其实就是让属性与一个对象产生关联,所以要个NSObject的分类的name属性赋值就是让name和NSObject产生关联,runtime可以做到这一点。
runtime字典转模型
字典转模型的方式:
一个一个给模型属性赋值
字典转模型KVC实现
1、KVC字典转模型弊端:必须保证,模型中的属性和字典中的key一一对应
2、如果不一致,就会调用[<Status 0x7fa74b545d60> setValue:forUndefinedKey:]
报key
找不到的错。
3、分析:模型中的属性和字典中的key
不一一对应,系统就会调用setValue:forUndefinedKey:
报错。
4、解决:重写对象的setValue:forUndefinedKey:
,把系统的方法覆盖,就能继续使用KVC字典转模型。字典转模型Runtime实现
思路:利用运行时,遍历模型中的所有属性,根据模型中的属性名,去字典中查找key
,取出对应的值,给模型的属性赋值(注:字典中的取值,不一定会全部取出来)。考虑情况:
1、当字典中的key
和模型的属性匹配不上。
2、模型中嵌套模型(模型属性是另一个模型对象)。
3、模型的属性是一个数组,数组中是一个个模型对象。注解:字典中的
key
和模型的属性不对应的情况有两种,一种是字典的键值对大于模型的属性数量,这时候我们不需要任何处理,因为runtime
是先遍历模型所有属性,再去字典中根据属性名找对应的值进行赋值,多余的键值对不需要去看;另外一种情况是模型属性数量大于字典中的键值对,这时候由于属性没有对应值会被赋值为nil
,就会导致crash
,只需加一个判断即可。实现步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。
MJExtension字典转模型实现也是通过底层对runtime进行封装,才可以把模型中所有属性遍历出来。
字典转模型Runtime方式实现
1、runtime字典转为模型 – 字典中的key和模型的属性不匹配(模型属性数量大于字典键值对),代码如下:
|
|
使用runtime
字典转模型获取模型属性名的时候,最好获取成员属性名Ivar
因为可能会有个属性是没有setter
和getter
方法的。
2、runtime字典转模型–模型中嵌套模型(模型属性是另外一个模型对象),代码如下:
|
|
动态变量控制
现在有一个Person的类,创建xiaoming对象
动态获取xiaoming类中的所有属性(包括私有的)
Ivar *ivar = class_copyIvarList([self.xiaoming class], &count);
遍历属性找到对应的name字段
const char *varName = ivar_getName(var);
修改对应的字段值为20
object_setIvar(self.xiaoMing, var, @"20");
代码
1234567891011121314-(void)answer{unsigned int count = 0;Ivar *ivar = class_copyIvarList([self.xiaoMing class], &count);for (int i = 0; i<count; i++) {Ivar var = ivar[i];const char *varName = ivar_getName(var);NSString *name = [NSString stringWithUTF8String:varName];if ([name isEqualToString:@"_age"]) {object_setIvar(self.xiaoMing, var, @"20");break;}}NSLog(@"XiaoMing's age is %@",self.xiaoMing.age);}
####实现NSCoding的自动归档和解档
实现自定义的模型持久化的过程,如果一个模型有许多个属性,需要对每个属性都实现一遍encodeObject
和 decodeObjectForKey
方法,当遇到这样的模型有很多个,这是一件十分麻烦的事情,下面介绍简单的实现方法。
假设现在有一个Movie类,有3个属性。先看下 .h文件
|
|
优化:上面是encodeWithCoder 和 initWithCoder这两个方法抽成宏。我们可以把这两个宏单独放到一个文件里面,这里以后需要进行数据持久化的模型都可以直接使用这两个宏。
runtime下Class的各项操作
下面是 runtime 下Class的常见方法 及 带有使用示例代码。各项操作,【转载原著】http://www.jianshu.com/p/46dd81402f63
unsigned int count;
- 获取属性列表
|
|
- 获取方法列表
|
|
- 获取成员变量列表
|
|
- 获得协议列表
|
|
现在有一个Person类,和person创建的xiaoming对象,有test1和test2两个方法
- 获得类方法
|
|
- 获得实例方法
|
|
- 添加方法
|
|
- 替换原方法实现
|
|
- 交换原方法实现
|
|
常用方法
|
|
runtime 几个参数概念
1、objc_msgSend
这是个最基本的用于发送消息的函数
其实编译器会根据情况在objc_msgSend
, objc_msgSend_stret
,objc_msgSendSuper
, 或objc_msgSendSuper_stret
四个方法中选择一个来调用。如果消息是传递给超类,那么会调用名字带有Super
的函数;如果消息返回值是数据结构而不是简单值时,那么会调用名字带有stret
的函数。
2、SELobjc_msgSend
函数第二个参数类型为SEL,它是selector
在Objc中的表示类型(Swift中是Selector
类)。selector
是方法选择器,可以理解为区分方法的ID
,而这个ID
的数据结构是SEL
:typedef struct objc_selector *SEL
;
其实它就是个映射到方法的C字符串,你可以用Objc
编译器命令@selector()
或者Runtime
系统的sel_registerName
函数来获得一个SEL
类型的方法选择器。
3、idobjc_msgSend
第一个参数类型为id
,大家对它都不陌生,它是一个指向类实例的指针:typedef struct objc_object *id;
那objc_object
又是啥呢:struct objc_object { Class isa; };
objc_object
结构体包含一个isa
指针,根据isa指针就可以顺藤摸瓜找到对象所属的类。
4、runtime.h中Class的定义
|
|
可以看到运行时一个类还关联了它的超类指针,类名,成员变量,方法,缓存,还有附属的协议。
在objc_class结构体中:ivars
是objc_ivar_list
指针;methodLists
是指向objc_method_list
指针的指针。也就是说可以动态修改*methodLists
的值来添加成员方法,这也是Category
实现的原理。
面试题
1、什么是 method swizzling(俗称黑魔法)
简单说就是进行方法交换
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的
每个类都有一个方法列表,存放着方法的名字和方法实现的映射关系,selector的本质其实就是方法名,IMP有点类似函数指针,指向具体的Method实现,通过selector就可以找到对应的IMP。
交换方法的几种实现方式
- 利用 method_exchangeImplementations 交换两个方法的实现
- 利用 class_replaceMethod 替换方法的实现
- 利用 method_setImplementation 来直接设置某个方法的IMP。
参考:
Runtime Method Swizzling开发实例汇总(持续更新中)
OC运行时黑魔法 Method Swizzling
2、下面的代码输出什么?
@implementation Son : NSObject
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
答案:都输出 Son
class
获取当前方法的调用者的类,superClass
获取当前方法的调用者的父类,super
仅仅是一个编译指示器,就是给编译器看的,不是一个指针。本质:只要编译器看到super这个标志,就会让当前对象去调用父类方法,本质还是当前对象在调用这个题目主要是考察关于
objc
中对self
和super
的理解:self
是类的隐藏参数,指向当前调用方法的这个类的实例。而super
本质是一个编译器标示符,和self
是指向的同一个消息接受者当使用
self
调用方法时,会从当前类的方法列表中开始找,如果没有,就从父类中再找;而当使用
super
时,则从父类的方法列表中开始找。然后调用父类的这个方法调用
[self class]
时,会转化成objc_msgSend
函数
id objc_msgSend(id self, SEL op, ...)
- 调用 `[super class]`时,会转化成 `objc_msgSendSuper` 函数.
id objc_msgSendSuper(struct objc_super *super, SEL op, ...)
第一个参数是 objc_super 这样一个结构体,其定义如下
struct objc_super {
__unsafe_unretained id receiver;
__unsafe_unretained Class super_class;
};
第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个参数self
第二个成员是记录当前类的父类是什么,告诉程序从父类中开始找方法,找到方法后,最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用, 此时已经和[self class]调用相同了,故上述输出结果仍然返回 Son
objc Runtime 开源代码对- (Class)class方法的实现
-(Class)class { return object_getClass(self);
}
runtime模块推荐阅读文章
西木 http://www.jianshu.com/p/6b905584f536
天口三水羊 http://www.jianshu.com/p/9e1bc8d890f9
夜千寻墨 http://www.jianshu.com/p/46dd81402f63
袁峥Seemygo http://www.jianshu.com/p/e071206103a4
郑钦洪_ http://www.jianshu.com/p/bd24c3f3cd0a
HenryCheng http://www.jianshu.com/p/f6300eb3ec3d