1

Runtime整理(一)——Runtime的介绍和知识点


前言

    本篇文章是runtime知识点的整理,以便于今后学习和快速查找。

本篇文章分为2个章节:

  • (一)Runtime的介绍和知识点
  • (二)Runtime包含的所有函数

目录

  • 介绍
  • runtime.h
  • 消息发送和转发
  • 常见问题
  • 使用案例

介绍

在讲runtime之前,我们先来明白什么是动态语言。

动态语言:是指程序在运行时可以改变其结构:新的函数可以被引进,已有的函数可以被删除等在结构上的变化,类型的检查是在运行时做的,优点为方便阅读,清晰明了,缺点为不方便调试。动态语言-百度百科

我们都知道OC是一门动态语言,因为它

1.可以在运行时新增方法(使用class_addMethod为类新增方法)

2.可以改变类的结构(使用class_replaceMethod替换方法的实现等)

3.运行时检查类型(运行时多态,id类型)

动态语言特性意味着OC不仅需要一个编译器,还需要一个运行时系统Objc Runtime来实现上述的操作。Objc Runtime其实是一组API,它基本上是用C和汇编写的,是它让C语言获得了面向对象的能力而蜕变为OC,(objc是一个动态库,这个库就是OC语言,objc.h提供了一些结构体来定义OC中的类、父类、元类、协议等赋予了OC面向对象的特性;runtime.h提供了一组运行时的api,比如改变类结构交换方法等,赋予了OC语言的动态特性;message.h提供了oc语言方法调用的规则,应该说这整个动态库使C语言变化为OC)并使OC语言拥有了动态语言特性。它主要功能是下面两点:

1.构建和改变类的结构(在objc/runtime.h文件中定义)

2.处理运行时消息的发送(在objc/message.h文件中定义)

总结:OC中的runtime是OC语言中实现面向对象和动态语言特性的一组API。


runtime.h

我们来看一下runtime.h文件,看它是如何构建出OC中的类,并通过哪些方法来改变类结构的。

OC类的构建

若要构建出类,我们要先了解类,我们知道类的概念是面向对象设计实现封装的基础,面向对象编程的三大特性是:封装、继承、多态

1.封装:抽象出数据类型和数据操作构成一个整体。

那么我们如果想要构建出类,就需要类中有这些:成员变量、方法

2.继承:类之间的父子关系,例如A is a B(A属于B,B是父类,A是子类)isa的由来

这就要求我们建立起的类之间的父子关系

3.多态:指一个类实例(对象)的相同方法在不同情形有不同表现形式。多态的三个必要条件是:继承、重写、父类引用指向子类对象。引用变量指向的具体类型和通过该引用变量发出的方法调用在编程时不确定。如B、C、D继承自A,A对象的引用可以指向A、B、C、D,调用A的某个方法时在不同的情况下调用的方法也不同。

这就要求我们的类能够建立起和父类之间的消息响应。这点会在后面的message.h中讲

我们来看runtime中关于对象(Object)和类(Class)的定义与结构:

// 以下删去了不重要的代码

<objc/objc.h>
typedef struct objc_class *Class;
struct objc_object {
    Class _Nonnull isa;
};

<objc/runtime.h>
struct objc_class {
    // 指向元类的指针
    Class _Nonnull isa;
    
    // 父类
    Class _Nullable super_class;
    // 类名
    const char * _Nonnull name;
    // 类的版本信息,默认为0
    long version;
    // 其他信息,供运行期使用的一些位标识
    long info;
    // 实例的大小
    long instance_size;
    // 成员变量链表
    struct objc_ivar_list         * _Nullable ivars;
    // 方法链表
    struct objc_method_list     * _Nullable * _Nullable methodLists;
    // 方法缓存
    struct objc_cache             * _Nonnull cache;
    // 协议链表
    struct objc_protocol_list     * _Nullable protocols; 
};

对于封装特性来说:我们可以看到OC中的类其实是结构体,而类Class实际上是一个objc_class结构体指针,成员变量存放在ivars链表中,方法存放在methodLists链表中,此外还有包含了类名、协议链表等其他内容。

对于继承特性来说:我们可以看到OC中的类中存在一个isa指针和super_class指针,通过他们建立起了类之间的父子关系。

如此,OC中类的雏形就初步构建出来了。下面分别来详细介绍他们

类和实例和父类之间的关系

先上经典的图,以下内容请结合图来一起食用。
clipboard.png

通过方法调用的过程我们可以了解到类和实例和其父类之间的关系。

实例的方法调用过程:

1.当一个实例调用方法时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类,然后会在类的methodLists方法链表中搜寻是否存在有这个方法。

