互联网科技

iOS开采Runtime的明白与运用

作者:金沙国际官网    发布时间:2020-05-04 23:27     浏览次数 :71

[返回]

由上面一章中,我们了解了什么是RunTime,RunTime用来做什么,下面了解一下Runtime数据结构。

今天整理了iOS中比较难的一个模块Runtime,想要深入学习OC,那Runtime是你必须要熟练掌握的东西,接下来将会详细的说下Runtime。

一、Runtime概念

  • RunTime简称运行时,其中最主要的是消息机制。

  • 对于C语言,函数的调用在编译的时候会决定调用哪个函数,编译完成之后直接顺序执行,无任何二义性。

  • OC的函数调用成为消息发送,属于动态调用过程。在编译的时候并不能决定真正调用哪个函数(事实证明,在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要声明过就不会报错。而C语言在编译阶段就会报错)。

  • 只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。

    其动态性体现在三个方面:

1.动态类型:
即运行时再决定对象的类型。简单说就是id类型,任何对象都可以被id指针所指,只有在运行时才能决定是什么类型。像内置的明确的基本类型都属于静态类型(int、NSString等)。静态类型在编译的时候就能被识别出来。所以,若程序发生了类型不对应,编译器就会发出警告。而动态类型就编译器编译的时候是不能被识别的,要等到运行时(run time),即程序运行的时候才会根据语境来识别。所以这里面就有两个概念要分清:编译时跟运行时。
2.动态绑定:
基于动态类型,在某个实例对象被确定后,其类型便被确定了。该对象对应的属性和响应的消息也被完全确定,这就是动态绑定。比如我们一般向一个NSObject对象发送-respondsToSelector:或者 -instancesRespondToSelector:等来确定对象是否可以对某个SEL做出响应,而在OC消息转发机制被触发之前,对应的类 的+resolveClassMethod:和+resolveInstanceMethod:将会被调用,在此时有机会动态地向类或者实例添加新的方 法,也即类的实现是可以动态绑定的;isKindOfClass也是一样的道理。
3.动态加载:
所谓动态加载就是我们做开发的时候icon图片的时候在Retina设备上要多添加一个张@2x的图片,当设备更换的时候,图片也会自动的替换。

我们知道在Objective-C中,使用[object doSomething]语法并不会马上执行object接受者对象的doSomething方法的代码,而是向object接受者对发送一条doSomething消息,这条消息可能由object接受者对来处理,也可能由转发给其他对象来处理,也有可能假装没有接收到这条消息而没有处理。

Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。对于Objective-C来说,这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行。Runtime基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。在Runtime中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,让OC的面向对象编程变为可能。找出方法的最终执行代码:当程序执行[object doSomething]时,会向消息接收者发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。

二、Runtime数据结构

其实[object doSomething]被编译器转化为:

Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针。

1.Class

Objective-C类是由Class类型来表示,Class 其实是指向 objc_class 结构体的指针

typedef struct objc_class *Class;

我们可以从<objc/runtime.h>里面看到类的定义

struct object_class{
    Class isa OBJC_ISA_AVAILABILITY;
#if !__OBJC2__
     Class super_class                        OBJC2_UNAVAILABLE;  // 父类
     const char *name                         OBJC2_UNAVAILABLE;  // 类名
     long version                             OBJC2_UNAVAILABLE;  // 类的版本信息,默认为0
     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;
id objc_msgSend ( id self, SEL op, ... );
typedef struct object_class *Class
2.id

id 是一个结构体指针类型,它可以指向 Objective-C 中的任何对象。从<objc/objc.h>中可以看出id其实是指向objc_object结构体的指针。objc_object只有一个成员变量 isa,对象可以通过 isa 指针找到其所属的类,这也是为什么id可以表示任意对象的原因。isa 是一个 Class 类型的成员变量。
注意:在KVO中,isa在运行时会被修改,指向一个中间类,对于编译器而言,isa的指向才是最真实的类型。

/// Represents an instance of a class.
struct objc_object {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};

/// A pointer to an instance of a class.
typedef struct objc_object *id;

当我们向一个Objective-C对象发送消息时,运行时库会根据
实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表由super_class指针找到父类的方法列表直至根类NSObject中去寻找与消息对应的selector指向的方法,找到后即运行这个方法。

下面从两个数据结构idSEL来逐步分析和理解Runtime有哪些重要的数据结构。

它的定义如下:查看objc/runtime.h中objc_class结构体的定义如下:

3.元类(meta-Class)

类自身也是一种对象,可以叫做“类对象”。类对象包含一个指向其类的一个isa指针( Class _Nonnull isa )

为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,meta-class中存储着一个类的所有类方法。
所以,调用类方法的这个类对象的isa指针指向的就是meta-class。当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。

下图是一个经典的类及相应meta-class类的一个继承体系图解:

图片 1

image.png

从图中可以看出:
1.每个实例对象的类都是类对象,每个类对象的类都是元类对象,每个元类对象的类都是根元类(root meta class的isa指向自身)。即任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。
2.类对象的父类最终继承自根类对象NSObject,NSObject的父类为nil
3.元类对象(包括根元类)的父类最终继承自根类对象NSObject

objc_msgSend方法里面的第一个参数的数据类型id,通用类型指针,能够表示任何对象。

struct object_class{ Class isa OBJC_ISA_AVAILABILITY;#if !__OBJC2__ Class super_class OBJC2_UNAVAILABLE; // 父类 const char *name OBJC2_UNAVAILABLE; // 类名 long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0 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;

三、方法和消息

图片 2Paste_Image.png

说明其执行过程:NSArray *array = [[NSArray alloc] init];

1.SEL
typedef struct objc_selector *SEL;

SEL sel1 = @selector(func1);
SEL sel2 = NSSelectorFromString(func2);

SEL本质上是一个指向方法的指针。Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识即SEL,每一个方法都对应着一个SEL。所以即使返回值类型或参数类型不同,方法名相同也会报错。

查看源文件,可以看出id其实就是一个指向objc_object结构体指针,它包含一个Class isa成员,根据Class isa指针就可以找到对象所属的类。

objc_object是表示一个类的实例的结构体它的定义如下(objc/objc.h):

2.IMP
id (*IMP)(id, SEL,...)

IMP的本质是函数指针,指向方法实现的地址,直接通过IMP就可以找到各个方法。这样效率更高,因为绕过了消息传递阶段,直接定位。
SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的IMP。

从源文件看出,Objective-C的对象就是一个包含isa指针的数据结构,而isa指针的数据类型是ClassClass表示对象所属的类。

struct objc_object{ Class isa OBJC_ISA_AVAILABILITY;};typedef struct objc_object *id;
3.Method
typedef struct objc_method *Method
struct objc_method{
    SEL method_name      OBJC2_UNAVAILABLE; // 方法名
    char *method_types   OBJC2_UNAVAILABLE;
    IMP method_imp       OBJC2_UNAVAILABLE; // 方法实现
}

可以看出method包含SEL和IMP,在实现方法交换时,主要原理就是交换SEL和IMP的映射关系。

参考:

图片 3Paste_Image.png

可以看到,这个结构体只有一个字体,即指向其类的isa指针。这样,当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法,找到后即运行这个方法。

从源文件看出,Class其实就是一个objc_class结构体指针。objc_class结构体定义如下:

meta-class是一个类对象的类。在上面我们提到,所有的类自身也是一个对象,我们可以向这个对象发送消息。既然是对象,那么它也是一个objc_object指针,它包含一个指向其类的一个isa指针。那么,这个isa指针指向什么呢?

isa:在Objective-C中,所有的类自身也是一个对象,即类对象。在这个类对象里面也有一个isa指针,它指向metaClass。super_class:指向该类的父类,如果该类已经是最顶层的根类(如NSObject),则super_class为NULL。name:这个类的类名。version:提供类的版本信息,这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。info:类信息,供运行期使用的一些位标识。instance_size:该类的实例变量大小。ivars:该类的成员变量链表。methodLists:方法定义的链表。protocols:协议链表。cache:一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。

为了调用类方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念,meta-class中存储着一个类的所有类方法。

注意:

所以,调用类方法的这个类对象的isa指针指向的就是meta-class当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。

