美文网首页程序员首页投稿iOS开发程序员
详解Runtime,在Objective-C开发中的用途

详解Runtime,在Objective-C开发中的用途

作者: 曲和之殇 | 来源:发表于2018-02-12 17:12 被阅读56次

    原文地址原文

    Runtime在Objective-C中被称为“运行时系统”。

    预热几个知识点

    一、所有都是对象,方法都是消息

    1、OC中所有id类型都被设计成对象,类本身也是一个对象。OC代码在运行时会动态转化为C代码。

    2、所有方法调用都是发消息,例如[self init];被转化为objc_msgSend(self,@selector(init))

    在OC中id指针,可以代表所有对象,其实id是结构体,我们看看id的具体结构:

    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;
    

    这是属性都是什么意思呢?下面一一介绍

    • .Class 的isa指针,指向元类
    • .super_class :指向超类
    • .name是类名
    • .version是类版本信息
    • .info是这个类的详情信息
    • .instance_size是这个类实例对象的大小
    • .ivars是类成员变量
    • .methodLists类的方法列表
    • .cache是,存储被调用过的方法,方便下次使用
    • .protocols是类的协议列表

    二、方法调用顺序

    1. 调用一个方法A,首先runtime把方法转为消息发送,可以简单理解为objc_msgSend
    2. cache里查找,找到执行,否则
    3. 在本类的methodLists中查找,找到执行,否则
    4. 在父类中重复2、3步骤
    5. 直到根类NSObject都没找到,转向方法拦截
    6. 动态解析方法A,判断是不是系统忽略方法,例如retain、release等
    7. 判断调用者target是不是nil,OC语法允许nil对象调用不存在方法而不Crash
    8. 进入第二阶段,消息转发
    9. 进入 resolveInstanceMethod: 方法。如果返回YES,调用class_addMethod,执行方法完成。如果返回NO,向下
    10. 进入重定向 forwardingTargetForSelector: ,指定一个可以实现方法A对象,完成这次调用。如果返回nil,向下
    11. 进入方法签名操作 methodSignatureForSelector: ,如果签名成功,有返回值,这时会调用消息转发方法forwardInvocation:
    12. forwardInvocation:中可以修改实现方法、修改响应对象。

    三、获取参数

    runtime可以获取类的各种参数,方法如下:

    • class_copyPropertyList:获取属性列表
    • class_copyMethodList:获取方法列表
    • class_copyIvarList:获取成员变量
    • class_copyProtocolList:获取协议列表

    Runtime用途

    一、直接通过C发送消息,来调用方法

    • objc_msgSend:调用普通方法
    • objc_msgSend_stret:消息返回值是数据结构
    • objc_msgSend_fpret:消息返回值是浮点数
    • objc_msgSendSuper:调用父类方法
    • objc_msgSendSuper_stret:父类消息返回值是数据结构

    通过代码来说明下:

    新建一个Person类

    @interface Person : NSObject
    @property (nonatomic,strong) NSString  *name;
    @property (nonatomic,strong) NSString  *idcard;
    @property (nonatomic)        NSInteger age;
    

    person有下面方法

    - (void)hello:(NSString*)name andAge:(NSInteger)age;
    - (NSString*)goodMornig:(NSString*)name;
    - (float)getHeight;
    

    正常情况下,OC调用方法如下

    Person *person = [[Person alloc] init];
    [person hello:@"Dave" andAge:12];
    

    在runtime机制下,可以直接用c方法调用,如下

    1、((void (*) (id, SEL)) objc_msgSend) (person, sel_registerName("hello:andAge:"));
    
    2、NSString *str = ((NSString* (*) (id, SEL)) objc_msgSend) (person, sel_registerName("goodMornig:"));
    
    3、float f = ((float (*) (id, SEL)) objc_msgSend_fpret) (objct, sel_registerName("getHeight"));
    

    二、关联对象

    允许开发者对已经存在的对象在 Category 中添加自定义的属性:
    设置关联对象核心方法是:

    OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy) __OSX_AVAILABLE_STARTING(__MAC_10_6, __IPHONE_3_1);

    参数解析:

    • .object:源对象
    • .value :被关联的对象
    • .key :关联键
    • .plicy :关联行为,是个枚举
        typedef OBJC_ENUM(uintptr_t, objc_AssociationPolicy) {
            OBJC_ASSOCIATION_ASSIGN = 0,           /**< Specifies a weak reference to the associated object. */
            OBJC_ASSOCIATION_RETAIN_NONATOMIC = 1, /**< Specifies a strong reference to the associated object.
                                                    *   The association is not made atomically. */
            OBJC_ASSOCIATION_COPY_NONATOMIC = 3,   /**< Specifies that the associated object is copied.
                                                    *   The association is not made atomically. */
            OBJC_ASSOCIATION_RETAIN = 01401,       /**< Specifies a strong reference to the associated object.
                                                    *   The association is made atomically. */
            OBJC_ASSOCIATION_COPY = 01403          /**< Specifies that the associated object is copied.
                                                    *   The association is made atomically. */
        };
    

    移除关联

    objc_removeAssociatedObjects

    通过代码来说明下:

    例如给UIButton的Category,添加属性(判断按钮是否被点击了),正常Category只能扩展方法不能添加属性,但是关联对象打破了这个限制。

    .m代码如下:

    #import "UIButton+Tap.h"
    #import <objc/runtime.h>
    
    static const void *associatedKey = "associatedKey";
    @implementation UIButton (Tap)
    
    
    - (void)setTapButton:(Tap_button)tapButton{
        objc_setAssociatedObject(self, associatedKey, tapButton, OBJC_ASSOCIATION_COPY_NONATOMIC);
        
        [self removeTarget:self action:@selector(buttonTap:) forControlEvents:UIControlEventTouchUpInside];
        if (tapButton) {
            [self addTarget:self action:@selector(buttonTap:) forControlEvents:UIControlEventTouchUpInside];
        }
    }
    - (Tap_button)tapButton{
        return objc_getAssociatedObject(self, associatedKey);
    }
    - (void)buttonTap:(UIButton*)sender{
        if (self.tapButton) {
            self.tapButton();
        }
    }
    

    调用扩展属性

    UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
    button.tapButton = ^{
        NSLog(@"button tap");
    };
    

    三、自动归档

    归档和解档是iOS中的序列化和反序列化操作,需要遵循NSCoding协议。
    例如对上面的Person类进行归档和解档。初级操作如下:

    pragma mark --- 归档
    - (void)encodeWithCoder:(NSCoder *)aCoder{
        //设置归档属性
        [aCoder encodeObject:self.name forKey:@"name"];
        [aCoder encodeObject:self.idcard forKey:@"idCord"];
        [aCoder encodeInteger:self.age forKey:@"age"];
        [aCoder encodeFloat:self.height forKey:@"height"];
    }
    pragma mark --- 解档
    - (instancetype)initWithCoder:(NSCoder *)aDecoder{
        self = [super init];
        if (self) {
            self.name = [aDecoder decodeObjectForKey:@"name"];
            self.idcard = [aDecoder decodeObjectForKey:@"idCord"];
            self.age = [[aDecoder decodeObjectForKey:@"age"] integerValue];
            self.height = [[aDecoder decodeObjectForKey:@"height"] floatValue];
        }
        return self;
    }
    

    BUT,BUT,BUT这种写法太没有技术含量了,一旦模型属性数量增加,工作量就成倍增加,有了Runtime就可以轻松搞定了。

    pragma mark --- 归档
    - (void)encodeWithCoder:(NSCoder *)aCoder{
        unsigned int count = 0;//属性个数
        Ivar *ivars = class_copyIvarList([Person class], &count);
        for (int i=0; i<count; i++) {
            //获取属性名字
            Ivar ivar = ivars[i];
            const char *name = ivar_getName(ivar);//c字符串
            NSString *key = [NSString stringWithUTF8String:name];//转为OC字符串
            //kvc,归档
            [aCoder encodeObject:[self valueForKey:key] forKey:key];
        }
        free(ivars);//c语言函数,ARC不能处理,需要手动释放
    }
    pragma mark --- 解档
    - (instancetype)initWithCoder:(NSCoder *)aDecoder{
        self = [super init];
        if (self) {
            //解档
            unsigned int count = 0;
            Ivar *ivars = class_copyIvarList([Person class], &count);
            
            for (int i=0; i<count; i++) {
                //取出属性
                Ivar ivar = ivars[i];
                const char *name = ivar_getName(ivar);
                NSString *key = [NSString stringWithUTF8String:name];
                
                id value = [aDecoder decodeObjectForKey:key];
                
                //kvc赋值
                [self setValue:value forKey:key];
            }
            
            free(ivars);
        }
        
        return self;
    }
    

    在VC中调用方法如下

    //---自动归档
    Person *person = [[Person alloc] init];
    person.name = @"Deve";
    person.idcard = @"123456";
    person.age = 18;
    person.height = 170.0;
    NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"person.data"];//名字取什么都行
    [NSKeyedArchiver archiveRootObject:person toFile:filePath];//归档
    
    //---解档
    //获取归档地址
    NSString *filePath = [NSTemporaryDirectory() stringByAppendingPathComponent:@"person.data"];
    Person *person = [NSKeyedUnarchiver unarchiveObjectWithFile:filePath];
    
    NSLog(@"person's name is %@,and age is = %ld",person.name,person.age);
    

    四、模型与字典转换

    这其实是最常见的场景,我们从后台拿来数据,需要对数据进行处理,往往会建立模型,也就是Model。那么用字典生成模型是怎么操作的呢?一般方法如下:

    - (instancetype)initWithDictionary:(NSDictionary*)dict{
         if(self = [super init]){
             self.name = dict[@"name"];
             self.idCard = dict[@"idCard"];
             ...
         }
         return self;
     }
    

    看上去合情合理,但是属性一多,就会很麻烦,要写很多重复类似的赋值语句。

    Runtime来解决!

    简单说下原理:

    • 字典转模型:利用objc_msgSend方法主动调用setter方法为Model赋值
    • 模型转字典:利用objc_msgSend方法主动调用getter方法获取属性值生成字典
    • kvc也可以替换setter或getter方法

    通过代码来说明下:

    1. 首先,建立NSObject的Category,如下:

        @interface NSObject (KeyValue)
      
    2. 然后,建立两个方法,分别是字典转模型和模型转字典,如下:

       .h定义方法
       //字典转模型
       +(id)objectInitWithDictionary:(NSDictionary*)dic;
       //模型转字典
       - (NSDictionary*)dictionaryWithObject;
       
       .m方法实现
       //字典转模型
       +(id)objectInitWithDictionary:(NSDictionary*)dic{
           id objc = [[self alloc] init];
           
           for (NSString *key in dic.allKeys) {
               
               id value = dic[key];//取值
               
               //1、判断属性是不是Model,如果是Model递归改方法,如果不是向下
               objc_property_t property = class_getProperty(self, key.UTF8String);//获取模型属性
               unsigned int count = 0;//属性数量
               objc_property_attribute_t *attributeList = property_copyAttributeList(property, &count);
               objc_property_attribute_t att = attributeList[0];//获取属性
               NSString *attString = [NSString stringWithUTF8String:att.value];//转OC字符串
               
               if ([attString isEqualToString:@"@\"Person\""]) {
                    value = [self objectInitWithDictionary:value];//递归
               }
               //2、用objc_msgSend调用setter方法,进行赋值
               NSString *methodName = [NSString stringWithFormat:@"set%@%@:",[key substringToIndex:1].uppercaseString,[key substringFromIndex:1]];//例如setName:
               SEL setter= sel_registerName(methodName.UTF8String);
               if ([objc respondsToSelector:setter]) {
                   ((void (*) (id,SEL,id)) objc_msgSend) (objc,setter,value);//runtime,发消息方法
               }
               
               free(attributeList);
               
           }
           
           return objc;
       }
       //模型转字典
       - (NSDictionary*)dictionaryWithObject{
           unsigned int count = 0;
           NSMutableDictionary *dic = [NSMutableDictionary dictionary];
           
           objc_property_t *propertyList = class_copyPropertyList([self class], &count);
           
           for (int i=0; i<count; i++) {
               objc_property_t property = propertyList[i];
               
               //getter方法
               const char *propertyName = property_getName(property);
               SEL getter = sel_registerName(propertyName);
               
               if ([self respondsToSelector:getter]) {
                   id value = ((id (*) (id,SEL)) objc_msgSend)(self,getter);//获取值
                   //判断value是不是Model,如果是,继续递归转化为字典
                   if ([value isKindOfClass:[self class]] && value) {
                       value = [value dictionaryWithObject];
                   }
                   //不是Model,继续转化
                   if (value) {
                       NSString *key = [NSString stringWithUTF8String:propertyName];
                       [dic setObject:value forKey:key];
                   }
               }
           }
           free(propertyList);
           
           return dic;
       }
      
    3. 最后,在VC里面调用

       pragma mark --- 六、字典和模型互转
       - (void)keyValueExchange{
           NSDictionary *dic = @{@"name":@"李磊",
                                 @"idcard":@"888888",
                                 @"age":@5,
                                 @"height":@170.0,
                                 @"student":@{
                                         @"name":@"Halen"
                                         }
                                 };
           //字典转模型
           Person *person = [Person objectInitWithDictionary:dic];
           NSLog(@"\n person's name is %@,\n person'age is %ld,\n person'height is %f",person.name,[person.age integerValue],[person.height floatValue]);
           
           //模型转字典
           NSDictionary *dict = [person dictionaryWithObject];
           NSLog(@"转换后的字典是\n%@",dict);
       }
      

    五、动态解析

    现在暂停,回到文章开头预热知识点,第二部分我们讲到方法调用顺序,在调启不存在方法时,系统会Crash,为了避免崩溃,我们可以利用runtime动态解析

    通过代码来说明下:

    首先我们建立两个对象School和Teacher,如下

    • school

        #import <Foundation/Foundation.h>
        
        @interface School : NSObject
        -(void)RecruitmentTeacher;//招聘老师
        @end
      
    • teacher

        #import <Foundation/Foundation.h>
        
        @interface Teacher : NSObject
        - (void)haveClass;//上课
        @end
      
    • VC里调用方法如下:

        School *school = [[School alloc] init];
        //调用
        ((void (*) (id, SEL)) objc_msgSend) (school,sel_registerName("haveClass"));
      

    可以看到,School对象,调用了haveClass方法,但是这个方法是Teacher对象的。这个时候如果不做特殊处理程序就会崩溃。此时就该动态解析出场的了。

    有以下几种情况:

    1. 用class_addMethod方法动态添加一个方法,避免崩溃
    2. 没有动态添加方法,进行重定向forwardingTargetForSelector
    3. 重定向失败,配合签名方法methodSignatureForSelector,进行消息转发,在转发方法forwardInvocation里进行处理。

    附上具体的.m文件代码

    #import "School.h"
    #import "Teacher.h"
    #import <objc/runtime.h>
    @implementation School
    -(void)RecruitmentTeacher{
        NSLog(@"recruitment a teacher");
    }
    #pragma mark --- 1
    /*
     如果当前对象调用了一个不存在的方法
     Runtime会调用resolveInstanceMethod:来进行动态方法解析
     我们需要用class_addMethod函数完成向特定类添加特定方法实现的操作
     返回NO,则进入下一步forwardingTargetForSelector:
     */
    +(BOOL)resolveInstanceMethod:(SEL)sel{
        
    #if 0
        return NO;
    #else
        class_addMethod(self, sel, class_getMethodImplementation(self, sel_registerName("RecruitmentTeacher")), "v@:");
        return [super resolveInstanceMethod:sel];
    #endif
    }
    #pragma mark ---2
    /*
     在消息转发机制执行前,Runtime 系统会再给我们一次 “重定向” 的机会
     通过重载forwardingTargetForSelector:方法来替换消息的接受者为其他对象
     返回nil则进步下一步forwardInvocation:
     */
    -(id)forwardingTargetForSelector:(SEL)aSelector{
        
    #if 0
        return nil;
    #else
        //    return nil;
        return [[Teacher alloc] init];//找到可以实现方法的对象,进行替换
    #endif
    }
    #pragma mark ---3
    /*
     进行方法签名,
     返回nil,表示不做签名处理,
     若返回方法签名,进入下一步,消息转发
     */
    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
    
        //    return nil;
        return [NSMethodSignature signatureWithObjCTypes:"v@:"];
    }
    #pragma mark ---4
    /*
     消息转发
     可以做很多操作,修改实现方法,修改相应对象
     */
    -(void)forwardInvocation:(NSInvocation *)anInvocation{
        
        return [anInvocation invokeWithTarget:[[Teacher alloc] init]];//修改相应对象
    }
    
    @end
    

    Demo下载地址Demo下载

    相关文章

      网友评论

        本文标题:详解Runtime,在Objective-C开发中的用途

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