2.如果在类的methodLists中并未搜索到需要执行的方法,会通过super_class指针找到其父类,并在父类的methodLists中搜寻,然后一直向上寻找一直到根类(也就是NSObject),在过程中如果找到即运行这个方法。

过程为:

实例 --(isa)--> 类 --(super_class)--> 父类  --(super_class)--> ...  --(super_class)--> 根类(NSObject) --(super_class)--> nil

可以说:isa指针建立起了实例与他所属的类之间的关系,super_class指针建立起了类与其父类之间的关系。

类方法的调用过程:

1.当一个类调用方法时,运行时库会根据类对象的isa指针找到这个类的元类(metaClass),然后会在元类的methodLists方法链表中搜寻是否存在有这个方法。

2.如果在元类的methodLists中并未搜索到需要执行的方法,会通过super_class指针找到这个元类的父类,并在它的methodLists中搜寻,然后一直向上寻找一直到根元类(也就是NSObject的元类),在过程中如果找到即运行这个方法。

过程为:

类 --(isa)--> 元类 --(super_class)--> 父类  --(super_class)--> ...  --(super_class)--> 根元类(NSObject的元类) --(super_class)--> 根元类的父类(NSObject)--(super_class)--> nil

可以说:isa指针建立起了类与其元类之间的关系,super_class指针建立起了元类与其父类之间的关系。

那么元类(metaClass)是什么:

我们知道类其实也是一个对象,他存放了实例的信息(成员变量、实例方法等),既然是对象就会有他所属的类,元类(metaClass)就是类对象的类,它存储了类对象的信息(类方法等)。
其实元类也是类对象,你又会问了那么元类的类是什么?所有元类的类都是根元类(也就是NSObject的元类),根元类本身也不例外它的类是其自身,以此来形成闭环。

objc_class中objc_cache的作用:

一个类往往大部分的方法都不会被调用到,但是每次调用方法都需要遍历一次 objc_method_list,这种方式不太合理效率低。如果把经常被调用的函数缓存下来,那可以大大提高函数查询的效率。这也就是 objc_cache 做的事情,调用方法时会将 method_name 作为 key ,method_imp 作为 value 存起来。当再次调用该方法时,可以直接在 cache 里找到,避免去遍历 objc_method_list。

变量的结构

成员变量的定义:
struct objc_ivar {
    // 变量名
    char * _Nullable ivar_name;
    // 变量类型
    char * _Nullable ivar_type;
    // 基地址偏移字节
    int ivar_offset;
#ifdef __LP64__
    int space;
} 

成员变量列表的定义:
struct objc_ivar_list {
    int ivar_count;
#ifdef __LP64__
    int space;
#endif
    /* variable length structure */
    struct objc_ivar ivar_list[1];
}  

方法的结构

// 方法的定义
struct objc_method {
    // 方法名
    SEL _Nonnull method_name;
    // 方法返回值和参数描述字符串
    char * _Nullable method_types;
    // 方法实现
    IMP _Nonnull method_imp;
};

// 方法列表的定义
struct objc_method_list {
    struct objc_method_list * _Nullable obsolete;
    
    int method_count;
#ifdef __LP64__
    int space;
#endif
    /* variable length structure */
    struct objc_method method_list[1];
} 

对于方法几个类型的解释:

SEL:方法选择器。不同类中相同名字的方法所对应的 selector 是相同的。
IMP:方法实现。是一个函数指针,指向方法的实现。
Method:方法的结构体,其中保存了方法的名字,实现和类型描述字符串

对于objc_class结构体中的方法链表为什么是objc_method_list **类型的理解:

原因是为了支持category,在oc初始化时类的实例method_list 会先插入链表,然后再头插category的实例method_list。
这就是为什么category中重写方法会覆盖原类。
又因为是头插法,方法调用时是顺序查找,所以最晚编译的category中的方法会被执行。

对于objc_cache的作用:

objc_cache用来缓存使用过的方法,因为方法非常多,如果每次objc_msgSend调用都从objc_method_list中查找一遍的话效率很低。

对于method_types的解释:
TypeEncoding

//编码值   含意
//c     代表char类型
//i     代表int类型
//s     代表short类型
//l     代表long类型,在64位处理器上也是按照32位处理
//q     代表long long类型
//C     代表unsigned char类型
//I     代表unsigned int类型
//S     代表unsigned short类型
//L     代表unsigned long类型
//Q     代表unsigned long long类型
//f     代表float类型
//d     代表double类型
//B     代表C++中的bool或者C99中的_Bool
//v     代表void类型
//*     代表char *类型
//@     代表对象类型
//#     代表类对象 (Class)
//:     代表方法selector (SEL)
//[array type]  代表array
//{name=type…}  代表结构体
//(name=type…)  代表union
//bnum  A bit field of num bits
//^type     A pointer to type
//?     An unknown type (among other things, this code is used for function pointers)

