Runtime

作者: cdd48b9d36e0 | 来源:发表于2017-02-15 00:06 被阅读19次

概念

Runtime就是一个C语言写的API库,通过Runtime这个库,我们可以在程序运行时对类、实例对象、变量、属性、方法进行各种操作。

  1. isa:Runtime中每个对象都有一个isa指针。实例对象的isa指向类,类也是一个对象,类的isa指向该类的元类,所有的元类的isa指向基类(NSObject)的元类,基类的元类的isa指针指向自己。需要注意一点,基类(NSObject)的父类为空,而基类的元类的父类就是基类。
  2. SEL:选择器。我的理解是这就是一个指向方法名字的指针,它的存在就是为了找到函数指针IMP。两个类都有一个相同名字的方法,那么他们的SEL就相同,所以一个SEL可能指向不同的IMP。
  3. IMP:就是函数指针,也就是方法实现的首地址,找到了他就能执行方法。
  4. Method:SEL到IMP的映射。

消息机制

  1. 先判断receiver为不为nil,是的话直接忽略这次消息发送
  2. 根据isa指针找到实例所属的类
  3. 在类中根据sel查找IMP,先从方法缓存Cache里找,再从方法分发列表里面找
  4. 还是没找到的话就根据指向父类的指针superclass找到父类,再从父类里面去找,直到找到基类NSObject里面为止
  5. 最后还是没找到,那么触发消息转发机制

消息转发机制(轻松学习消息转发

void run(id self,SEL _cmd){
//    NSLog(@"%@正在%@",self,NSStringFromSelector(_cmd));
    NSLog(@"%@正在%s",self,sel_getName(_cmd));
}

+(BOOL)resolveInstanceMethod:(SEL)sel{
    NSLog(@"20");
//    if (sel == @selector(run)) {
//        class_addMethod(self, sel, (IMP)run, "v@:");
//    }
    return [super resolveInstanceMethod:sel];
}

-(id)forwardingTargetForSelector:(SEL)aSelector{
    NSLog(@"28");
//    return [[Car alloc]init];
    return self;
}

-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    NSLog(@"34");
    NSString *sel = NSStringFromSelector(aSelector);
    //判断你要转发SEL
    if ([sel isEqualToString:@"run"]) {
        //为你的转发方法手动生成签名
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    return [super methodSignatureForSelector:aSelector];
}

-(void)forwardInvocation:(NSInvocation *)anInvocation{
    NSLog(@"43");
    SEL aSelector = [anInvocation selector];
    //新建需要转发的消息对象
    Car *aCar = [Car new];
    if ([aCar respondsToSelector:aSelector]) {
        //唤醒这个方法
        [anInvocation invokeWithTarget:aCar];
    }
}

应用场景(OC最实用的runtime总结,面试、工作你看我就足够了

应用场景1:解归档,特别是当模型的属性比较多的时候

核心API:class_copyIvarList这个方法可以得到一个类h和m文件里所有属性的个数及其名字

 - (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];
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];

            // 如果有实现该方法再去调用
            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];
    }
}

 - (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];
    }
}
应用场景2:方法替换

楼主经常的做法是用自定义方法替换系统方法防止崩溃。比如往数组、字典里写入空值会导致cash,比如数组读取越界元素,比如URL的网址带有中文时该url会被系统判定为空,此时发起request请求会cash等。这时候就可以用runtime进行方法替换,让他们什么都不变却执行我们新写的加了判断的方法。
核心API: class_getInstanceMethod/class_getClassMethod/method_exchangeImplementations这三个API可以取得两个方法的名字并对他们进行交换

//加载这个类的load方法
 +(void)load{
    //class_getClassMethod : 获取类方法
    //class_getInstanceMethod : 获取对象方法
    
    //1.拿到两个方法 苹果原来的URLWithString  和HK_URLWithString
    //2.1 类类型
    //2.2 方法编号
    Method mURLWithStr = class_getClassMethod([NSURL class], @selector(URLWithString:));
    Method mHK_URLWithStr = class_getClassMethod([NSURL class], @selector(HK_URLWithString:));
    //2.交换这两个方法!(你调用A 个么会执行 B )
    method_exchangeImplementations(mURLWithStr, mHK_URLWithStr);    
}
+(instancetype)HK_URLWithString:(NSString *)URLString
{
    NSURL * url = [NSURL HK_URLWithString:URLString];//会找 URLWithString这个方法
    if (url == nil) {
        NSLog(@"哥么是一个空的URL");
    }
    return url;
}
应用场景3:方法的懒加载(这个实际上消息转发机制里面的方案一)

