Runtime大体介绍(附代码)
Runtime
- 本文章适合人群:对Runtime有一个初步了解
- 示例代码:https://github.com/ChenZeBin/DemoTest/tree/master
一、简要介绍
-
C语言编译的时候如果有的方法没有实现是会报错的,但是oc不会,因为oc在编译的时候只管找得到这个方法就好,不管你这个方法实现了没有,所以这才导致在运行的时候,去找这个方法发现没有实现,所以就报错了,这也就是OC是动态运行的语言
-
那么runtime是什么呢?runtime就像一个操作系统一样,oc就运行在这个操作系统上或者说runtime是一套纯c语言写的机制,然后平时我们用的oc方法都是这个runtime封装的,也就是说runtime就是oc的底层实现的机制
// 这句代码是oc的,那么oc是怎么去实现这个代码的呢
Person *p = [Person allow];
// 这句就是上面那句oc代码在底层的实现
// 也就是说,你写了上面的那句代码后,其实oc的实现,就是调用了
// objc_msgSend方法,这个方法的作用是谁发送什么方法
// 整句的意思就是Person类发送allow的消息
Person *p = objc_msgSend(objc_getClass("Person"),sel_registerName("alloc"));
// 现在知道runtime是个什么东西了把,其实就是一套纯C语言写的API,给OC调用的,实现了OC的底层实现机制
- runtime的方法都是有前缀的,谁的事情谁开头
objc_msgSend // 发送方法, 是对象的事情,所以是objc开头
- 在oc中方法调用的底层实现 :任何方法的调用都是发送一个消息,用runtime发送消息
// 这句就是Person类调用allow方法
// 底层的实现,就是Person发送了一个allow消息
Person *p = objc_msgSend(objc_getClass("Person"),sel_registerName("alloc"));
- 使用runtime是需要导包的
#import <objc/message.h>
- 使用runtime没有参数提醒的问题
- objc_msgsend()代码提醒没有参数,但是我们需要参数的
去项目,Build Setting msg 改为NO
二、应用
-
利用runtime的消息机制,帮我们调用私有方法
-
交换方法
-
动态添加方法
-
动态添加属性
-
runtime字典转模型(MJExtension的实现方式)
三、应用详细解释
1.为了装逼,仅仅装逼,oc的代码都可以换成runtime的API
// oc创建
// NSObject *objc = [[NSObject alloc] init];
//
// NSLog(@"%@",objc);
// runtime创建
// id:谁发送消息
// SEL:发送什么消息
// 以下代码就相当[NSObject alloc]
id mySelf = objc_getClass("NSObject"); // 获取NSObject类
SEL msgAlloc = sel_registerName("alloc"); // 发送alloc消息
NSObject *objc = objc_msgSend(mySelf, msgAlloc); // NSObject类发送了alloc的消息
objc = objc_msgSend(objc, sel_registerName("init")); // objc发送init消息
NSLog(@"%@",objc);
2.利用runtime的消息机制,帮我们调用私有方法
- 我们为什么要调用私有方法
开发觉得小伙伴有个类的私有方法写的很好,我想拿来用,所以这时候就只能通过runtime去拿
// 假如,我有一个类SendMsg,中有一个私有方法,正常的话oc是
调用不到私有方法的,所以我们用runtime来实现
// 2.利用runtime的消息机制,帮我们调用私有方法
SendMsg *sendMsg = [[SendMsg alloc] init];
objc_msgSend(sendMsg, @selector(privateMethod));// 成功调用了私有方法
// 回想下上面讲过的,oc方法的调用,就是发送一个消息
// 那么上面那个代码就是发送一个privateMethod的消息
- oc方法调用的流程怎么样?
1.调用实例方法:是去类对象的方法列表中找这个实例方法名
2.调用类方法: 是去元类中的方法列表找
3.每个类对象都会有一个isa指针指向自身
- 调用流程
1.通过isa指针去对应的类中查找
2.注册方法编号
-
好处
-
根据方法编号快速找到对应的方法
3.根据方法编号去查找对应方法
4.找到只是最终函数实现的地址,根据地址去方法区调用实现的函数
image3.交换方法-对系统的方法感到不满
1.要让[UIImage imageNamed:@"1.png"];
添加成功输出成功
- 最蠢方法
// 图片
UIImage *image = [UIImage imageNamed:@"1.png"];
if (image) {
NSLog(@"加载成功");
} else {
NSLog(@"加载失败");
}
这样好麻烦,我每次都得去进行判断,这个重复的代码,程序猿不做重复的事情的
- 没有远见的方法
// 把上面的那个代码封装起来一个方法中,比如```myImageName```这个方法
UIImage *image = [UIImage myImageName:name];
// 虽然这样ok了,看着挺成功,但是如果你的大佬跟你说,去维护一个项目,把这个项目的imageNamed都输出成功不成功,那么是不是都得改啊,改成myImageName调用,哎呦,还是很麻烦
- 可以想到,但没法实现的方法 - 重写imageNamed方法
// 尝试分类中重写系统方法
#import "UIImage+Exchange.h"
@implementation UIImage (Exchange)
+ (UIImage *)imageNamed:(NSString *)name {
[self imageNamed:name]; // 死循环不行
[super imageNamed:name]; // 父类是NSObject,没有这个方法
// 总结:没法在分类中重写系统方法
}
- 最好的方法 - runtime交换方法
1.runtime的API
- class_getClassMethod 获取指定方法的地址
- method_exchangeImplementations交换两个方法指向的地址
分类代码,看下我的源码比较清晰一点
//
// UIImage+Exchange.m
// RuntimeDemo
//
// Created by user on 2017/6/8.
// Copyright © 2017年 陈泽槟. All rights reserved.
//
#import "UIImage+Exchange.h"
#import <objc/message.h>
@implementation UIImage (Exchange)
#pragma mark - 没有在分类中重写系统方法
//+ (UIImage *)imageNamed:(NSString *)name {
//
// [self imageNamed:name]; // 死循环不行
// [super imageNamed:name]; // 父类是NSObject,没有这个方法
//
// // 总结:没法在分类中重写父类的方法
//
//}
//
#pragma mark - 把类加载进内存的时候调用,只会调用一次
+ (void)load {
// self : 获取哪个类的方法
//@selector(imageNamed:) : 获取imageNamed方法的地址
Method imageName = class_getClassMethod(self, @selector(imageNamed:));
Method imageWithName = class_getClassMethod(self, @selector(imageWithName:));
// 交换两个方法的地址
method_exchangeImplementations(imageName, imageWithName);
}
#pragma mark - 这个方法也是类加载进内存调用,但是会调用多次
+ (void)initialize {
// 解决调用多次的方法
// static dispatch_once_t onceToken;
// dispatch_once(&onceToken, ^{
// <#code#>
// });
//
}
#pragma mark - 实现加载图片成功后的输出
+ (UIImage *)imageWithName:(NSString *)name {
UIImage *image = [UIImage imageWithName:name];
if (image) {
NSLog(@"成功");
}else {
NSLog(@"失败");
}
return image;
}
@end
主类调用
// 3.交换方法
UIImage *image = [UIImage imageNamed:@"流程图.png"];
// 这时候就会输出成功了
4、动态添加方法(看源码)
-
动态添加方法:oc都是懒加载机制(用到才去加载),只要一个方法实现了就会马上添加到方法列表中,那么添加多了就消耗了性能了
-
app 免费版,收费版的功能是不一样的,那么比如收费版的就可以是动态添加的一些方法,比如说你买了会员,就可以用某个功能,那么这个功能的方法就应该是动态添加的
-
用到的API
1.performSelector
2.resolveInstanceMethod
// 什么时候调用:只要一个对象调用了一个未使用的实例方法就会调用这个方法进行处理
// 作用:动态添加方法,处理未实现
+ (BOOL)resolveInstanceMethod:(SEL)sel
{
if (sel == @selector(eat)) {
// 动态添加eat方法
// 第一个参数:给哪个类添加方法
// 第二个参数:添加方法的方法编号
// 第三个参数:添加方法的函数实现(函数地址)
// 第四个参数:函数的类型,(返回值+参数类型) v:void @:对象->self :表示SEL->_cmd
class_addMethod(self, @selector(eat), eat, "v@:");
}
return [super resolveInstanceMethod:sel];
}
每个方法都有隐式的两个参数
_cmd : 当前方法的编号
self :
5.动态添加属性
-
什么时候需要动态添加属性
需求:让一个nsobject类,添加一个name属性,这个属性用来保存一个字符串
runtime一般都是针对系统类
本质:动态添加属性,就是让某个属性和对象产生关联
如果在分类@property中添加属性?
分类中是不能添加属性的,只会生成get、set方法声明,不会生成实现,也不会生成下划线成员属性(分类中是没有属性的)
-
动态添加属性用到的方法
添加属性就是把这个属性跟这个类产生关联
- (void)setName:(NSString *)name {
// 第一个参数:给哪个对象添加属性
// 第二个参数:属性名称
// 第三个参数:属性值
// 第四个参数:保存策略
objc_setAssociatedObject(self, @"name", name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSString *)name {
// 第一个参数:从哪里取
// 第二个参数:取谁
return objc_getAssociatedObject(self, @"name");
}