Runtime回忆录

2018-01-05  本文已影响14人  VanChan

Runtime简介

Rutime又叫运行时, 是一套底层的C语言API, 是iOS系统的核心之一. 开发者在编码过程中, 可以给任意一个对象发送消息, 在编译阶段只是确定了要向接受者发送这条消息, 而接受者将要如何响应和处理这条消息, 那就要看运行时来决定.

C语言中, 在编译期, 函数的调用就会决定调用哪个函数. 而OC的函数, 属于动态调用过程, 在编译期并不能决定真正调用哪个函数, 只有在真正运行时才会根据函数的名称找到对应的函数来调用.

Objective-C 是一门动态语言, 这意味着它不仅需要一个编译器, 也需要一个运行时系统来动态的创建类和对象, 进行消息传递和转发.

NSObject的定义如下

typedef struct objc_class *Class;

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

在Objc2.0之前

objc_class源码如下:

struct objc_class {  
    Class isa  OBJC_ISA_AVAILABILITY;
    
#if !__OBJC2__
    Class super_class                                        OBJC2_UNAVAILABLE;
    const char *name                                         OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list *ivars                             OBJC2_UNAVAILABLE;
    struct objc_method_list **methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache *cache                                 OBJC2_UNAVAILABLE;
    struct objc_protocol_list *protocols                     OBJC2_UNAVAILABLE;
#endif
    
} OBJC2_UNAVAILABLE;

在这里可以看到, 一个类中, 有超类的指针, 类名, 版本的信息. ivars是objc_ivar_list成员变量列表的指针; methodLists是指向objc_method_list指针的指针. *methodLists是指向方法列表的指针. 动态修改 * methodLists的值就可以添加成员方法, 这也是Category实现的原理, 同样解释了Category不能添加成员变量的原因.

tip: 关于Category

我们知道, 所有的OC类和对象, 在runtime层都是用struct表示的, Category也不例外, 在runtime层, Category用结构体category_t定义, 它包含了:

  1. 类的名字
  2. category中所有给类添加的实例方法的列表
  3. category中所有添加的类方法的列表
  4. category实现的所有协议的列表
  5. category中添加的所有属性
typedef struct category_t {
    const char *name;
    classref_t cls;
    struct method_list_t *instanceMethods;
    struct method_list_t *classMethods;
    struct protocol_list_t *protocols;
    struct property_list_t *instanceProperties;
} category_t;

从category的定义也可以看出category的可为(可以添加实例方法, 类方法, 甚至可以可以实现协议, 添加属性(不含成员变量和getter,setter方法))和不可谓(无法添加实例变量).

在Objc2.0之后

objc_class的定义就变成这样了:

typedef struct objc_class *Class;  
typedef struct objc_object *id;

@interface Object { 
    Class isa; 
}

@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;
}

struct objc_object {  
private:  
    isa_t isa;
}

struct objc_class : objc_object {  
    // Class ISA;
    Class superclass;
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
}

union isa_t  
{
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }
    Class cls;
    uintptr_t bits;
}

从上述源码中, 我们可以看到, Objective-C对象都是C语言结构体实现的, 类也是一个对象, 叫类对象. 在objc2.0中, 所有的对象都会包含一个isa_t类型的结构体成员变量isa, 这也是所有对象的第一个成员变量.

当一个对象的实例方法被调用的时候, 会通过isa找到相应的类, 然后在该类的clas_data_bits_t中去查找方法. class_data_bits_t是指向了类对象的数据区域, 在该数据区域内查找相应方法的对应实现,即IMP.

但是在我们调用类方法的时候, 类对象的isa又指向的是哪里呢? 这里为了和对象查找方法的机制一致, 遂引入了元类(meta-class)的概念.

实例对象的调用实例方法时, 通过对象的isa在类中获取方法的实现, 类对象的类方法调用时, 通过类的isa在元类中获取方法的实现.

meta-class之所以重要, 是因为它存储着一个类的所有类方法, 每个类都会有单独的meta-class, 因为每个类的类方法基本不可能完全相同.

下图很好的描述了对象, 类, 元类之间的关系:

[图片上传失败...(image-2eea5d-1515141587282)]