核心API:resolveInstanceMethod/resolveClassMethod当一个类被调用了没有实现实例方法或者类方法就会触发,然后class_addMethod动态添加方法,具体的格式全是C语言的,如下

//C语言的
//所有的C语言的函数里面!都有这两个隐式参数!只要调用,系统都会传递进来!
void eat(id self, SEL _cmd){
    NSLog(@"调用了%@对象的%@方法",self,NSStringFromSelector(_cmd));
}
void eat1(id self, SEL _cmd,id obj){
     NSLog(@"今晚吃五个%@",obj);
}
//当这个类被调用了没有实现的方法!就会来到这里
+(BOOL)resolveInstanceMethod:(SEL)sel{
//    NSLog(@"你没有实现这个方法%@",NSStringFromSelector(sel));
    if (sel == @selector(eat)) {
        /*
         1.cls: 类类型
         2.name: 方法编号
         3.imp: 方法实现,函数指针!
         4.types:函数类型 C字符串(代码) void === "v"
         */
       //v代表返回值,后面的都代表参数,具体哪个符号什么意思参看文档,这里@代表Object,:代表方法
        class_addMethod([Pseron class],sel, (IMP)eat, "v@:");
    }else if(sel == @selector(eat:)){
        class_addMethod([Pseron class], sel, (IMP)eat1, "v@:@");
    }
    return [super resolveInstanceMethod:sel];
}
应用场景4:避免动态绑定

避免动态绑定的唯一办法就是取得方法的地址,并且直接像函数调用一样调用它。当一个方法会被连续调用很多次,而且您希望节省每次调用方法都要发送消息的开销时,使用方法地址来直接调用方法就显得很有效。

应用场景5:给Category添加成员变量
//步骤一:创建一个分类,比如给任何一个对象都添加一个name属性,就是NSObject添加分类(NSObject+Category)
//步骤二:先在.h 中@property 声明出get 和 set 方法,方便点语法调用
@property(nonatomic,copy)NSString *name;
//步骤三:在.m 中重写set 和 get 方法,内部利用runtime 给属性赋值和取值
char nameKey;
 - (void)setName:(NSString *)name {
    // 将某个值跟某个对象关联起来,将某个值存储到某个对象中
    objc_setAssociatedObject(self, &nameKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
 - (NSString *)name {
    return objc_getAssociatedObject(self, &nameKey);
应用场景6: 字典转模型(这其实就是YYModel或者MJExtension的大致原理)

以往我们都是利用KVC进行字典转模型,但是它还是有一定的局限性,例如:模型属性和键值对对应不上会crash(虽然可以重写setValue:forUndefinedKey:方法防止报错),模型属性是一个对象或者数组时不好处理等问题,所以无论是效率还是功能上,利用runtime进行字典转模型都是比较好的选择。

三种特殊情况:
1.当字典的key和模型的属性匹配不上
2.模型中嵌套模型(模型属性是另外一个模型对象)
3.数组中装着模型(模型的属性是一个数组,数组中是一个个模型对象)


  • 当字典的key和模型的属性匹配不上

不对应有两种,一种是字典的键值大于模型属性数量,这时候我们不需要任何处理,因为runtime是先遍历模型所有属性,再去字典中根据属性名找对应值进行赋值,多余的键值对也当然不会去看了;另外一种是模型属性数量大于字典的键值对,这时候由于属性没有对应值会被赋值为nil,就会导致crash,我们只需加一个判断即可

 - (void)setDict:(NSDictionary *)dict {

    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)];

            // 成员变量名转为属性名(去掉下划线 _ )
            key = [key substringFromIndex:1];
            // 取出字典的值
            id value = dict[key];

            // 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错
            if (value == nil) continue;

            // 将字典中的值设置到模型上
            [self setValue:value forKeyPath:key];
        }
        free(ivars);
        c = [c superclass];
    }
}
  • 模型的属性是另外一个模型对象

