本篇文章是runtime知识点的整理,以便于从此学习和快速查找。html
本篇文章分为2个章节:ios
- (一)Runtime的介绍和知识点
- (二)Runtime包含的全部函数
- 介绍
- runtime.h
- 消息发送和转发
- 常见问题
- 使用案例
在讲runtime以前,咱们先来明白什么是动态语言。面试
动态语言:是指程序在 运行时能够改变其结构:新的函数能够被引进,已有的函数能够被删除等在结构上的变化,类型的检查是在运行时作的,优势为方便阅读,清晰明了,缺点为不方便调试。 动态语言-百度百科
咱们都知道OC是一门动态语言,由于它objective-c
1.能够在运行时新增方法(使用class_addMethod为类新增方法)2.能够改变类的结构(使用class_replaceMethod替换方法的实现等)编程
3.运行时检查类型(运行时多态,id类型)segmentfault
动态语言特性意味着OC不只须要一个编译器,还须要一个运行时系统Objc Runtime来实现上述的操做。Objc Runtime实际上是一组API,它基本上是用C和汇编写的,是它让C语言得到了面向对象的能力而蜕变为OC,并使OC语言拥有了动态语言特性。它主要功能是下面两点:缓存
1.构建和改变类的结构(在objc/runtime.h文件中定义)2.处理运行时消息的发送(在objc/message.h文件中定义)app
总结:OC中的runtime是OC语言中实现面向对象和动态语言特性的一组API。ide
咱们来看一下runtime.h文件,看它是如何构建出OC中的类,并经过哪些方法来改变类结构的。函数
若要构建出类,咱们要先了解类,咱们知道类的概念是面向对象设计实现封装的基础,面向对象编程的三大特性是:封装、继承、多态
1.封装:抽象出数据类型和数据操做构成一个总体。那么咱们若是想要构建出类,就须要类中有这些:成员变量、方法
2.继承:类之间的父子关系,例如A is a B(A属于B,B是父类,A是子类)isa的由来。
这就要求咱们创建起的类之间的父子关系
3.多态:引用变量指向的具体类型和经过该引用变量发出的方法调用在编程时不肯定。通俗点举例说明就是A类型指针能够指向B、C、D类型的对象,在编程时你没法知道这个指针指向的具体是哪一个对象,这种A可以指向B、C、D的特性就是多态。
这就要求咱们的类和实例可以正确的响应消息。这点会在后面的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中类的雏形就初步构建出来了。下面分别来详细介绍他们
先上经典的图,如下内容请结合图来一块儿食用。
经过方法调用的过程咱们能够了解到类和实例和其父类之间的关系。
实例的方法调用过程:
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:方法的结构体,其中保存了方法的名字,实现和类型描述字符串
对于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头文件中定义了一组消息发送和转发相关的函数。
在不少语言,好比 C ,调用一个方法其实就是跳到内存中的某一点并开始执行一段代码。没有任何动态的特性,由于这在编译时就决定好了。而OC中全部对象或类的方法调用都是以消息发送的形式进行的。方法调用会被编译器转化为objc_msgSend(receiver, selector)函数的调用,而后开始消息发送的过程。过程以下图:
在类和实例和父类之间的关系小结中提到了方法的调用过程,这里再也不赘述。
若是从本类到父类一层层的找也没有找到对应方法的话,就会走消息转发。
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)添加属性
Objective-C Runtime 运行时之一:类与对象
iOS-runtime通篇详解-上
Objective-C Runtime
iOS开发-Runtime详解(简书)