OC之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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值