Runtime 概念
runtime(简称运行时),是一套纯C(C和汇编写的) 的API。而 OC 就是运行时机制(消息机制)。
在编译阶段,OC 调用并未实现的函数,只要声明过就不会报错,只有当运行的时候才会报错,这是因为OC是运行时动态调用的。而C语言,函数的调用在编译的时候会决定调用哪个函数,调用未实现的函数就会报错
runtime 消息机制
OC方法调用本质:就是用 runtime发送一个消息,每一个 OC 的方法底层必然有一个与之对应的 runtime 方法.
消息机制原理:对象根据方法编号SEL去映射表查找对应的方法实现。
- 例子:
创建一个macos工程,就在main.m里写下面简单的代码
Dog *dog = [[Dog alloc] init];
[dog run];
1. 导入 #import <objc/message.h>,因为这个里面包含下面两个
#include <objc/objc.h>
#include <objc/runtime.h>
2.去到 build setting -> 搜索msg ->将Enable Strict Checking of objc_msgSend Calls 改为no
否则使用 objc_msgSend 编译出错,因为xcode默认不建议使用
3.去到main.m所在的目录,在终端用下面命令编译一下
clang -rewrite-objc main.m
就会生成一个main.cpp文件
4.打开该文件看最下面main方法,可以看到编译后的代码就是runtime
- 使用:
objc_msgSend(id self, SEL op, ...)
参数:oc对象,方法编号,其他参数...
Dog *dog = [[Dog alloc] init];
[dog run];
可以写成下面的
//Class 类类型 就是一个特殊的对象
Dog *dog = objc_msgSend([Dog class], @selector(alloc));
dog = objc_msgSend(dog, @selector(init));
objc_msgSend(dog, @selector(run));
//
// 底层的实际写法
Dog *dog = objc_msgSend(objc_getClass("Dog"),sel_registerName("alloc"));
dog = objc_msgSend(dog, sel_registerName("init"));
objc_msgSend(dog, @selector(run));
- 消息机制方法调用流程
对象方法:(保存到类对象的方法列表) ,类方法:(保存到元类(Meta Class)中方法列表)。
OC 在向一个对象发送消息时,runtime 库会根据对象的 isa指针找到该对象对应的类或其父类中根据方法编号(SEL)去查找对应方法,找到只是最终函数实现地址(IMP),根据地址去方法区调用对应函数。
补充:每一个对象内部都有一个isa指针,这个指针是指向它的真实类型,根据这个指针就能知道将来调用哪个类的方法。
runtime 使用场景
- 动态交换两个方法的实现(method swizzling)HOOK思想
需求:给系统的imageNamed添加额外功能(是否加载图片成功)
方案一:继承系统的类,重写方法.(弊端:每次使用都需要导入)
方案二:搞个分类,定义一个能加载图片并且能打印的方法(弊端:不能在分类中重写系统方法imageNamed,因为会把系统的功能给覆盖掉,而且分类中不能调用super,所以要 自己实现一个带有扩展功能的方法.但这样就得改调用的方法,改动大)
runtime方式实现步骤:
1.给UIImageView添加分类
2.自定义并实现带有扩展功能的方法
3.交换方法
- (void)viewDidLoad {
[super viewDidLoad];
UIImage *image = [UIImage imageNamed:@"123"];
}
#import <objc/message.h>
@implementation UIImage (Image)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 获取方法地址
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
// 交换方法地址
if (!class_addMethod([self class], @selector(ln_imageNamed:), method_getImplementation(ln_imageNamedMethod), method_getTypeEncoding(ln_imageNamedMethod))) {
method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
}
});
}
// 自己定义的方法
+ (UIImage *)ln_imageNamed:(NSString *)name {
UIImage *image = [UIImage ln_imageNamed:name];
if (image) {
NSLog(@"load image success");
} else {
NSLog(@"load image failed");
}
return image;
}
@end
上面代码执行过程,会先执行load方法,这个时候imageNamed:和ln_imageNamed:就交换了,走到viewDidLoad的 [UIImage imageNamed:@"123"] 时,实际上执行的是ln_imageNamed:,ln_imageNamed:里面又调用ln_imageNamed:,实际上调用的是imageNamed:,这样就根据imageNamed:的返回值来判断。
说明以及注意事项:
- 方法交换为什么写在load方法
load 把类加载进内存的时候调用,只会调用一次 - 为了避免Swizzling的代码被重复执行(调用[super load]),利用dispatch_once函数内代码只会执行一次的特性。
- class_getClassMethod(获取某个类的方法)
class_getInstanceMethod (获取某个对象的方法) - IMP本质上就是函数指针,所以我们可以通过打印函数地址的方式,查看SEL和IMP的交换流程
Method imageNamedMethod = class_getClassMethod(self, @selector(imageNamed:));
Method ln_imageNamedMethod = class_getClassMethod(self, @selector(ln_imageNamed:));
NSLog(@"%p", method_getImplementation(imageNamedMethod));
NSLog(@"%p", method_getImplementation(ln_imageNamedMethod));
method_exchangeImplementations(imageNamedMethod, ln_imageNamedMethod);
- 使用class_addMethod()函数对Method Swizzling做了一层验证,如果self没有实现被交换的方法,会导致失败。而且self没有交换的方法实现,但是父类有这个方法(或者自己有这个方法),这样就会调用父类的方法,结果就不是我们想要的结果了。所以我们在这里通过class_addMethod()的验证,如果self实现了这个方法,class_addMethod()函数将会返回NO,我们就可以对其进行交换了
- runtime结合kvc实现NSCoding的自动归档和解档
如果一个模型有许多个属性,那么我们需要对每个属性都实现一遍encodeObject 和 decodeObjectForKey方法,如果这样的模型又有很多个,就非常麻烦。
- 原来的做法
遵守协议NSCoding
@property (nonatomic, copy) NSString *name;
- (void)encodeWithCoder:(NSCoder *)aCoder{
[aCoder encodeObject:_Name forKey:@"name"];
}
- (id)initWithCoder:(NSCoder *)aDecoder{
if (self = [super init]) {
self.movieName = [aDecoder decodeObjectForKey:@"name"];
}
return self;
}
- 新做法(主要代码)
//解档
- (void)decode:(NSCoder *)aDecoder {
// 一层层父类往上查找,对父类的属性执行归解档方法
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList(c, &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
// 如果有实现该方法再去调用
if ([self respondsToSelector:@selector(ignoredNames)]) {
if ([[self ignoredNames] containsObject:key]) continue;
}
id value = [aDecoder decodeObjectForKey:key];
[self setValue:value forKey:key];
}
free(ivars);
c = [c superclass];
}
}
// 归档
- (void)encode:(NSCoder *)aCoder {
// 一层层父类往上查找,对父类的属性执行归解档方法
Class c = self.class;
while (c &&c != [NSObject class]) {
unsigned int outCount = 0;
Ivar *ivars = class_copyIvarList([self class], &outCount);
for (int i = 0; i < outCount; i++) {
Ivar ivar = ivars[i];
// 获取成员变量的名字
const char *name = ivar_getName(ivar);
//// C字符串 -> OC字符串
NSString *key = [NSString stringWithUTF8String:name];
// 如果有实现该方法再去调用
if ([self respondsToSelector:@selector(ignoredNames)]) {
if ([[self ignoredNames] containsObject:key]) continue;
}
id value = [self valueForKeyPath:key];
[aCoder encodeObject:value forKey:key];
}
free(ivars);
c = [c superclass];
}
}
- 动态添加方法
如果一个类方法非常多,因为需要给每个方法生成映射表,实际上只要一个类实现了某个方法,就会被加载进内存,加载类到内存的时候就比较耗费资源。当硬件内存过小的时候,如果我们将每个方法都直接加到内存当中去,但是很久都不用一次,这样就造成了浪费,那如果我想像懒加载一样,先把方法定义好,但是只有当你用的时候我再加载你,这就需要动态添加了。
当performSelector方法调用某个sel的时候,这时候会到调用对象的+ (BOOL)resolveInstanceMethod:(SEL)sel方法中,如果这里返回是NO,就表示找不到。
- 看下面的例子
// 动态添加方法就不会报错
Person * p = [[Person alloc] init];
[p performSelector:@selector(eat:) withObject:@"吃过了"];
//下面代码在Person.m里
#import <objc/runtime.h>
void addEat(id self, SEL _cmd, NSString *str) {
NSLog(@"%@", str);
}
// 任何方法默认都有两个隐式参数,self,_cmd(当前方法的方法编号)
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
// [NSStringFromSelector(sel) isEqualToString:@"run"];
if (sel == NSSelectorFromString(@"eat:")) {
BOOL isSuccess = class_addMethod(self, sel, (IMP)addEat, "v@:@");
return isSuccess;
}
return [super resolveInstanceMethod:sel];
}
- class_addMethod参数解释(可以command+shift+0查看官方文档)
class_addMethod(Class cls, SEL name, IMP imp,const char *types)
- class: 给哪个类添加方法
- SEL: 添加方法的方法编号
- IMP: 方法实现 (添加方法的函数实现(函数地址))
- type: 方法类型,(返回值+参数类型)
(1) v 返回值类型是void
(2)@ 对象->self
(3): 表示SEL->_cmd
(4)@ 第四个参数
- resolveInstanceMethod的作用
当调用了没有实现的方法没有实现就会调用,然后就可以根据他的参数sel(参数sel就是没有实现的方法)来做一系列的操作。
4.给分类添加属性
在分类中,所写的@property (nonatomic, strong) NSString *name;都仅仅是生成了get和set方法,并没有生成对应的_name属性,但是有时候我们会有一种需求,想要让分类中保存一下新的属性值,因为set和get方法只能是对已经有的东西做操作,比如说最常用的UIView的分类我们对frame中的x,y,width,height做操作。
//给Person添加一个分类addProperty
//在Person+addProperty.h中
@property (nonatomic, strong) NSString *name;
//在Person+addProperty.m中
#import <objc/message.h>
@implementation Person (addProperty)
- (void)setName:(NSString *)name{
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name{
return objc_getAssociatedObject(self, @"name");
}
- (void)viewDidLoad {
[super viewDidLoad];
//给分类动态添加属性
Person * p1 = [[Person alloc] init];
p1.name = @"这是给分类添加的属性";
NSLog(@"%@",p1.name);
}
解释:
objc_setAssociatedObject方法
/**
* 根据某个对象,还有key,还有对应的策略(copy,strong等) 动态的将值设置到这个对象的key上
* @param object 某个对象
* @param key 属性名,根据key去获取关联的对象
* @param value 要设置的值
* @param policy 策略(copy,strong,assign等)
*/
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
objc_getAssociatedObject方法
/**
* 根据某个对象,还有key 动态的获取到这个对象的key对应的属性的值
* @param object 某个对象
* @param key key
* @return 对象的值
*/
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
__OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);
4.实现字典转模型的自动转换
字典转模型KVC实现会有很多弊端,利用运行时,遍历模型中所有属性,根据模型的属性名,去字典中查找key,取出对应的值,给模型的属性赋值。
1.当字典的key和模型的属性匹配不上。
2.模型中嵌套模型(模型属性是另外一个模型对象)。
3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)。
注解:根据上面的三种特殊情况,先是字典的key和模型的属性不对应的情况。不对应有两种,一种是字典的键值大于模型属性数量,这时候我们不需要任何处理,因为runtime是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也当然不会去看了;另外一种是模型属性数量大于字典的键值对,这时候由于属性没有对应值会被赋值为nil,就会导致crash,我们只需加一个判断即可。考虑三种情况下面一一注解;
步骤:提供一个NSObject分类,专门字典转模型,以后所有模型都可以通过这个分类实现字典转模型。
MJExtension 字典转模型实现,底层也是对 runtime 的封装。
注:本文参考 http://www.jianshu.com/p/19f280afcb24
更全面的例子参考 https://github.com/lizelu/ObjCRuntimeDemo