美文网首页
runtime原理及应用

runtime原理及应用

作者: 任梦RM | 来源:发表于2019-03-24 11:10 被阅读0次

    runtime简介

    Runtime就是运行时, 核心就是消息机制. 对OC的函数调用,是一个动态调用过程,只有在运行的时候runtime系统才能知道真正调用的哪一个函数(C语言在函数调用过程中, 编译时候就已经决定会调用哪个函数了).

    iOS Runtime中实例对象和类的本质

    实例对象的本质

    OC是一门面向对象的编程语言,在编译过程中,编译器会将OC对象转化成结构体.
    在objc.h中找到:

    typedef struct objc_class *Class;
    typedef struct objc_object *id;
    
    struct objc_object {
        Class isa  OBJC_ISA_AVAILABILITY;
    };
    

    可以看到我们常用的Class, id等关键字的定义.
    OC中实际的类Class, 会被编译成struct objc_class. 我们操作的类的对象实例是struct objc_object, 并且该结构体中有一个指针指向struct objc_class.

    iOS OC中类的本质

    OC对象的结构体中有一个Class指针能够理解, 因为要知道该对象是哪个类的对象.但是我们在objc-runtime-new.h中发现objc_class继承自objc_object的.

    struct objc_class : objc_object {
        // Class ISA; // 继承了
        Class superclass;
        ...
    

    在runtime.h中, 我们看到OC类的结构体struct objc_class的具体定义

    struct objc_class {
        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
    
    } OBJC2_UNAVAILABLE;
    

    OC中类Class中也有一个指针指向Class, 因此类Class本质上也是一个对象, 我们一般称为类对象, 这个指向的Class是就是元类(metaClass)的对象.

    当我们调用对象方法时候, 会通过对象中的Class指针找到对应的Class,然后调用实例方法,同理当我们调用类方法时候, 会通过Class中的Class指针找到对应的meta Class,然后调用meta Class中的方法.

    OC一般会隐藏元类, 并且元类也是某个类的实例, 这个类我们一般称为根元类(root meta Class). 并且所有的元类的根元类都是一个, 并且根元类的元类是它自己. (实际中根元类是NSObject的元类)

    NSString实例的isa指针链:


    image
    iOS 中OC方法调用的本质

    OC中的方法调用称为消息发送, 具体格式是[receiver message].例如:

    NSMutableString *str = [[NSMutableString alloc] initWithString:@"hello"];
    [str appendString:@" world"];
    

    其中str就是receiver, appendString:就是message.

    在message.h头文件中如下方法,这个方法是runtime的核心方法,

    void objc_msgSend(void /* id self, SEL op, ... */ )
    objc_msgSend(receiever, selector, arg1, arg2, ...)
    

    调用实例如下:

    objc_msgSend(str, @selector(appendString:), @" world");
    

    该消息方法为消息的动态绑定完成了以下工作:

    • 它会主动查找receiver的selector对应的方法实现IMP
    • 然后将参数传递给receiver object, 然后调用这IMP
    • 最后返回该方法的返回值

    ==IMP:一个函数指针,保存了方法的地址==

    为了使得objc_msgSend能完成通过selector查找receiver对应的IMP, 我们知道一个OC类和OC对象会有一个isa指针,指向他们各自的Class, 同时OC类还有一个super指针指向父类.

    具体过程就是通过isa指针找到对应的class struct, 然后在dispatch table里面查找selector对应的方法, 如果没有找到,那么通过super指针查找父类的dispatch table, 一直找下去, 直到NSObject类, 如果还没有找到,就调用NSObject的doesNotRecognizeSelector:方法, 然后报unrecognized selector错误.

    iOS runtime实战应用

    1.iOS runtime 进行添加属性,并支持KVO监听

    iOS 中category和runtime的AssociatedObject是两大非常重要的工具:

    category可以给既有类直接添加方法
    associateObject可以给既有类添加属性(类似成员变量)
    结合这两个工具, 那么通过category添加property方法.然后结合associateObject增加关联对象,完成属性存取.

    ==需要加入头文件#import <objc/runtime.h>==

    @interface UIViewController (Extension)
    @property (nonatomic, copy) NSString * categoryString;
    @end
    
    @implementation UIViewController (Extension)
    -(NSString *)categoryString{
        return objc_getAssociatedObject(self, @selector(categoryString));
    }
    
    
    -(void)setCategoryString:(NSString *)categoryString{
        objc_setAssociatedObject(self, @selector(categoryString), categoryString, OBJC_ASSOCIATION_COPY);
    }
    
    @end
    

    并且这种方法也支持KVO的监听:

    -(void)test{
        self.categoryString = @"Runtime生成的属性";
        [self addObserver:self forKeyPath:@"categoryString" options:NSKeyValueObservingOptionNew context:nil];
    }
    
    -(void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSKeyValueChangeKey,id> *)change context:(void *)context{
        NSLog(@"<接收到通知: object:%@ keyPath:%@ change:%@>", object, keyPath,change);
    }
    

    2.(1)交换两个方法的实现,(2)拦截系统自带的方法调用功能

    Method class_getClassMethod(Class cls , SEL name) //获得某个类的类方法
    
    Method class_getInstanceMethod(Class cls , SEL name)//获得某个类的实例对象方法
    
    void method_exchangeImplementations(Method m1 , Method m2)//交换两个方法的实现
    
    案例1:方法简单的交换

    创建一个Person类,类中实现以下两个类方法,并在.h 文件中声明

    + (void)game {
        NSLog(@"游戏");
    }
    
    + (void)study {
        NSLog(@"学习");
    }
    
    [Person study];
    [Person game];
    

    下面通过runtime 实现方法交换,类方法用class_getClassMethod ,对象方法用class_getInstanceMethod

    // 获取两个类的类方法
    Method m1 = class_getClassMethod([Person class], @selector(run));
    Method m2 = class_getClassMethod([Person class], @selector(study));
    // 开始交换方法实现
    method_exchangeImplementations(m1, m2);
    // 交换后
    [Person study];
    [Person game];
    

    控制台打印

    2018-04-26 15:20:37.224745+0800 runtime-test[15020:7430831] 学习
    2018-04-26 15:20:37.224835+0800 runtime-test[15020:7430831] 游戏
    2018-04-26 15:20:37.225668+0800 runtime-test[15020:7430831] 游戏
    2018-04-26 15:20:37.225720+0800 runtime-test[15020:7430831] 学习
    
    案例2:拦截系统方法

    1、为UIImage建一个分类(UIImage+Category)
    2、在分类中实现一个自定义方法,方法中写要在系统方法中加入的语句,添加自己的逻辑判断

    + (UIImage *)xxx_imageNamed:(NSString *)name {
        double version = 11.11;
        if (version == 11.11) {
            name = [name stringByAppendingString:@"5.1_time"];
        }
        return [UIImage xxx_imageNamed:name];
    }
    

    3、分类中重写UIImage的load方法,实现方法的交换(只要能让其执行一次方法交换语句,load再合适不过了)

    + (void)load {
        // 获取两个类的类方法
        Method m1 = class_getClassMethod([UIImage class], @selector(imageNamed:));
        Method m2 = class_getClassMethod([UIImage class], @selector(xxx_imageNamed:));
        // 开始交换方法实现
        method_exchangeImplementations(m1, m2);
    }
    

    ==注意:自定义方法中最后一定要再调用一下系统的方法,让其有加载图片的功能,但是由于方法交换,系统的方法名已经变成了我们自定义的方法名(就是用我们的名字能调用系统的方法,用系统的名字能调用我们的方法),这就实现了系统方法的拦截!==

    3.获得一个类的成员变量( Ivar )、属性( Property )、方法( Method )、协议( Protocol )

    获得某个类的所有成员变量(outCount 会返回成员变量的总数)

    参数:
    1、哪个类
    2、放一个接收值的地址,用来存放属性的个数
    3、返回值:存放所有获取到的属性,通过下面两个方法可以调出名字和类型

    Ivar *class_copyIvarList(Class cls , unsigned int *outCount)
    
    获得成员变量的名字
    const char *ivar_getName(Ivar v)
    
    获得成员变量的类型
    const char *ivar_getTypeEndcoding(Ivar v)
    
    案例1:获取Person类中所有成员变量的名字和类型
    unsigned int outCount = 0;
    Ivar *ivars = class_copyIvarList([Person class], &outCount);
    
    // 遍历所有成员变量
    for (int i = 0; i < outCount; i++) {
        // 取出i位置对应的成员变量
        Ivar ivar = ivars[i];
        const char *name = ivar_getName(ivar);
        const char *type = ivar_getTypeEncoding(ivar);
        NSLog(@"成员变量名:%s 成员变量类型:%s",name,type);
    }
    // 注意释放内存!
    free(ivars);
    

    ==同样:==

    // 测试 打印属性列表
    - (void)testPrintPropertyList {
        unsigned int count;
        
        objc_property_t *propertyList = class_copyPropertyList([self class], &count);
        for (unsigned int i=0; i<count; i++) {
            const char *propertyName = property_getName(propertyList[i]);
            NSLog(@"property----="">%@", [NSString stringWithUTF8String:propertyName]);
        }
        
        free(propertyList);
    }
    
    // 测试 打印方法列表
    - (void)testPrintMethodList {
        unsigned int count;
        
        Method *methodList = class_copyMethodList([self class], &count);
        for (unsigned int i=0; i<count; i++) {
            Method method = methodList[i];
            NSLog(@"method----="">%@", NSStringFromSelector(method_getName(method)));
        }
        
        free(methodList);
    }
    
    // 测试 打印协议列表
    - (void)testPrintProtocolList {
        unsigned int count;
        
        __unsafe_unretained Protocol **protocolList = class_copyProtocolList([self class], &count);
        for (unsigned int i=0; i<count; i++) {
            Protocol *myProtocal = protocolList[i];
            const char *protocolName = protocol_getName(myProtocal);
            NSLog(@"protocol----="">%@", [NSString stringWithUTF8String:protocolName]);
        }
        
        free(protocolList);
    }
    
    案例2:利用Runtime进行 json/dict -> model
    -(instancetype)initWithNSDictionary:(NSDictionary *)dict{
        self = [super init];
        if (self) {
            [self processDict:dict];
        }
        return self;
    }
    
    -(void)processDict:(NSDictionary *)dict{
        NSMutableArray *keys = [[NSMutableArray alloc] init];
        unsigned int count = 0;
        objc_property_t *props = class_copyPropertyList([self class], &count);
        for (int i = 0; i < count; i++) {
            objc_property_t prop = props[i];
            const char *propCStr = property_getName(prop);
            NSString *propName = [NSString stringWithCString:propCStr encoding:NSUTF8StringEncoding];
            [keys addObject:propName];
        }
        free(props);
        for (NSString *key in keys) {
            if ([dict valueForKey:key]) {
                [self setValue:[dict valueForKey:key] forKey:key];
            }
        }
    }
    
    案例3:利用runtime 获取所有属性来重写归档解档方法
    // 解档方法
    - (instancetype)initWithCoder:(NSCoder *)aDecoder {
        // 获取所有成员变量
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &outCount);
        
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            // 将每个成员变量名转换为NSString对象类型
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // 忽略不需要解档的属性
            if ([[self ignoredNames] containsObject:key]) {
                continue;
            }
            
            // 根据变量名解档取值,无论是什么类型
            id value = [aDecoder decodeObjectForKey:key];
            // 取出的值再设置给属性
            [self setValue:value forKey:key];
            // 这两步就相当于以前的 self.age = [aDecoder decodeObjectForKey:@"_age"];
        }
        free(ivars);
        return self;
    }
    
    // 归档调用方法
    - (void)encodeWithCoder:(NSCoder *)aCoder {
        // 获取所有成员变量
        unsigned int outCount = 0;
        Ivar *ivars = class_copyIvarList([self class], &outCount);
        for (int i = 0; i < outCount; i++) {
            Ivar ivar = ivars[i];
            // 将每个成员变量名转换为NSString对象类型
            NSString *key = [NSString stringWithUTF8String:ivar_getName(ivar)];
            
            // 忽略不需要归档的属性
            if ([[self ignoredNames] containsObject:key]) {
                continue;
            }
            
            // 通过成员变量名,取出成员变量的值
            id value = [self valueForKeyPath:key];
            // 再将值归档
            [aCoder encodeObject:value forKey:key];
            // 这两步就相当于 [aCoder encodeObject:@(self.age) forKey:@"_age"];
        }
        free(ivars);
    }
    

    相关文章

      网友评论

          本文标题:runtime原理及应用

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