Runtime回忆录

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的可以做什么?

  • 实现多继承
  • Method Swizzling
  • AOP (AOP埋点方案)
  • Isa Swizzling
  • Associated Object关联对象
  • 动态的增加方法
  • NSCoding的自动归档和自动解档
  • 字典和模型互相转换

Reference

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容