总结

runtime.h文件包含了很多内容,总结来说有以下几点:

1.定义:
对象、类、父类、元类、方法、属性、协议、分类的定义。

2.结构:
isa指针,super_class指针。对象的的isa指针指向它的类,类的isa指针指向它的元类,类和元类的super_class指针指向他们的父类。

3.注册和创建:
创建新类、销毁类、注册新类、为新类添加方法、变量、属性、协议等;
创建协议、注册协议、为协议添加方法。

4.获取和设置:
获取Object中的信息:object所属的类
获取Class中的信息:类名、父类、元类、成员变量列表、属性列表、方法列表、协议列表
获取Ivar中的信息:变量名、类型
获取Method中的信息:方法名、方法实现、返回值和参数描述字符串
获取Protocol的信息:获取协议名
获取属性的信息:属性名、属性列表、属性值

5.功能方法:
方法交换、方法替换、获取和设置实例的关联对象等


消息发送和转发

message头文件中定义了一组消息发送和转发相关的函数。

OC中的消息发送

在很多语言,比如 C ,调用一个方法其实就是跳到内存中的某一点并开始执行一段代码。没有任何动态的特性,因为这在编译时就决定好了。而OC中所有对象或类的方法调用都是以消息发送的形式进行的。方法调用会被编译器转化为objc_msgSend(receiver, selector)函数的调用,然后开始消息发送的过程。过程如下图:

clipboard.png

类和实例和父类之间的关系小结中提到了方法的调用过程,这里不再赘述。

OC中的消息转发

如果从本类到父类一层层的找也没有找到对应方法的话,就会走消息转发。

1.方法解决:在消息转发前会先走本类的方法+resolveInstanceMethod:(处理找不到的实例方法)或+resolveClassMethod:(处理找不到的类方法)。在这个方法里面可以使用class_addMethod函数向实例添加方法,使得消息发送能够正常进行。

第一步主要是为了让我们给对象添加方法。该方法的返回值无论为YES还是NO,只要没有正确处理都会接着走转发流程第二步
如果是类方法未正确处理,则不会再走后面的转发流程,会直接crash。

2.快速转发:该步会响应forwardingTargetForSelector:方法,返回一个指定的接收者

返回nil,走转发流程第三步
返回非nil对象,走返回对象的消息发送流程,本次消息转发至此结束
如果返回的对象可处理该方法,哪怕是他自己没有该方法但是父类有,则ok(其实就是消息发送流程)
如果返回的对象无法处理该方法,接下来走返回对象的消息转发流程

3.完整转发:如果上一步没有处理者,那么会响应最后这两个方法处理转发,methodSignatureForSelector:返回一个方法签名,forwardInvocation:处理消息执行

methodSignatureForSelector如果返回nil,直接crash
methodSignatureForSelector返回只要是非nil且是NSMethodSignature类型的任何值都ok,
forwardInvocation的参数anInvocation中的signature即为上一步返回的方法签名,
forwardInvocation的参数anInvocation中的selector为导致crash的方法,target为导致crash的对象
forwardInvocation方法可以啥都不处理,或者做任何不会出问题的事,至此本次消息转发结束,也不会crash。


常见问题

[super class]和[self class]
解释:主要是因为class方法的实现在NSObject,子类也都没有重写class方法,并且class方法的内部实现代码是return objc_getClass(self)而不是return objc_getClass("NSObject")。如果把class方法重写掉一切就都会变了。

iOS元类面试一题
解释:由于NSObject的元类的父类是NSObject,所以[NSObject foo]这里调用的是- (void)foo;方法而不是+ (void)foo;方法。可以将类方法改为+ (int)foo;,这个时候会发现NSObject只能调用void foo。然后再将类方法改为+ (int)foo:(int)a;,这个时候就会发现NSObject中可以调用两个方法:void foo、int foo:(int),如果此处这样写[NSObject foo:1],那么结果会因为找不到方法实现而crash。NSObject的元类的父类是NSObject是最主要的原因,如果换个类这样搞就不行了。


使用案例

Runtime 10种用法
给分类(Category)添加属性


后续

Runtime整理(二)——Runtime包含的所有函数


参考文章

Objective-C Runtime 运行时之一:类与对象
iOS-runtime通篇详解-上
Objective-C Runtime
iOS开发-Runtime详解(简书)



花飞蝶舞剑
123 声望72 粉丝

Zcp大官人的iOS小站