Runtime的一些基本使用
消息转发
我们在OC中调用方法的时候,其实是在给一个对象发送一个消息
// OC调用方法
[Person new] sendMessage:@"message"];
//实质转换成底层方法执行
// objc_msgSend(void /* id self, SEL op, ... */ )
/**
* @param id(self) : 像那个对象发送消息
* @param SEL (op) : 消息名称
* @param ... 需要传递的参数列表
*/
objc_msgSend([Person new],@selector(sendMessage:),@"message");
每个实例对象其实都有有一个isa指针,他指向对象的类.而类里也有个isa的指针, 指向meteClass(元类)。元类保存了类方法的列表。当类方法被调用时,先会从本身查找类方法的实现,如果没有,元类会向他父类查找该方法。同时注意的是:元类(meteClass)也是类,它也是对象。元类也有isa指针,它的isa指针最终指向的是一个根元类(root meteClass)。根元类的isa指针指向本身,这样形成了一个封闭的内循环
struct objc_class {
// 类的isa指针
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
//父类
Class _Nullable super_class OBJC2_UNAVAILABLE;
const char * _Nonnull name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
//成员变量列表
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE;
//方法列表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE;
//缓存列表
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE;
//协议列表
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE;
#endif
方法的调用过程:
-
1.首先,在自己本类的缓存列表(objc_cache)中寻找方法,找到方法就直接执行其实现
-
2.如果缓存列表中没有寻找到, 那就去方法列表(objc_method_list)中去寻找,找到就直接执行其实现
-
3.如果方法列表中没有寻找到,说明这个类中没有这个方法,那就去其父类中寻找,执行1和2的过程
-
4.如果找到了根类还没有找到这个方法,说明没有这个方法, 就转向一个拦截调用的方法,我们可以在这个拦截方法中做一些操作(如果这个方法没有找到,就开始进入消息转发机制)
4.1动态方法实现:
//实现动态添加的方法 是一个c语言方法
void runAddMethod(id self, SEL _cmd, NSString *msg) {
NSLog(@"动态添加一个方法");
}
//1.动态方法解析.如果没有找到方法会报错:-[Person sendMessage:]: unrecognized selector sent to instance 0x600003a883a0
// 调用不存在的示例方法,默认返回NO 会触发这个方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
//1.匹配方法
NSString *methodName = NSStringFromSelector(sel);
//判断是不是我们要执行的方法
if ([methodName isEqualToString:@"sendMessage:"]) {
//2,动态的添加一个方法
/**
v@:@ : v->代表返回值是Void
@->代表id slef对象
:->代表 sel _cmd
@: 代表参数对象
*/
return class_addMethod(self, sel, (IMP)runAddMethod, "v@:@");
}
return NO;
}
//控制台打印
2019-10-29 11:04:51.752835+0800 Runtime[20894:207124] 动态添加一个方法
4.2:进入快速转发阶段:(实质是找一个备用的接受者)
// SparePerson的代码
@interface SparePerson : NSObject
- (void)sendMessage:(NSString *)msg;
@end
#import "SparePerson.h"
@implementation SparePerson
- (void)sendMessage:(NSString *)msg {
NSLog(@"我是备用的接受者SparePerson--------%@",msg);
}
@end
//2.快速转发 : 找一个备用的接受者
// 将调用的方法重新定向到一个其他类声明了的这个方法类里面去,返回这个类的target
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSString *methodName = NSStringFromSelector(aSelector);
if ([methodName isEqualToString:@"sendMessage:"]) {
return [SparePerson new];
}
//如果没有备用的接受者,则让其走自己默认的继承树,这个时候我们就达到了快速转发的目的
//消息机制越往后 ,系统的开销是越大
return [super forwardingTargetForSelector:aSelector];
}
//控制台打印
2019-10-29 11:19:08.623767+0800 Runtime[20992:215592] 我是备用的接受者SparePerson--------message
4.3 进入到慢速转发(慢速转发有两个步骤: 方法签名和消息转发)
//3.进入到慢速转发阶段 包括两个步骤
//3.1方法签名
//3.2消息转发
//方法签名 是要把我们的方法信息保存下载
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSString *methodName = NSStringFromSelector(aSelector);
if ([methodName isEqualToString:@"sendMessage:"]) {
//通过下面这个方法把方法的信息保存下来
return [NSMethodSignature signatureWithObjCTypes:"v@:@"];
}
//如果没有这个方法,那走自己原有的继承树
return [super methodSignatureForSelector:aSelector];
}
//消息转发
//所有方法签名的信息 都会保存到这个NSInvocation 这个类中
- (void)forwardInvocation:(NSInvocation *)anInvocation {
//1.获取方法的方法编号
SEL sel = [anInvocation selector];
//获取这个方法编号之后,我们需要给他找一个处理者
SparePerson *tempObj = [SparePerson new];
//如果这个处理者里面有实现我们这个方法, 那就直接制定这个方法的接受者是当前这个对象
if ([tempObj respondsToSelector:sel]) {
[anInvocation invokeWithTarget:tempObj];
}else {
//如果没有找到处理者,那就走自己的继承树
[super forwardInvocation:anInvocation];
}
}
//如果上述方法都没有实现,或者拦截 那就会执行下面的 crash错误的方法
//如果上述方法都没有处理这个内容,拿就会调用和这个carsh错误的方法 ,我们重写这个方法,会使得app虽然找不到方法 但是不会崩溃
- (void)doesNotRecognizeSelector:(SEL)aSelector {
NSLog(@"找不到方法=====");
}
//控制台输出
2019-10-29 12:14:58.631274+0800 Runtime[21326:244076] 找不到方法=====
- 5.如果拦截调用方法没有做处理, 那程序就会崩溃报错
Method Swizzling(方法交换)
假如我们想实现一个功能,当TableView的数据为空的时候,我们希望在TableView的视图上显示一个背景提示View:
我们可以给TableView写一个分类,然后使用Method Swizzling 交换TableView的reloadData()方法来实现,代码如下:
@interface UITableView (BGView)
/** 当表格数据为空时显示的背景提示View */
@property (nonatomic, strong) UIView *defaultBackGroundView;
/// 和TableView的刷新方法交换的方法
- (void)gy_reloadData;
@end
#import "UITableView+BGView.h"
#import <objc/runtime.h>
static NSString *key = @"backgroundViewKey";
@implementation UITableView (BGView)
//当文件加载运行时 就会执行 load方法中不宜当太过于复杂的操作,这样会影响我们app的启动时间
// 如果父类 子类 分类都有重写了load()方法
// 加载顺序 父类-> 子类 -> 分类(多个分类,按照编译顺序加载)
+ (void)load {
//1.首先获取TableView的刷新方法
Method originalMethod = class_getInstanceMethod(self, @selector(reloadData));
//2.获取我们需要j交换的当前方法
Method currentMethod = class_getInstanceMethod(self, @selector(gy_reloadData));
//3.交换两个方法
method_exchangeImplementations(originalMethod, currentMethod);
}
- (void)gy_reloadData {
//首先我们需要继续执行系统的方法,由于方法交换了,执行自己的实质上是执行系统的
[self gy_reloadData];
//刷新视图
[self gy_reloadView];
}
- (void)gy_reloadView {
//第一步 我们检测系统的数据是不是空的
//得到系统数据源
id<UITableViewDataSource> dataSource = self.dataSource;
//得到系统有几个分区
NSInteger section = [dataSource respondsToSelector:@selector(numberOfSectionsInTableView:)] ? [dataSource numberOfSectionsInTableView:self] : 1;
//标记UITableView有没有数据
NSInteger rows = 0;
for (int i = 0; i < section; i++) {
rows = [dataSource tableView:self numberOfRowsInSection:i];
}
if (!rows) {
if (self.defaultBackGroundView) {
self.defaultBackGroundView.hidden = NO;
}else {
//表示 没有数据 就显示一张默认背景View
self.defaultBackGroundView = [[UIView alloc] initWithFrame:self.frame];
self.defaultBackGroundView.backgroundColor = [UIColor orangeColor];
[self addSubview:self.defaultBackGroundView];
}
}else {
//表示表格有数据
self.defaultBackGroundView.hidden = true;
}
}
#pragma mark -- Setter and Getter
- (void)setDefaultBackGroundView:(UIView *)defaultBackGroundView{
//使用runtime绑定属性 set方法
objc_setAssociatedObject(self, &key, defaultBackGroundView, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (UIView *)defaultBackGroundView {
return objc_getAssociatedObject(self, &key);
}
@end
然后在使用TableView的reloadData()刷新数据,就可以实现这个效果了
字典模型互转
字典转模型
- 1.遍历字典获取key和value
- 2.通过objc_msgSend()方法 —> 调用set方法赋值
- 3.函数指针的写法–> 返回类型 (*名称)(param1,param2)
//key - value
//然后在使用 消息发送 发送一个消息
//key -> 字典中取 value -> 通过set方法赋值(objc_msgSend())
//函数指针格式
//返回类型 (*函数名)(param1, param2)objc_msgSend()
- (instancetype)initWithDic:(NSDictionary *)dic {
self = [super init];
if (self) {
//1.首先我们需要循环遍历和这个字典
for (NSString *key in [dic allKeys]) {
id value = dic[key];
//1.然就我们获取类中属性的set方法 capitalizedString把单词的首字母转换成大写
NSString *methodName = [NSString stringWithFormat:@"set%@:",key.capitalizedString];
NSLog(@"methodName============%@",methodName);
SEL sel = NSSelectorFromString(methodName);
if (sel) {
//2判断方法存在,然后我们利用消息发送 像set发送一个消息
((void(*)(id, SEL, id))objc_msgSend)(self,sel,value);
}
}
}
return self;
}
//测试代码
NSDictionary *dic = @{@"name":@"guoweiyong",@"sex":@"男",@"age":@(27)};
Person *temp = [[Person alloc] initWithDic:dic];
NSLog(@"runtime-->实现字典转模型---%@",temp);
//控制台输出打印
2019-10-29 20:52:32.921577+0800 RuntimeModelDic[22935:429445] runtime-->实现字典转模型---{
age = 27;
name = guoweiyong;
sex = "\U7537";
}
问题: 目前知道,模型如果定义是int类型 ,而字典中是NSNumber类型的话,转出来int类型的数据会乱码.只能转相同类型的,目前还不知到怎么解决这个问题?
模型转字典
- key值 --> class_copyPropertyList ,property_getName 这两个方法得到
- 2.遍历属性列表,获取相应的key的value值 ((id(*)(id,SEL))objc_msgSend)(self,sel);
/**
* 字典中肯定是 key - value 模型
* key: class_getPropertList();得到类中的属性列表
* value: 通过调用get方法来获取(objc_msgSend())
*/
- (NSDictionary *)convertModleToDic {
unsigned int count = 0;
//1.获取该类中的属性列表
//class_copyPropertyList(Class _Nullable cls, unsigned int * _Nullable outCount)
objc_property_t *propertys = class_copyPropertyList([self class], &count);
//2.判断属性个数是否是0
if (count != 0) {
//3.首先创建一个可变字典
NSMutableDictionary *tempDic = [NSMutableDictionary dictionary];
for (int i = 0; i < count; i++) {
const char *propertyName = property_getName(propertys[i]);
NSString *methodName = [NSString stringWithUTF8String:propertyName];
SEL sel = NSSelectorFromString(methodName);
if (sel) {
//发送一个get消息得到值
id value = ((id(*)(id,SEL))objc_msgSend)(self,sel);
if (value) {
tempDic[methodName] = value;
}else {
tempDic[methodName] = @"";
}
}
}
//class_copyPropertyList c语言中使用copy需要释放内存
free(propertys);
return tempDic;
}
free(propertys);
return nil;
}
//测试代码
Person *temp = [[Person alloc] init];
temp.name = @"guoweiyong";
temp.sex = @"男";
temp.age = [NSNumber numberWithInt:27];
NSDictionary *tempDic = [temp convertModleToDic];
NSLog(@"runtime--->实现模型转字典----%@",tempDic);
//控制台输出
2019-10-29 21:12:14.883853+0800 RuntimeModelDic[23106:442994] runtime--->实现模型转字典----{
age = 27;
name = guoweiyong;
sex = "\U7537";
}
另外一种方法:
- 首先获取Model的成员变量—>class_copyIvarList()
- 遍历Model的成员变量,用成员变量的名字为key,到字典中去值
- 使用KVC给数据模型赋值
- 参考链接:https://www.jianshu.com/p/f6c8914014a3