  • 在面向对象设计中,一切都是对象,Class在设计中本身也是一个对象。
  • 由此可见,结构体objc_class也是继承objc_object,说明Class在设计中本身也是一个对象。

再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。

Objective-C中,所有的类自身也是一个对象,这个对象里面也有一个isa指针,它指向metaClass,向这个对象发送消息。

即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。

图片 4Paste_Image.png

通过上面的描述,再加上对objc_class结构体中super_class指针的分析,我们就可以描绘出类及相应meta-class类的一个继承体系了,如下代码

从图中看出:

图片 5Snip20160501_1.png

  • 当发送一个实例方法的消息时,isa指针会在这个类的实例方法列表中查找;
  • 当发送一个类方法的消息时,isa指针会在这个类的meta-class的方法列表中查找。meta-class之所以重要,是因为它存储着一个类的所有类方法。
  • 每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。

Category是表示一个指向分类的结构体的指针,其定义如下:

Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己,这样就形成了一个完美的闭环。

typedef struct objc_category *Categorystruct objc_category{ char *category_name OBJC2_UNAVAILABLE; // 分类名 char *class_name OBJC2_UNAVAILABLE; // 分类所属的类名 struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表 struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表 struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分类所实现的协议列表}

objc_msgSend函数第二个参数类型为SEL,它是selectorObjc中的表示类型(Swift中是Selector类)。selector是方法选择器,可以理解为区分方法的id,而这个id的数据结构是SEL,即表示一个方法的selector的指针。

这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。可发现,类别中没有ivar成员变量指针,也就意味着:类别中不能够添加实例变量和属性

图片 6Paste_Image.png

struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
  • 方法的selector用于表示运行时方法的名字,Objective-C在编译时,会依据每一个方法的名字、参数序列,生成一个唯一的整型标识(int类型的地址),这个标识就是SEL
  • Objective-C中,只要方法名相同,那么方法的SEL就是一样的,每一个方法都对应着一个SEL,所以在Objective-C中,同一个类中或者这个类的继承体系中,不能存在2个同名的方法,不同的类可以拥有相同的selector,不同的类的实例对象执行相同的selector,会在各自的方法列表中根据selector去寻找对应的IMP
  • 在本质上,SEL只是一个指向方法的指针(被hash化得KEY值),能提高方法的查询速度。

我们先看看关联API,只有这三个API,使用也是非常简单的:

IMP实际上是一个函数指针,指向方法实现的首地址。其定义如下:

参数说明:object:与谁关联,通常是传selfkey:唯一键,在获取值时通过该键获取,通常是使用staticconst void *来声明value:关联所设置的值policy:内存管理策略,比如使用copy

id (id, SEL, ...)
void objc_setAssociatedObject(id object, const void *key, id value, objc _AssociationPolicy policy)

第一个参数是指向 self 的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器( selector ),接下来是方法的实际参数列表。

参数说明:object:与谁关联,通常是传self,在设置关联时所指定的与哪个对象关联的那个对象key:唯一键,在设置关联时所指定的键

SEL就是为了查找方法的最终实现IMP的,由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的IMP

id objc_getAssociatedObject(id object, const void *key)

void objc_removeAssociatedObjects(id object)

Method是一种代表类中的某个方法的类型。

使用场景:可以在类别中添加属性