我们其实应该明白, 类对象和元类对象都是唯一的, 对象是可以在运行时创建无数个的. 而在main方法执行之前, 从dyld(动态链接器)到runtime这期间, 类对象和元类对象在这期间被创建.

tip: iOS程序main函数之前发生了什么

一个iOS App的main函数位于main.m中, 是程序的入口.

整个事件由dyld主导, 完成运行环境的初始化后, 配合imageloader将二进制文件加载内存, 动态链接依赖库, 并由runtime负责加载成objc定义的结构, 所有初始化工作结束后, dyld调用真正的main函数.值得说明的是, 这个过程远比写出来的要复杂, 这里只提到了runtime这个分支, 还有像GCD,XPC等重头的系统库初始化分支没有提及. 总结起来就是main函数执行之前, 系统做了很多的加载和初始化工作, 但都被很好的隐藏了, 我们无需关心.

当这个一切都结束时, dyld会清理现场, 将调用栈回归, 只剩下main函数, 孤独的main函数, 看上去是程序的开始, 却是一段精彩的终结.

下面代码输出什么?

 @implementation Son : Father
- (id)init
{
    self = [super init];
    if (self)
    {
        NSLog(@"%@", NSStringFromClass([self class]));
        NSLog(@"%@", NSStringFromClass([super class]));
    }
return self;
}
@end

self和super的区别: self是类的一个隐藏参数, 每个方法的实现的第一个参数即为self; super并不是隐藏参数, 它实际上只是一个"编译器标示符", 它负责告诉编译器当调用方法时, 去父类中去查找方法, 而不是从本类中查找方法.

因此在这个问题中, 都是在根类中找方法实现, 要明确的是, 发送消息(调用方法)的主体是son, 接受消息的主体也是son, 所有打印的都是son.

消息发送和转发

objc_msgSend函数

最初接触到OC 的 Runtime, 一定是从[receiver message]这里开始的, [receive message] 会被编译器转化为:

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

这是一个可变参数函数, 第二个桉树类型是SEL, SEL在OC中是selector方法选择器

typedef struct objc_selector *SEL;

objc_selector是一个映射到方法的C字符串. 需要注意的是@selector()选择子只与函数名有关. 不同类中相同名字的方法所对应的方法选择器是相同的, 即使方法名字相同二变量类型不同也会导致它们具有相同的方法选择器, 由于这点特性, 也导致了OC不支持函数重载.(ps: 函数重载是指方法名相同而参数不同的函数)

在receiver拿到对应的selector之后, 如果自己无法执行这个方法, 那么该条消息会被转发, 或者临时动态的添加方法实现, 如果转发到最后依旧没法处理, 程序就会崩溃.

所以编译器仅仅是确定了要发送消息, 而消息如何处理是要在运行期解决的事情.

总结一下objc_msgSend会做的几件事情:

  1. 检测这个selector是不是要忽略的.

  2. 检测target是不是为nil.

    如果这里有相应的nil的处理函数, 就跳转到相应的函数中, 如果没有处理nil的函数, 就自动清理现场并返回. 这一点就是为何在OC中给nil发送消息不会崩溃的原因.

  3. 确定不是给nil发消息之后, 在该class的缓存中查找方法对应的IMP实现.

    如果找到, 就跳转进去执行. 如果没有找到, 就在父类方法列表里面继续查找, 一直找到NSOject为止.

  4. 如果还没有找到, 那就需要开始消息转发阶段了. 至此, 发送消息阶段完成. 这一阶段主要完成的是通过select()快速查找IMP的过程.

消息转发Message Forwarding阶段

到了转发阶段, 会调用id_objc_msgForward(id self, SEL _cmd,...)方法, 在执行_objc_msgForward之后会调用__objc_forward_handler函数. 当我们给一个对象发送一个没有实现的方法的时候, 如果其父类也没有这个方法, 则会崩溃, 报错信息类似于: unrecognized selector sent to instance,然后接着会跳出一些堆栈信息. 这些信息就是从这个方法中抛出的.

要设置转发只要重写_objc_forward_handler方法即可, 这一步是替消息找备援接受者, 如果这一步返回的是nil, 那么不就措施就完全的失效了, 接下来未识别的方法崩溃之前, 系统会再做一次完整的消息转发.

Runtime的可以做什么?

Reference

上一篇下一篇

猜你喜欢

热点阅读