这时候我们就需要利用runtime的ivar_getTypeEncoding 方法获取模型对象类型,对该模型对象类型再进行字典转模型,也就是进行递归,需要注意的是我们要排除系统的对象类型,例如NSString,下面的方法中我添加了一个类方法方便递归。

 - (void)setDict:(NSDictionary *)dict {  
    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)];

            // 成员变量名转为属性名(去掉下划线 _ )
            key = [key substringFromIndex:1];
            // 取出字典的值
            id value = dict[key];

            // 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错
            if (value == nil) continue;

            // 获得成员变量的类型
            NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];

            // 如果属性是对象类型
            NSRange range = [type rangeOfString:@"@"];
            if (range.location != NSNotFound) {
                // 那么截取对象的名字(比如@"Dog",截取为Dog)
                type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
                // 排除系统的对象类型
                if (![type hasPrefix:@"NS"]) {
                    // 将对象名转换为对象的类型,将新的对象字典转模型(递归)
                    Class class = NSClassFromString(type);
                    value = [class objectWithDict:value];
                }
            }

            // 将字典中的值设置到模型上
            [self setValue:value forKeyPath:key];
        }
        free(ivars);
        c = [c superclass];
    }
}
 + (instancetype )objectWithDict:(NSDictionary *)dict {
    NSObject *obj = [[self alloc]init];
    [obj setDict:dict];
    return obj;
}
  • 模型的属性是一个数组,数组中是一个个模型对象

我们既然能获取到属性类型,那就可以拦截到模型的那个数组属性,进而对数组中每个模型遍历并字典转模型,但是我们不知道数组中的模型都是什么类型,我们可以声明一个方法,该方法目的不是让其调用,而是让其实现并返回模型的类型。
这块语言可能解释不太清楚,可以参考我的demo,直接运行即可。

 - (void)setDict:(NSDictionary *)dict {  
    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)];

            // 成员变量名转为属性名(去掉下划线 _ )
            key = [key substringFromIndex:1];
            // 取出字典的值
            id value = dict[key];

            // 如果模型属性数量大于字典键值对数理,模型属性会被赋值为nil而报错
            if (value == nil) continue;

            // 获得成员变量的类型
            NSString *type = [NSString stringWithUTF8String:ivar_getTypeEncoding(ivar)];

            // 如果属性是对象类型
            NSRange range = [type rangeOfString:@"@"];
            if (range.location != NSNotFound) {
                // 那么截取对象的名字(比如@"Dog",截取为Dog)
                type = [type substringWithRange:NSMakeRange(2, type.length - 3)];
                // 排除系统的对象类型
                if (![type hasPrefix:@"NS"]) {
                    // 将对象名转换为对象的类型,将新的对象字典转模型(递归)
                    Class class = NSClassFromString(type);
                    value = [class objectWithDict:value];

                }else if ([type isEqualToString:@"NSArray"]) {

                    // 如果是数组类型,将数组中的每个模型进行字典转模型,先创建一个临时数组存放模型
                    NSArray *array = (NSArray *)value;
                    NSMutableArray *mArray = [NSMutableArray array];

                    // 获取到每个模型的类型
                    id class ;
                    if ([self respondsToSelector:@selector(arrayObjectClass)]) {

                        NSString *classStr = [self arrayObjectClass];
                        class = NSClassFromString(classStr);
                    }
                    // 将数组中的所有模型进行字典转模型
                    for (int i = 0; i < array.count; i++) {
                        [mArray addObject:[class objectWithDict:value[i]]];
                    }

                    value = mArray;
                }
            }

            // 将字典中的值设置到模型上
            [self setValue:value forKeyPath:key];
        }
        free(ivars);
        c = [c superclass];
    }
}
 + (instancetype )objectWithDict:(NSDictionary *)dict {
    NSObject *obj = [[self alloc]init];
    [obj setDict:dict];
    return obj;
}

相关文章

网友评论

      本文标题:Runtime

      本文链接:https://www.haomeiwen.com/subject/smdfwttx.html