欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

OC之Runtime基础

程序员文章站 2022-06-02 12:42:21
...

消息转发

我们在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指针指向本身,这样形成了一个封闭的内循环

OC之Runtime基础

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,动态的添加一个方法
        /**
         aaa@qq.com:@ : v->代表返回值是Void
                @->代表id slef对象
                :->代表 sel _cmd
                @: 代表参数对象
         */
        return class_addMethod(self, sel, (IMP)runAddMethod, "aaa@qq.com:@");
    }
    
    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:"aaa@qq.com:@"];
    }
    
    //如果没有这个方法,那走自己原有的继承树
    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
相关标签: Runtime OC

上一篇: Homework Scipy

下一篇: Https抓包总结