美文网首页
runtime(运行时)

runtime(运行时)

作者: 一片枫叶随风舞 | 来源:发表于2017-11-22 23:48 被阅读0次

    本文分为4个部分

    1.介绍OC和C语言之间的转换

    2.介绍运行时和相关术语

    3.介绍消息发送机制已及怎样找到函数实现

    4.runtime的应用

    一、OC和C语言之间的转换

    Apple 官方对OC的定义

    Objective-C is the primary programming language you use when writing software for OS X and iOS. It’s a superset of the C programming language and provides object-oriented capabilities and a dynamic runtime. Objective-C inherits the syntax, primitive types, and flow control statements of C and adds syntax for defining classes and methods. It also adds language-level support for object graph management and object literals while providing dynamic typing and binding, deferring many responsibilities until runtime

    总结:OC = C+runtime+面向对象;

    C语言是编译型静态语言,也就是说C语言编译完后,是不可以更改代码的结构的,如更改某个方法的实现,如调用eat(),但执行的确实run()的实现。

    而OC虽然是基于C的但是因为有了runtime机制,却是编译型动态语言,这种语言可以动态的创建类,属性以及交换方法的实现,这种交换方法实现有个热门词叫Method Swizzling(俗称黑魔法)。

      runtime对OC来说,类似操作系统的功能,很多方法如kvc、kvo等功能因为有运行时才得以实现。可以说 C语言和面向对象特性是OC的身体,而Runtime才是OC的灵魂。

    二、运行时和相关术语

    1.运行时是什么?

    运行时是用Apple提供给OC的一套用C语言编写的底层API,它属于1个C语言库,平时我们编写的OC代码最终都转换成了

    运行时的C语言代码。

    当在 OC 中使用方法 是这样的:

    [self doSomething];

    实际上会被转化为调用objc_msgSend函数,给某个对象发送消息:

    objc_msgSend(self, @selector(doSomething));

    佐证:1、苹果官方文档

    In Objective-C, messages aren’t bound to method implementations until runtime. The compiler converts a message expression,

    [receiver message]

    into a call on a messaging function,objc_msgSend.

    我们可以在这里查看官方文档

    佐证:2 编译OC代码

        编译前

    编译后

    2、相关术语

    我们通过 objc_msgSend来了解runtime的相关术语

    它的声明是这样的:

    // message.h

    id objc_msgSend(id self, SEL op, ...);

    那具体是怎样转换的?OC 中的类、方法、属性等在 C 语言中是怎样被表示的?下面先看下一些相关术语。

    1、SEL

    SEL是转换后的函数中第二个参数的类型,它的对象selector(方法选择器) ,顾名思义,是用来识别和选择要执行的 OC 方法的。它的定义如下:

    // objc.h

    typedef struct objc_selector *SEL;

    其实它就是个映射到方法的 C 字符串,上面就是通过@selector(doSomething)来获取一个名字叫 doSomething 的selector。

    2、id

    id 是转换后的函数中第一个参数的类型,它在 OC 中被称为万能指针,可以指向任何类的实例。它的定义如下:

    // objc.h

    typedef struct objc_object *id;

    struct objc_object {

    Class isa; 

    };

    objc_object结构体中的第一个元素是isa指针,根据它可以找到对象所属的类。(但要注意在 KVO 中 isa 指针指向的是一个中间类了,kVO利用运行时机制动态的创建了一个继承被观察类的中间类,通过对中间类属性变化,通知观察对象,这个应用我们在应用部分再详细解释

    3、Class

    上面说的 isa 的类型是Class,而它的定义如下:

    // objc.h

    typedef struct objc_class *Class;

    // runtime.h

    struct objc_class {

    Class isa; // 类同样有自己父类,指向所属的父类

    Class super_class;  // 父类指针

    const char *name;  // 类名

    long version;

    long info;

    long instance_size;

    struct objc_ivar_list *ivars;  // 成员变量列表

    struct objc_method_list **methodLists; // 方法列表

    struct objc_cache *cache;  // 缓存

    struct objc_protocol_list *protocols; // 协议列表

    };

    可见在 Runtime 系统中,一个类还关联了它的父类指针、类名、成员变量、方法、缓存、协议。

    注意到不仅表示对象的objc_object结构体中有个isa指针,表示类的objc_class结构体中也有个isa指针,这是因为在 OC 中,类本身也是一个对象(类对象)。对象的方法存储在它所属的类中,那类的方法呢?这时就需要类对象所属的类来存储类方法了,它叫meta class(元类)。对象的类、父类、元类之间的关系如下(实现是 super_class 指针,虚线是 isa 指针):

    注意到所有的元类的元类都是root class(meta),而这个根元类的元类是它自己,它的父类是NSObject;NSObject 的元类也是那个根元类,但它没有父类。


    由上图可知 实例对象的isa都指向自己的类对象,类对象中的superClass指向自己的父类,类对象中的isa指向了元类。

    这样清晰的继承关系,将会是 runtime 消息机制发送消息的基础。

    4、成员变量

    其中 objc_ivar_list 是成员变量列表,定义如下:

    // runtime.h

    struct objc_ivar_list {

    int ivar_count;

    int space;

    struct objc_ivar ivar_list[1];//成员变量的数组

    }

    struct objc_ivar {

    char *ivar_name; //单个变量的名字

    char *ivar_type; //变量类型

    int ivar_offset; //偏移量

    int space;

    }

    typedef struct objc_ivar *Ivar;

    可见成员变量列表objc_ivar_list结构体存储着由成员变量objc_ivar结构体组成的数组,objc_ivar结构体存储着单个成员变量的名字、类型、偏移量等信息。

    注意:成员变量和属性的区别,属性可以1.生成成员变量 2.声明和实现set、get方法(在分类中除外,因为分类中不能用成员变量)

    5、方法

    objc_method_list是方法列表,定义如下:

    // runtime.h

    struct objc_method_list {

    struct objc_method_list *obsolete;

    int method_count;

    int space;

    struct objc_method method_list[1];

    }

    struct objc_method {

    SEL method_name;

    char *method_types;

    IMP method_imp;

    }

    typedef struct objc_method *Method;

    可见方法列表objc_method_list结构体存储着由方法objc_method结构体组成的数组,objc_method 结构体存储着单个方法的信息:名称(SEL类型的)、参数类型和返回值类型(method_types中)和具体实现(IMP类型的)。

    6、IMP

    IMP(method implementation,方法实现) 的定义是:

    // objc.h

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

    所以它其实是一个函数指针,指向某个方法的具体实现。它的类型和objc_msgSend函数相同,参数中也都包含有 id 和 SEL 类型,这是因为一个 id 和 一个 SEL 参数就能确定唯一的方法实现地址。

    7、Cache

    在objc_class结构体中还有个指向objc_cache结构体的指针,它的定义如下:

    // runtime.h

    typedef struct objc_cache *Cache

    // objc-cache.m

    struct objc_cache {

    // 当前能达到的最大 index

    uintptr_t mask;

    // 被占用的槽位。因为缓存是以散列表的形式存在,所以会有空槽

    uintptr_t occupied;

    // 用数组表示的 hash 表

    cache_entry *buckets[1];

    };

    typedef struct {

    SEL name;

    void *unused;

    IMP imp;

    } cache_entry;

    // _uintptr_t.h

    typedef unsigned long uintptr_t;

    所以它用来做缓存的,用buckets数组来存储被调用过的方法。因为一个方法被调用过,那它以后有可能还会被调用,所以将其存储起来,下次要找某方法先到缓存中找,如果找到的话,免去后面的寻找过程,速度虽然仍会比直接调用函数慢一点点,但已经有很大提升。

    8、属性

    还有我们常用的属性其实也是结构体,它的定义如下:

    // runtime.h

    typedef struct objc_property *objc_property_t;

    typedef struct {

    const char *name;

    const char *value;

    } objc_property_attribute_t;

    // objc-runtime-new.h

    typedef struct objc_property {

    const char *name;

    const char *attributes;

    } property_t;

    typedef struct property_list_t {

    uint32_t entsize;

    uint32_t count;

    property_t first;

    } property_list_t;

    所以一个property_t结构包含了属性的名称和属性字符串。与属性相关的一些方法如下:

    #define newproperty(p) ((property_t *)p)

    // 返回协议中的属性列表,属性个数存储在参数 outCount 中

    objc_property_t *protocol_copyPropertyList(Protocol *proto, unsigned int *outCount)

    // 返回类中的属性列表,属性个数存储在参数 outCount 中

    objc_property_t *class_copyPropertyList(Class cls_gen, unsigned int *outCount)

    // 返回属性列表中的属性数组,属性个数存储在参数 outCount 中

    static property_t **copyPropertyList(property_list_t *plist, unsigned int *outCount)

    // 返回类中的特定名字的属性

    objc_property_t class_getProperty(Class cls_gen, const char *name)

    // 返回某个属性的名字

    const char *property_getName(objc_property_t prop)

    // 返回某个属性的属性字符串

    const char *property_getAttributes(objc_property_t prop)

    三、介绍消息发送机制已及怎样找到函数实现

    OC是一种动态语言,它把编译和链接时候要做的很多事情移到了运行时去做,例如C语言中,源代码要想通过编译,函数声明和函数实现必须都存在,它应该在编译时就把函数实现的地址和函数名称关联了。但是OC是动态语言,没有方法(面向对象称为方法)实先,只有方法声明也是可以编译通过的,方法名称和方法实现是在运行时动态绑定的如官方所述(In Objective-C, messages aren’t bound to method implementations until runtime)。

    那么问题来了,在运行时,方法是怎么被找到的呢?

    使用某对象的方法,都是给这个对象发送消息,消息和方法实现直到运行时才会绑定。Runtime 系统会把使用方法转换为调用函数:

    objc_msgSend(receiver, selector)

    注意到此时函数多了两个参数:消息接收者、方法的 selector。这是每个方法调用时都会默认存在的隐藏参数。如果还有其他参数则是:

    objc_msgSend(receiver, selector, arg1, arg2, ...)

    objc_msgSend 要做的事件有三件:

    (1)找到 selector 对应的方法实现;

    (2)调用该方法实现,并把消息接收者(如果有参数则加上那些参数)传给它;

    (3)把方法实现的返回值传回去(它自己并没有任何返回值)。

    其中第(1)件事的最关键的,具体过程如下:

    (1)检查该selector是不是要忽略的;

    (2)检查这个target是否为nil。在 OC 中给 nil 发送任何消息都不会出错,返回的结果都是 0 或 nil。

    (3)开始找这个类的IMP。先在cache中找,找到则调到对应的方法实现中去执行。

    (4)在cache中没找到,则在该类的方法分发表(dispatch table,即方法列表)中找,找到则执行。

    (5)在该类的方法分发表中找不到,则到父类的分发表中找,再找不到则往上找,直到 NSObject 类为止。这两个过程的示意图如下:

    如果找到,还会根据是否把消息传给父类、返回值是否数据结构而选择下面四个函数中的一个来调用:

    // message.h

    id objc_msgSend(id self, SEL op, ...)

    id objc_msgSendSuper(struct objc_super *super, SEL op, ...)

    void objc_msgSend_stret(id self, SEL op, ...)

    void objc_msgSendSuper_stret(struct objc_super *super, SEL op, ...)

    函数中的super关键字是指向objc_super结构体的指针,objc_super结构体定义如下:

    struct objc_super {

    // receiver 仍是 self 本身

    __unsafe_unretained id receiver;

    // 父类的类型

    __unsafe_unretained Class super_class;

    };

    要注意的是如果想获取某个类的父类,要用cls->super_class或class_getSuperclass方法,而不应该用[super class],因为[super class]会变为objc_msgSend(objc_super->receiver, @selector(class)),即获得的是objc_super->receiver的类,跟[self class]的结果是一样的。

    (6)动态方法解析(Dynamic Method Resolution):如果该类及其继承体系的分发表都没找到,则开始动态方法解析,这是 Runtime 系统在报错前给我们的第一次补救的机会,它会调用resolveInstanceMethod:或者resolveClassMethod:方法,所以我们可以在这两方法中分别用class_addMethod给某个类或对象的某个 selector 动态添加一个方法实现。

    如在 main 函数中调用 Person 对象的一个 aMethod 方法:

    Person *p = [[Person alloc] init];

    [p aMethod];

    它的 .h 和 .m 文件如下:

    //  Person.h

    #import

    @interface Person : NSObject

    - (void)aMethod;

    @end

    //  Person.m

    #import "Person.h"

    #import

    // 要被动态添加的方法实现

    void dynamicMethodIMP(id self, SEL _cmd) {

    NSLog(@"dynamicMethodIMP");

    }

    @implementation Person

    // 动态方法解析

    + (BOOL)resolveInstanceMethod:(SEL)sel {

    // 如果是要被添加方法实现的 selector

    if (sel  == @selector(aMethod)) {

    // 给 self 的类的 sel 方法选择器动态添加方法实现 dynamicMethodIMP

    class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");

    // 返回 YES 后, Runtime 重新给对象发送 aMethod 消息,这次就可以找到 dynamicMethodIMP 方法实现并调用它了

    return YES;

    }

    return [super resolveInstanceMethod:sel];

    }

    @end

    (7)重定向:如果在上面的方法中不做处理或返回 NO,Runtime 系统在报错前还会给第二次补救机会,就是会调用forwardingTargetForSelector:方法索要一个能响应这个消息的对象,所以我们可以在这里返回另外一个能处理该消息的对象:1

    //  Person.m

    - (id)forwardingTargetForSelector:(SEL)aSelector {

    // 如果是要被添加方法实现的 selector

    if (aSelector == @selector(aMethod)) {

    // 返回另外一个对象,让它去接收该消息

    return [[Car alloc] init];

    }

    return [super forwardingTargetForSelector:aSelector];

    }

    上面返回的是一个 Car 对象,如果 Car 类定义如下:

    //  Car.h

    #import

    @interface Car : NSObject

    - (void)aMethod;

    @end

    //  Car.m

    #import "Car.h"

    @implementation Car

    - (void)aMethod {

    NSLog(@"car aMethod");

    }

    @end

    则输出结果就是 “car aMethod”了。

    (8)消息转发:如果在上一步中不做处理或者返回 nil 或 self,则 Runtime 系统会在报错前给我们最后一次补救机会。系统会先调用methodSignatureForSelector:方法,在该方法返回一个包含了消息的描述信息的方法签名(NSMethodSignature对象),并用此方法签名去生成一个NSInvocation对象,然后调用forwardInvocation:方法并把刚生成的NSInvocation对象作参数传进去。我们就可以重写forwardInvocation:方法,在这里将消息转发给其他对象:

    // 获取一个方法签名,用于生成 NSInvocation 对象

    - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {

    NSMethodSignature *signature = [super methodSignatureForSelector:aSelector];

    if (!signature) {

    signature = [[Car new] methodSignatureForSelector:aSelector];

    }

    return signature;

    }

    - (void)forwardInvocation:(NSInvocation *)anInvocation {

    // 如果另一个对象能响应该方法

    if ([[Car new] respondsToSelector:[anInvocation selector]]) {

    // 则让另一个对象来响应该方法

    [anInvocation invokeWithTarget:[Car new]];

    } else {

    [super forwardInvocation:anInvocation];

    }

    }

    尽管消息转发的效果类似于多继承,让一个对象看起来能处理自己不拥有的方法,但 NSObject 类不会将两者混淆。如上面的例子,[p respondsToSelector:@selector(aMethod)]的结果还是NO。

    PS: 上面调用的方法顺序也可以这样获得:在程序启动之后暂停,然后在 gdb 中输入这个命令:call (void)instrumentObjcMessageSends(YES),再运行,则发送的所有消息都会打印到/tmp/msgSend-xxxx文件里了。如新建一个Teacher类,给它的实例发送一条错误的消息:

    Teacher *teacher = [[Teacher alloc] init];

    [teacher aMehtod];

    四、runtime的应用

    1.归档和解档(NSCoding)

    利用runtime函数 遍历模型的所有属性

    代码如下图

    归档代码

    接档代码

    调用代码

    结果

    优势:如果模型对象比较多,不用一个个复制

    注意:因为runtime是C语言文件,它的内存不属于ARC管理因此涉及到copy等方法时要手动管理内存。

    2.Method Swizzing

    这个就是俗称“黑魔法”,其本质在于交换两个函数的IMP,从而达到调用A方法但是执行的却是B方法,其具体实现和原理参考这篇OC中hook方案(一):method swizzling 文章

    3.关联属性

    提示:MJRefresh 里面的UIScrollerView 本身就是利用关联属性在category 里面添加 header 和 footer的。

    我们还可能希望给某些常用的类添加 category,但 category 是只能添加方法而不能添加存储属性的。或者更严谨的说是分类不允许添加成员变量,因为属性的作用就是

    1.生成下划线成员变量

    2.声明和实现set 和get方法

    因为属性不能添加成员变量所以就不能实现添加属性

    现在我们可以用 Runtime 来间接在 category 添加属性了,如在给 UIButton 的 category 中添加一个属性作回调:

    //  UIButton+Extension.h

    #import <UIKit/UIKit.h>

    typedef void (^CallbackBlock)();

    @interface UIButton (Extension)

    @property (copy, nonatomic) CallbackBlock callback;

    @end

    //  UIButton+Extension.m

    #import "UIButton+Extension.h"

    #import <objc/runtime.h>

    const void *yg_callbackKey = @"yg_callbackKey";

    @implementation UIButton (Extension)

    - (void)setCallback:(CallbackBlock)callback {

        // 设置关联属性

        objc_setAssociatedObject(self, yg_callbackKey, callback, OBJC_ASSOCIATION_COPY_NONATOMIC);

    }

    - (CallbackBlock)callback {

        // 获取关联属性

        return objc_getAssociatedObject(self, yg_callbackKey);

    }

    @end

    这样就可以把 callback 当做按钮的属性来用了:

    //  ViewController.m

    #import "ViewController.h"

    #import "UIButton+Extension.h"

    @interface ViewController ()

    @property (weak, nonatomic) IBOutlet UIButton *button;

    @end

    @implementation ViewController

    - (void)viewDidLoad {

        [super viewDidLoad];

        // 设置按钮的 callback “属性”的内容

        self.button.callback = ^{

            NSLog(@"button callback");

        };

        // 获取并执行按钮的 callback “属性”

        self.button.callback();

    }

    @end

    我们常用的第三方库中有很多也是这样用的,如 SDWebImage 会用这样的方法来存储传进来的图片的 URL:

    // UIImageView+WebCache.m

    - (void)sd_setImageWithURL:(NSURL *)url

              placeholderImage:(UIImage *)placeholder

                      options:(SDWebImageOptions)options

                      progress:(SDWebImageDownloaderProgressBlock)progressBlock

                    completed:(SDWebImageCompletionBlock)completedBlock {

        [self sd_cancelCurrentImageLoad];

        objc_setAssociatedObject(self, &imageURLKey, url, OBJC_ASSOCIATION_RETAIN_NONATOMIC);

        ...

    }

    - (NSURL *)sd_imageURL {

        return objc_getAssociatedObject(self, &imageURLKey);

    }

    参考文章

    Objective-C 的 runtime 特性与小蝌蚪找妈妈

    Objective-C Runtime

    相关文章

      网友评论

          本文标题:runtime(运行时)

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