美文网首页
Runtime入门总结

Runtime入门总结

作者: 黑夜里的貓 | 来源:发表于2018-12-07 00:21 被阅读0次
    1. 简介
    2. Runtime的基础数据结构
    3. 消息发送
      1. 方法调用流程
      2. 动态方法解析
      3. 快速转发
      4. 标准转发
    4. API使用
      1. 动态创建类,添加方法
      2. 分类中动态绑定属性
      3. 字典转模型
      4. 方法交换(method swizzling)

    简介


    • runtime是到底是个什么狼狗?
      runtime就是一个由汇编语言和c语言编写的库,它实现了OC语言面向对象和动态语言的特性。多数情况下runtime库都是在幕后工作,但是它也提供了一些API给我们使用。你可以在官方文档查看这些API的使用,也可以在这里下载runtime的开源代码来研究它的具体实现

    • runtime是根据什么原理来实现的?
      答案很简单,就是消息机制。OC中[receiver message]并不是简单的函数调用,它会被编译器转化为[objc_msgSend(receiver, selector)],解释为向object发送一条message消息,程序运行时根据receiver(消息的接受者)和selector来确定执行具体的操作,而不是在编译时决定。

    • clang -rewrite-objc Myclass.m可以查看转换后的代码

    Runtime的基础数据结构


    objc_msgSend()函数是所有消息发送的必经之路,在我们详细了解消息发送流程之前,先从objc_msgSend()函数入手了解一下Runtime中的数据结构。

    objc_msgSend(<#id  _Nullable self#>, <#SEL  _Nonnull op, ...#>)
    

    SEL
    objc_msgSend第二个参数为SEL类型,表示方法选择器,在objc.h文件中可以看到其定义:

    /// An opaque type that represents a method selector.
    typedef struct objc_selector *SEL;
    

    实际上它就是一个映射方法的分段字符串。用于指明调用哪个方法,可以理解为区分方法的ID。可以使用@selector()、sel_registerName()或NSSelectorFromString()来获取选择器。

    IMP

    typedef void (*IMP)(void /* id, SEL, ... */ ); 
    

    它其实就是一个函数指针,指向具体的方法实现,在同一个对象中SELIMP是一一对应的。

    id
    objc_msgSend的第一个参数,大家对它都不陌生,可以接收OC中任何类型的对象。

    typedef struct objc_object *id;
    

    本质上就是一个结构体指针,指向类实例。接着看objc_object

    struct objc_object {
        Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    };
    

    包含一个isa指针,指向它所属的类。

    Class

    typedef struct objc_class *Class;
    

    又是一个指针,指向objc_class,定义在runtime.h

    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
    }
    

    其中包括指向父类的指针super_class、类的名字name、实例变量的大小instance_size、成员变量列表ivars、方法列表methodLists、缓存cache和协议列表protocols等。其中methodLists就是一个链表,存储所有的实例方法(Method)

    typedef struct objc_method *Method;
    
    struct objc_method {
            SEL method_name;
            char *method_types;
                    IMP method_imp;
        } method_list[1];
    

    method_types存储着方法的参数类型和返回值累型。
    cache用来缓存常用的方法,以达到优化方法查找效率的目的。

    struct objc_cache {
        unsigned int mask;            /* total = mask + 1 */
        unsigned int occupied;        
        Method buckets[1];
    };
    

    最重要的我们发现Class里也有一个isa指针。

    实例对象的isa指针指向实例对象所属的类,那么类的isa指针指向哪里呢?

    我们先看个图


    image.png
    • 类本身也是一个对象,类对象所属的类称之为元类(MetaClass)
    • 每个对象都是一个类的实例,类中定义了实例方法列表。对象的isa指针指向它所属的类
    • 每个类都是它所属元类的实例,在元类中定义了类方法列表,类的isa指针指向它的元类
    • 每个类都有一个与之相关的元类,所有元类最终都指向根元类(Root meta class),根元类的isa指向自身。
    • 其实根元类的父类就是NSObject,根元类就是NSObject的元类

    消息发送


    方法调用流程
    假如有一个Person类,有一个run的实例方法。实例化一个对象p,[p run]是怎么执行的?

    1. 根据对象p的isa指针,找到所属的类
    2. 根据selector在类的cache缓存中寻找方法实现的地址,找到执行,没找到执行下一步
    3. 在类的methodLists方法列表中寻找,找到执行,没找到执行下一步
    4. 根据类的super_class指针,到父类的缓存中寻找,没找到执行下一步
    5. 在父类的方法列表中寻找,如果没找,继续向上找,一直到NSObject
    6. 如果最终没找到,则会调用resolveInstanceMethod或者resolveClassMethod方法,让我们可以动态添加方法实现

    动态方法解析
    .h文件

    #import <Foundation/Foundation.h>
    
    @interface Person : NSObject
    
    - (void)run;
    + (void)eat:(NSString *)str;
    
    @end
    

    .m文件

    #import "Person.h"
    #import <objc/runtime.h>
    
    @implementation Person
    
    // 当找不到实例方法实现时,调用此方法。在此方法中我们动态添加一个实例方法
    + (BOOL)resolveInstanceMethod:(SEL)sel{
        if (sel == @selector(run)) {
            class_addMethod([self class], sel, imp_implementationWithBlock(^(id self){
                NSLog(@"run");
            }), "v@:");
            
            return YES;
        }
        return [super resolveInstanceMethod:sel];
    }
    
    // 找不到类方法时调用,在此动态添加一个类方法
    + (BOOL)resolveClassMethod:(SEL)sel {
        if (sel == @selector(eat:)) {
            class_addMethod(object_getClass(self), sel, (IMP)eat, "v@:@");
            // 如果返回NO,则会进入消息转发
            return YES;
        }
        return [super resolveClassMethod:sel];
    }
    
    // 函数实现,函数默认都有两个参数
    void eat(id self, SEL _cmd, NSString *str) {
        NSLog(@"eat %@",str);
    }
    
    @end
    

    需要注意类方法要添加到元类中。

    • [NSObject class]返回类本身
    • [object class]返回对象isa所指的类,简单点说对象是哪个类的实例,就返回哪个类
    • object_getClass返回传入对象的isa所指的类,如果传入的是一个实例,则返回实例所属的类;如果传入的是一个类,则返回类isa指针指向的类,也就是元类
    • v@:@是描述函数的参数类型以及返回值类型的类型编码,v代表函数的返回值为void,第一个@代表第一个参数也就是id self:代表第二个参数SEL _cmd,第二个@代表第三个参数NSString *str。类型编码可参考类型编码

    如果以上两个方法返回NO则会调用forwardingTargetForSelector方法进入消息转发

    快速转发

    - (id)forwardingTargetForSelector:(SEL)aSelector {
        if (aSelector == @selector(run)) {
            return [Proxy new];
        }
        return [super forwardingTargetForSelector:aSelector];
    }
    

    可以在此将消息转发给其他对象。所以我们需要一个可以相应此消息的对象。新建一个类Proxy,只需在.m文件中给出方法实现。

    #import "Proxy.h"
    
    @implementation Proxy
    
    - (void)run{
        NSLog(@"proxy run");
    }
    
    @end
    

    这样便完成了快速转发,当Person实例调用run方法时,就会转发到Proxy中,如果Proxy类里有对应的实现,则会执行。
    forwardingTargetForSelector中,如果返回nil或者self则会进入标准转发forwardInvocation

    标准转发
    在调用forwardInvocation之前,会先调用methodSignatureForSelector获取方法签名,方法签名中包含了参数,返回值,以及消息接受者的相关信息。然后包装成一个NSInvocation对象调用forwardInvocation进行最后的消息转发。

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
        if (aSelector == @selector(run)) {
            // 构造一个方法签名
            NSMethodSignature *sig = [NSMethodSignature signatureWithObjCTypes:"v@:@"];
            return sig;
        }
        return [super methodSignatureForSelector:aSelector];
    }
    
    // 可以将消息转发,可以更改参数值,还可以更改所要调用的方法。总之可以肆无忌惮的做任何事情
    - (void)forwardInvocation:(NSInvocation *)anInvocation {
        // 将方法选择器更改为eat方法
        [anInvocation setSelector:sel_registerName("eat:")];
        NSString *str = @"apple";
        // 添加参数,函数默认有两个参数,所以我们添加参数下标要从2开始
        [anInvocation setArgument:&str atIndex:2];
        // 转发给Proxy 对象
        [anInvocation invokeWithTarget:[Proxy new]];
    }
    

    如果在消息标准转阶段不做处理,最后就会抛出unrecognized selector异常,导致程序crash 。
    总体来说消息发送的过程可以归纳成下图:

    image.png
    如果想更加深入了解请看 消息发送与转发机制原理

    Runtime API的使用


    动态创建类、添加方法、变量

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        
        // 动态创建一个类
        Class DynaClass = objc_allocateClassPair([NSObject class], "DynaClass", 0);
        
        // 从类中取出一个方法
        Method des = class_getClassMethod([NSObject class], @selector(description));
        // 获取方法的特征,包含参数与返回值的信息
        const char* types = method_getTypeEncoding(des);
        
        // 添加实例方法
        class_addMethod(DynaClass, @selector(objcMethod), (IMP)objcMethod, types);
        
        //添加一个成员变量,只能在objc_registerClassPair之前添加
        class_addIvar(DynaClass, "_name", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));
        
        // 注册类
        objc_registerClassPair(DynaClass);
        
        // 获取元类
        Class metaCls = objc_getMetaClass("DynaClass");
        // 添加一个类方法
        class_addMethod(metaCls, NSSelectorFromString(@"classMethod"), imp_implementationWithBlock(^(id self, NSString *str){
            NSLog(@"class method %@", str);
        }), "v@:");
        
        
        
        // 根据动态创建的类,实例化一个对象
        id dynaObjc = [[DynaClass alloc] init];
        
        // 访问成员变量
        [dynaObjc setValue:@"动态添加属性" forKey:@"name"];
        NSLog(@"%@", [dynaObjc valueForKey:@"name"]);
    
        // 调用方法
        NSString *res = [dynaObjc performSelector:@selector(objcMethod)];
        NSLog(@"%@", res);
        
        [DynaClass performSelector:@selector(classMethod) withObject:@"我是参数"];    
    }
    
    NSString *objcMethod(id self, SEL _cmd)
    {
        return [NSString stringWithFormat:@"hello"];
    }
    

    分类中动态绑定属性
    分类(Category)本来是不支持添加属性的,即使我们使用@property也只会声明settergetter ,并没有生成对应的实例变量和方法实现。我们可以使用runtime进行动态绑定来达到添加属性的效果,但是实质上只是添加一个关联,并不是真正的添加一个变量到类的地址空间中。

    - (void)setTitle:(NSString *)title {
        objc_setAssociatedObject(self, "title", title, OBJC_ASSOCIATION_COPY);
    }
    
    - (NSString *)title {
        return objc_getAssociatedObject(self, "title");
    }
    

    详细请看关联对象实现原理

    字典转模型

    1. 获取model对象的所有属性
    2. 根据属性名字查找字典中的key,取出对应的value
    3. 赋值给model
    @implementation NSObject (Model)
    + (id)modelWithDic:(NSDictionary *)dic {
        id pModel = [[self alloc] init];
        
        unsigned int count;
        
        // 获取对象的成员变量数组
        Ivar *iList = class_copyIvarList(self, &count);
        
        for (int i=0; i<count; i++) {
            Ivar var = iList[i];
            // 获取变量的名字
            NSString *varName = [NSString stringWithUTF8String:ivar_getName(var)];
            // 获取变量的类型
            NSString *varType = [NSString stringWithUTF8String:ivar_getTypeEncoding(var)];
            
            // 成员变量都是以下划线开头,所以需要截取一下
            varName = [varName substringFromIndex:1];
            varType = [varType substringWithRange:NSMakeRange(2, varType.length - 3)];
            
            // 根据属性名获取字典的value
            id value = dic[varName];
            
            // 模型嵌套模型。字典的值是字典,需要将其也转换成对应模型
            if ([value isKindOfClass:[NSDictionary class]] && ![varType hasPrefix:@"NS"]) {
                Class class = NSClassFromString(varType);
                value = [class modelWithDic:value];
            }
            
            // 字典的值是数组,数组包含字典,将数组中的字典也转成模型
            if ([value isKindOfClass:[NSArray class]]) {
                // 判断是否实现modelClassInArray协议,协议方法返回数组中字典对应的model类
                if ([self respondsToSelector:@selector(modelClassInArray)]) {
                    id idSelf = self;
                    NSString *type = [idSelf modelClassInArray][varName];
                    Class class = NSClassFromString(type);
                    NSMutableArray *modelArray = [[NSMutableArray alloc] init];
                    
                    for (NSDictionary *dic in value) {
                        id model = [class modelWithDic:dic];
                        [modelArray addObject:model];
                    }
                    value = modelArray;
                }
            }
            
            if (value) {
                [pModel setValue:value forKey:varName];
            }
        }
        
        free(iList);
        
        return pModel;
    }
    @end
    

    MJExtensionJSONModel等大部分框架应该也是这种方式实现的。

    方法交换
    直白点就是调用A的时候,执行的是B的实现,调用B的时候,其实执行的是A。其实就是将两个方法的实现进行交换,如下图:

    image.png
    举个例子,在执行[NSURL URLWithString:urlStr]这句代码的时候,如果urlStr包含中文,需要先对其进行编码才能正确返回NSURL对象。那么可不可以只修改某一个地方,不用每次调用URLWithString :前都对urlStr进行编码呢?

    这时就可以利用方法交换来达到目的。

    1. 新建一个NSURL的分类
    2. 在分类添加一个方法my_ URLWithString :,进行处理中文问题
    3. load方法中将系统的URLWithString :和我们新添的my_ URLWithString :进行交换。
    #import "NSURL+category.h"
    
    @implementation NSURL (category)
    
    + (instancetype)my_URLWithString:(NSString *)URLString
    {
        // 此处不会造成死循环,因为my_URLWithString和URLWithString已经交换,
        // 所以调用my_URLWithString实际上就是调用的URLWithString
        return [self my_URLWithString:[URLString stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]];
    }
    
    + (void)load {
        // 为了确保只执行一次
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
    
            Method originalMethod = class_getClassMethod(self, @selector(URLWithString:));
            Method swizzledMethod = class_getClassMethod(self, @selector(my_URLWithString:));
            
            /*
             class_addMethod:如果类中存在方法,则添加失败。如果不存在则添加成功
             
             判断是否添加成功的目的:
                如果本类中没有实现originalMethod,但是父类中实现了。
                直接使用method_exchangeImplementations进行交换,
                交换的两个方法就是父类中的originalMethod和swizzledMethod。
                那么父类的其他子类调用originalMethod也会执行swizzledMethod。
                进行判断就是为了避免这种情况以及带来的其他麻烦
             
             在本分类中,因为确定URLWithString一定实现了,可以直接使用method_exchangeImplementations进行交换
             */
            BOOL didAddMethod = class_addMethod(object_getClass(self), @selector(URLWithString:), method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));
            
            if (didAddMethod) {
                // class_replaceMethod:替换方法的实现
                class_replaceMethod(self, @selector(my_URLWithString:), method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
            }else {
                // 将两个方法交换
                method_exchangeImplementations(originalMethod, swizzledMethod);
            }
            
        });
    }
    
    @end
    

    这样直接使用[NSURL URLWithString:urlStr]就可以了,不必再去理会urlStr中是否存在中文。

    参考:
    iOS 模块分解—「Runtime面试、工作」看我就 🐒 了 _.
    Objective-C Method Swizzling
    Objective-C Runtime
    iOS runtime和runloop

    相关文章

      网友评论

          本文标题:Runtime入门总结

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