Objective-C Runtime
Objective-C Runtime
动态语言,是指程序在运行时可以改变其结构:新的函数可以被引进,已有的函数可以被删除等在结构上的变化,类型的检查是在运行时做的,优点为方便阅读,清晰明了,缺点为不方便调试。
这里有三个名词容易被混淆:
Dynamic Programming Language,即动态语言或动态编程语言
Dynamically Typed Language,即动态类型语言
Statically Typed Language,即静态类型语言
所谓的动态类型语言,指的是类型的检查是在运行时做的;与此相对,静态类型语言的类型判断是在运行前判断(如编译阶段),C#、Java 等就是静态类型语言,静态类型的语言为了达到多态会采取一些类型鉴别手段,如接口、继承,而动态类型的语言却不需要,所以一般动态语言都会采用 dynamic typing,常出现于脚本语言中。
Objective-C 就是一门动态语言,它有一个运行时系统来执行编译的代码。这个运行时系统就是 Objc Runtime,它是一个由 C 和汇编语言编写的库。
Runtime 库的主要工作是封装和找出最终执行的代码。
- 封装 :在
Objc Runtime库中,对象可以用 C 中的结构体表示,而方法可以用 C 中的函数来实现,另外再加上一些额外的特性。这些结构体和函数被Runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。 - 找出方法的最终执行代码:当程序执行
[object doSomething]时,会向消息接收者发送一条消息,Runtime会根据消息接收者是否能响应该消息而做出不同的反应。
Objective-C Runtime 目前有两个版本:Modern Runtime和 Legacy Runtime。Moder Runtime覆盖了64位的 Mac OS X Apps和 iOS Apps,Legacy Runtime是早期用来给32位的 Mac OS X Apps用的。
Objective-C Runtime 中的类与对象
类与对象的基本数据结构
Class
Objective-C 中的类是由 Class 类型来表示的,实际上是一个指向 objc_class 结构体的指针
1 | typedef struct objc_class *Class; |
它的结构体定义如下
1 | struct objc_class { |
这些字段的意义如下:
isa:Objective-C中所有类自身也是对象,这个对象的Class里也有一个isa指针,它指向metaClass元类。super_class:指向该类的父类,如果该类已经是最顶层的根类,则为NULL。name:类名。version:类的版本信息,默认为0。我们可以使用这个字段来提供类的版本信息,这对于对象的序列化非常有用,它可以让我们识别出不同类定义版本中实例变量布局的改变。info:类信息,供运行期使用的一些位标识。instance_size:类的实例变量的大小。ivars:类的成员变量链表。methodLists:方法定义的链表。cache:用于缓存最近使用的方法。当一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这消息的对象。在实际情况里,这个对象只有一部分方法是常用的,很多方法其实很少用或根本用不上。这种情况下,如果每次消息来时,都是在methodLists链表中遍历一遍,性能势必很差。更好的做法是,在我们每次调用过一个方法后,就将这个方法缓存到cache列表中,下次调用就会优先去cache中查找。protocols:协议链表。
当我们向一个 Objective-C 对象发送消息时,Runtime 会根据实例对象的 isa 指针找到这个实例对象所属的类,然后 Runtime 会在类的方法列表及其父类的方法列表中寻找与消息对应的 selector 指向的方法,找到后即运行这个方法。
当创建一个特定类的实例对象时,分配的内存会包含一个 objc_object 数据结构,然后是类的实例变量的数据。NSObject 类的 alloc 和 allocWithZone: 方法使用函数 class_createInstance 来创建 objc_object 数据结构。
1 | struct objc_object { |
常见的 id 是一个 objc_object 结构类型的指针。
objc_cache
objc_class 中的 cache 字段用于缓存调用过的方法,它是一个指向 objc_cache 结构体的指针。
1 | struct objc_cache { |
该结构体的字段描述如下:
mask:一个整数,指定分配的缓存bucket的总数。在方法查找过程中,Runtime使用这个字段来确定开始线性查找数组的索引位置。指向方法的selector指针与该字段做一个AND位操作。occupied:一个整数,指定实际占用的缓存bucket的总数。buckets: 指向Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。如果指针为NULL,表明这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。
元类(Meta Class)
所有的类自身也是一个对象,我们也可以向这个对象发送消息。
当我们向一个对象发送消息时,Runtime 会在这个对象所属的类的方法列表中查找方法;而向一个类发送消息时,会在这个类的 meta-class 的方法列表中查找。
meta-class 存储着一个类的所有类方法。每个类都会有个一个单独的 meta-class ,因为每个类的类方法基本不可能完全相同。
meta-class 的 isa 指针指向基类的 meta-class ,以此作为它的所属类。可以理解为,任何 NSObject 继承体系下的 meta-class 都使用 NSObject 的 meta-class 作为自己的所属类,而基类的 meta-class 的 isa 指针则指向它自己。
对于 NSObject 继承体系来说,其实例方法对体系中的所有实例、类和 meta-class 都是有效的;而类方法对于体系内的所有类和 meta-class 都是有效的。
类与对象操作函数
Runtime 提供了大量函数来操作类与对象。类的操作方法大部分以 class_ 为前缀,对象的操作方法大部分是以 objc_ 或 object_ 为前缀。
类名
1 | // 获取类的类名 |
对于 class_getName 函数来说,如果传入的 cls 为 nil,则返回一个字字符串。
父类(super_class)和元类(meta-class)
1 | // 获取类的父类 |
class_getSuperClass函数,当cls为nil或cls为根类时,返回nil。class_isMetaClass函数,如果cls是元类,返回YES。如果传入的cls为nil,则返回NO。
实例变量大小(instance_size)
1 | // 获取实例大小 |
成员变量(ivar)及属性
objc_class 中,所有的成员变量、属性的信息都是放在链表 ivars 中。ivars 是一个数组,数组中的每个元素是指向 ivar 的指针。
成员变量操作函数
1
2
3
4
5
6
7
8// 获取类中指定名称实例成员变量的信息
Ivar class_getInstanceVariable (Class cls, const char *name);
// 获取类成员变量的信息
Ivar class_getClassVariable (Class cls, const char *name);
// 添加成员变量
BOOL class_addIvar (Class cls, const char *name, size_t size, uint8_t alignment, const char *types);
// 获取整个成员变量列表
Ivar * class_copyIvarList (Class cls, unsigned int *outCount);class_getInstanceVariable函数,它返回一个指向包含name指定的成员变量信息的objc_ivar结构体的指针Ivar。class_getClassVariable函数,目前没有找到关于Objective-C中类变量的信息,一般认为Objective-C不支持类变量。Objective-C不支持向已经存在的类中添加实例变量,但如果通过Runtime来创建一个类的话,就可以使用class_addIvar函数了。需要注意的是,这个函数只能在objc_allocateClassPair与objc_registerClassPair之间调用。另外这个类也不能是元类。class_copyIvarList,返回一个包含成员变量信息的数组,数组中的每个元素是指向该成员变量信息的objc_ivar结构体的指针。这个数组不包含在父类中声明的变量。outCount指针返回数组的大小。需要注意的是,我们必须使用free()来释放这个数组。
属性操作函数
1
2
3
4
5
6
7
8// 获取指定的属性
objc_property_t class_getProperty (Class cls, const char *name);
// 获取属性列表
objc_property_t * class_copyPropertyList (Class cls, unsigned int *outCount);
// 为类添加属性
BOOL class_addProperty (Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount);
// 替换类的属性
void class_replaceProperty (Class cls, const char *name, const objc_property_attribute_t *attributes, unsigned int attributeCount);在
Mac OS X系统中,Runtime提供了几个函数来确定一个对象的内存区域是否可以被垃圾回收器扫描,以处理strong/weak引用。1
2
3
4const uint8_t * class_getIvarLayout (Class cls);
void class_setIvarLayout (Class cls, const uint8_t *layout);
const uint_8 * class_getWeakIvarLayout (Class cls);
void class_setWeakIvarLayout (Class cls, const uint8_t *layout);通常情况下,我们不需要去主动调用这些方法,在调用
objc_registerClassPair时,会生成合理的布局。
方法(methodLists)
1 | // 添加方法 |
class_addMethod的实现会覆盖父类的方法实现,但不会取代本类中已经存在的实现,如果本类中包含一个同名的实现,则函数会返回NO。如果要修改已经存在的实现,可以使用method_setImplementation。一个Objectice-C方法是一个简单的 C 函数,它至少包含两个参数self和_cmd,所以我们实现函数至少需要两个参数。1
2
3void myMethodIMP (id self, SEL _cmd) {
// implementation
}与成员变量不同的是,我们可以动态的为类添加方法,不管这个类是否已经存在。
参数
types是一个描述传递给方法的参数类型的字符数组。class_getInstanceMethod、class_getClassMethod函数,与class_copyMethodList不同的是,这两个函数都会去搜索父类的实现。class_copyMethodList函数,返回包含所有实例方法的数组,如果需要获取类方法,则可使用class_copyMethodList(object_getClass(Class cls), &count)(一个类的实例方法是定义在元类中的)。该列表不包含父类实现的方法。outCount参数返回方法的个数。在获取到列表后,我们需要使用free()来释放它。class_replaceMethod函数,该函数的行为可以分为两种 :如果类中不存在参数name指定的方法,则类似于class_addMethod函数一样会添加方法;如果类中已经存在name指定的方法,则类似于method_setImplementation一样替代原方法的实现。class_getMethodImplementation函数,该函数在向类实例发送消息时会被调用,并返回一个指向方法实现函数的指针。这个函数会比method_getImplementation(Class cls, const char * name)更快。返回的函数指针可能是一个指向Runtime内部的函数,而不一定是方法的实际实现。class_respondsToSelector函数,我们经常使用NSObject类的respondsToSelector:或instanceRespondToSelector:方法来达到同样的目的。
协议 (objc_protocol_list)
协议相关的操作包含以下函数::
1 | // 添加协议 |
版本(version)
版本相关的操作函数:
1 | // 获取版本号 |
动态创建类和对象
Runtime 经常被用到的功能就是在运行时创建类和对象。
动态创建类
动态创建类涉及到一下几个函数
1 | // 创建一个新类和元类 |
objc_allocateClassPair函数 :如果我们要创建一个根类,则指定superClass为Nil。extraBytes通常指定为 0 ,该参数是分配给类和元类对象尾部的索引ivars的字节数。
为了创建一个新类,我们需要调用 objc_allocateClassPair ,然后使用诸如 class_addMethod , class_addIvar 等函数来为新创建的类添加方法、实例变量和属性等。完成这些后,我们需要调用 objc_registerClassPair 函数来注册类,之后这个类就可以在程序中使用了。
实例方法和实例变量应该添加到类自身上,而类方法应该添加到类的元类上。
objc_disposeClassPair函数用于销毁一个类,值得注意的是,如果程序运行中还存在类或其子类的实例,则不能对针对类调用该方法。
动态创建对象
动态创建对象的函数如下:
1 | // 创建类实例 |
class_createInstance函数 :创建实例时,会在默认的内存区域为类分配内存。extraBytes表示分配的额外字节数。这些额外的字节数可用于存储在类定义中所定义的实例变量之外的实例变量。该函数在ARC环境下无法使用。调用
class_createInstance的效果和+alloc方法类似。objc_constructInstance函数 :在指定的位置(bytes)创建类实例。objc_destructInstance函数 :销毁一个类的实例,但不会释放并移除任何与其先关的引用。
实例操作函数
实例操作函数主要是针对创建的实例对象的一系列操作函数。
针对整个对象进行操作的函数
1 | // 返回指定对象的一份拷贝 |
针对对象实例变量进行操作的函数
1 | // 修改类实例的实例变量的值 |
如果实例变量的 Ivar 已经知道,那么调用 objc_getIvar 会比 objc_getInstanceVariable 函数快。相同情况下,object_setIvar 也比 object_setInstanceVariable 快。
针对对象的类进行操作的函数
1 | // 返回给定对象的类名 |
获取类定义
Objective-C Runtime 会自动注册我们代码中定义的所有的类。我们也可以在运行时创建类定义并使用 objc_addClass 来注册它们。
获取类定义的相关函数
1 | // 获取以注册的类定义的列表 |
objc_getClassList函数 :获取已注册的类定义的列表。我们不能假设从该函数中获取的类对象是继承自NSObject体系的,所以在这些类上调用方法时,都应该先检测一下这个方法是否在这个类中实现。- 获取类定义的方法有三个 :
objc_lookUpClass,objc_getClass和objc_getRequiredClass。如果类在运行时未注册,则objc_lookUpClass会返回nil,而objc_getClass会调用类处理回调,并在此确认类是否注册,如果确认未注册,再返回nil。而objc_getRequiredClass函数的操作与objc_getClass相同,只不过没有找到类,就会杀死进程。 objc_getMetaClass函数 :如果指定的类没有注册,则该函数会调用类处理回调,并在此确认类是否注册,如果确认未注册,再返回nil。不过,每个类定义都必须有一个有效的元类定义,所以这个函数总是会返回一个元类定义。不管它是否有效。
Objective-C Runtime 中的成员变量与属性
类型编码(Type Encoding)
作为对 Runtime 的补充,编译器将每个方法的返回值和参数类型编码为一个字符串,并将其与方法的 selector 关联在一起。这种编码方案在其它情况下也是非常有用的,因此我们可以使用 @encod 编译器指令来获取它。当给定一个类型时,@encode 会返回这个类型的字符串编码。
成员变量、属性
Runtime 中关于成员变量和属性的相关数据结构并不多,只有三个。
基础数据类型
Ivar
Ivar 是表示实例变量的类型,其实际上是一个指向 objc_ivar 结构体的指针 :
1 | typedef struct objc_ivar *Ivar; |
objc_property_t
objc_property_t 是声明属性的类型,是一个指向 objc_property 结构体的指针 :
1 | typedef struct objc_property *objc_property_t; |
objc_property_attribute_t
objc_property_attribute_t 定义了属性的特性,它是一个结构体:
1 | typedef struct { |
关联对象(Associated Object)
关联对象是Runtime 中一个非常实用的特性。关联对象类似于成员变量,不过是在运行时添加的。我们通常会把变量(Ivar)放在类声明的头文件中,或者放在类实现的 @implementation 后面。但这有一个缺点,我们不能在分类中添加成员变量。Objective-C 针对这一问题,提出了一个解决方案:即关联对象(Associated Object)。
我们可以把关联对象想象成一个Objective-C 对象(如字典),这个对象通过给定的key连接到类的一个实例上。不过由于使用的是C接口,所以key是一个void 指针。我们还需要指定一个内存管理策略,告诉Runtime 如何管理这个对象的内存,这个内存管理策略可以由一下指定:
1 | OBJC_ASSOCIATION_ASSIGN |
当宿主对象被释放时,会根据指定的内存管理策略来处理关联对象。如果指定的是 OBJC_ASSOCIATION_ASSIGN
,则宿主释放时,关联对象不会被释放;而如果指定的是 retain 或者是 copy ,则宿主释放时,关联对象会被释放。
将一个对象连接到其它对象所需的就是下面两行代码:
1 | static char aKey; |
在这种情况下,self 对象将获取一个新的关联对象 anObject ,且内存管理策略是自动 retian 管理对象,当self 对象释放时,会自动 release 关联对象。另外,如果我们使用同一个key来关联另外一个对象时,也会自动释放之前关联的对象,在这种情况下,先前的关联对象会被妥善的处理掉,并且新的对象会使用它的内存。
1 | // 获取指定key的关联对象 |
使用 objc_removeAssociatedObjects 函数来移除一个关联对象,或者使用 objc_setAssociatedObject 函数将key指定的关联对象设置为nil 。
成员变量、属性的操作方法
成员变量
成员变量的操作函数包括 :
1 | // 获取成员变量名 |
ivar_getOffset函数,对于id或其它对象类型的实例变量,可以调用object_getIvar和object_setIvar来直接访问成员变量,而不使用偏移量。
关联对象
关联对象的操作函数包括 :
1 | // 设置关联对象 |
属性
属性操作相关函数包括 :
1 | // 获取属性名 |
property_copyAttributeValue函数,返回的char*在使用完后需要调用free()释放。property_copuAttributeList函数,返回值在使用完后需要调用free()释放。
Objective-C Runtime 中的方法与消息
基础数据类型
SEL
SEL 又叫做选择器,是表示一个方法的 selector 的指针,定义如下:
1 | typedef struct objc_selector *SEL; |
方法的 selector 用于表示运行时方法的名字。Objective-C 在编译时,会根据每一个方法的名字、参数序列,生成一个唯一的整型标识(Int 类型的地址),这个标识就是 SEL。
两个类之间,不管它们是不是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么方法的 SEL 就是一样的。每一个方法都对应着一个 SEL。所以在 Objective-C 的同一个类(及类的继承体系)中,不能存在两个同名的方法,即使参数类型不同也不行。因为相同的方法只能对应一个 SEL,这导致 Objectice-C 在处理相同方法名且参数个数相同但类型不同的方法方面能力很差。
当然不同的类可以拥有相同的 selector ,这个没有问题。不同类的实例对象执行相同的 selector 时,会在各自的方法列表中根据 selector 去寻找自己对应的 IMP。
工程中的所有的 SEL 组成了一个 Set 集合,Set 特点就是唯一,因此 SEL 也是唯一的。因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的 SEL 就行了,SEL 实际上就是根据方法名 hash 化了的一个字符串,而对于字符串的比较仅仅需要比较它们的地址就可以了。
本质上,SEL 只是一个指向方法的指针(准确地说,只是一个根据方法名 hash 化了的 KEY 值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。
我们可以在运行时添加新的 selector ,也可以在运行时获取已经存在的 selector ,我们可以通过下面三种方式来获取 SEL :
sel_registerName函数Objective-C编译器提供的@selector()NSSelectorFromString()方法
IMP
IMP 实际上是一个函数指针,指向函数实现的首地址。
1 | id (*IMP)(id, SEL, ...) |
这个函数使用当前 CPU 架构实现的标准 C 调用约定。第一个参数是指向 self (如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针)的指针,第二个参数是 selector ,接下来是方法的实际参数列表。
通过取得 IMP ,我们可以跳过 Runtime 的消息传递机制,直接执行 IMP 指向的函数实现,这样就省去了 Runtime 消息传递过程中所做的一系列查找操作。
Method
Method 用于表示类定义中的方法
1 | typedef struct objc_method *Method; |
方法操作相关函数
Runtime 提供了一系列的方法来处理与方法相关的操作,包括方法本身及 SEL 。
方法
方法的相关操作函数如下:
1 | // 调用指定方法的实现 |
method_invoke函数,返回的是实际实现的返回值。参数receiver不能为空。这个方法的效率会比method_getImplementation和method_getName更快。method_getName函数,返回的是一个SEL。如果想获取方法名的C字符串,可以使用sel_getName(method_getName(method))。method_getReturnType函数,类型字符串会被拷贝到dst中。method_setImplementation函数,注意该函数返回值是方法之前的实现。
方法选择器
选择器的相关操作函数包括 :
1 | // 返回给定选择器指定的方法的名称 |
sel_registerName函数:在我们将一个方法添加到类定义时,我们必须在Objective-C Runtime系统中注册一个方法名以获取方法的选择器。
方法调用流程
在 Objective-C 中,消息直到运行时才绑定到方法实现上。编译器会将消息表达式 [receiver message] 转化为一个消息调用的函数,即 objc_msgSend 。这个函数将消息接收者和方法名作为其基础参数。
1 | objc_msgSend(receiver, selector) |
如果消息中还有其它参数,则该方法的形式如下
1 | objc_msgSend(receiver, selector, arg1, arg2, ...) |
这个函数完成了动态绑定的所有事情:
- 首先它找到
selector对应的方法实现。因为同一个方法在不同的类中有不同的实现,所以我们需要依赖于接收者的类来找到确切的实现。 - 它调用方法实现,并将接收者对象及方法的所有参数传递给它。
- 最后,它将实现返回的值作为它自己的返回值。
消息的关键在于结构体 objc_class ,这个结构体中有两个字段是我们在分发消息时关注的:
- 指向父类的指针
- 一个类的方法分发表,即
methodLists。
当我们创建一个新对象时,先为其分配内存,并初始化其成员变量。其中 isa 指针也会被初始化,让对象可以访问类及类的继承体系。
当消息发送给一个对象时,objc_msgSend 通过对象的 isa 指针获取到类的结构体,然后在方法列表里查找方法的 selector 。如果没有找到 selector ,则通过 objc_class 结构体中指向父类的指针找到其父类,并在父类的方法列表里寻找方法的 selector 。依此,会一直沿着类的的继承体系到达 NSObject 类。一旦定位到 selector ,函数就会获取到实现的入口点,并传入相应的参数来执行方法的具体实现。如果没有定位到 selector ,则会走消息转发流程。
为了加速消息的处理,运行时系统缓存使用过的 selector 及对应的方法的地址。
隐藏参数
objc_msgSend 有两个隐藏参数 :
- 消息接受对象
- 方法的
selector
这两个参数为方法的实现提供了调用者的信息,之所以说是隐藏的,是因为它们在定义方法的源代码中没有声明,而是在编译期被插入实现代码的。
虽然这些参数没有声明,但是我们仍然能在代码中引用它们。我们可以用 self 来引用接收者对象,使用 _cmd 来引用选择器。
获取方法地址
Runtime 中方法的动态绑定让我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。不过灵活性的提升也带来了性能上的一些损耗。毕竟我们需要去查找方法的实现,而不像函数调用来得那么直接。当然,方法的缓存一定程度上解决了这一问题。
我们上面提到过,如果想要避开这种动态绑定方式,我们可以获取方法实现的地址,然后像调用函数一样来直接调用它。特别是当我们需要在一个循环内频繁地调用一个特定的方法时,通过这种方式可以提高程序的性能。
NSObject 类提供了methodForSelector:方法,让我们可以获取到方法的指针,然后通过这个指针来调用实现代码。我们需要将methodForSelector:返回的指针转换为合适的函数类型,函数参数和返回值都需要匹配上。
我们通过以下代码来看看methodForSelector:的使用:
1 | void (*setter)(id, SEL, BOOL); |
这里需要注意的就是函数指针的前两个参数必须是id和SEL。
当然这种方式只适合于在类似于for循环这种情况下频繁调用同一方法,以提高性能的情况。另外,methodForSelector:是由Cocoa运行时提供的;它不是Objective-C语言的特性。
消息转发
当一个对象能接收一个消息时,就会走正常的方法调用流程。但如果一个对象无法接收指定消息时,又会发生什么事呢?默认情况下,如果是以[object message]的方式调用方法,如果object无法响应message消息时,编译器会报错。但如果是以perform...的形式来调用,则需要等到运行时才能确定object是否能接收message消息。如果不能,则程序崩溃。
通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。如下代码所示:
1 | if ([self respondsToSelector:@selector(method)]) { |
不过,我们这边想讨论下不使用respondsToSelector:判断的情况。这才是我们这一节的重点。
当一个对象无法接收某一消息时,就会启动所谓”消息转发(message forwarding)“机制,通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃,并由 NSObject 的 doesNotRecognizeSelector 方法抛出 ‘unrecognized selector sent to instance’ 错误信息。不过,我们可以采取一些措施,让我们的程序执行特定的逻辑,而避免程序的崩溃。
消息转发机制基本上分为三个步骤:
- 动态方法解析
- 备用接收者
- 完整转发
下面我们详细讨论一下这三个步骤。
动态方法解析
对象在接收到未知的消息时,首先会调用所属类的类方法+resolveInstanceMethod:(实例方法)或者+resolveClassMethod:(类方法)。在这个方法中,我们有机会为该未知消息新增一个”处理方法””。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。如下代码所示:
1 | void functionForMethod1(id self, SEL _cmd) { |
不过这种方案更多的是为了实现@dynamic属性。
备用接收者
如果在上一步无法处理消息,则 Runtime会继续调以下方法 :
1 | - (id)forwardingTargetForSelector:(SEL)aSelector |
如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。
使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。
这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。
完整消息转发
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:
1 | - (void)forwardInvocation:(NSInvocation *)anInvocation |
运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation方法中选择将消息转发给其它对象。
forwardInvocation:方法的实现有两个任务:
- 定位可以响应封装在
anInvocation中的消息的对象。这个对象不需要能处理所有未知消息。 - 使用
anInvocation作为参数,将消息发送到选中的对象。anInvocation将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。
不过,在这个方法中我们可以实现一些更复杂的功能,我们可以对消息的内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。
还有一个很重要的问题,我们必须重写以下方法:
1 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector |
消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。
1 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector { |
NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。
从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。这取决于具体的实现。
消息转发与多重继承
回过头来看第二和第三步,通过这两个方法我们可以允许一个对象与其它对象建立关系,以处理某些未知消息,而表面上看仍然是该对象在处理消息。通过这种关系,我们可以模拟“多重继承”的某些特性,让对象可以“继承”其它对象的特性来处理一些事情。不过,这两者间有一个重要的区别:多重继承将不同的功能集成到一个对象中,它会让对象变得过大,涉及的东西过多;而消息转发将功能分解到独立的小的对象中,并通过某种方式将这些对象连接起来,并做相应的消息转发。
不过消息转发虽然类似于继承,但NSObject的一些方法还是能区分两者。如respondsToSelector:和isKindOfClass:只能用于继承体系,而不能用于转发链。如果我们想让这种消息转发看起来像是继承,则可以重写这些方法,如以下代码所示:
1 | - (BOOL)respondsToSelector:(SEL)aSelector |
Objective-C Runtime Method Swizzling
Method Swizzling是改变一个selector的实际实现的技术。通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。
例如,我们想跟踪在程序中每一个view controller展示给用户的次数:当然,我们可以在每个view controller的viewDidAppear中添加跟踪代码;但是这太过麻烦,需要在每个view controller中写重复的代码。创建一个子类可能是一种实现方式,但需要同时创建UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子类,这同样会产生许多重复的代码。
这种情况下,我们就可以使用Method Swizzling,如在代码所示:
1 | #import <objc/runtime.h> |
在这里,我们通过method swizzling修改了UIViewController的@selector(viewWillAppear:)对应的函数指针,使其实现指向了我们自定义的xxx_viewWillAppear的实现。这样,当UIViewController及其子类的对象调用viewWillAppear时,都会打印一条日志信息。
上面的例子很好地展示了使用method swizzling来向一个类中注入一些我们新的操作。当然,还有许多场景可以使用method swizzling,在此不多举例。在此我们说说使用method swizzling需要注意的一些问题:
Swizzling应该总是在+load中执行
在Objective-C中,运行时会自动调用每个类的两个方法。+load会在类初始加载时调用,+initialize会在第一次调用类的类方法或实例方法之前被调用。这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证–事实上,如果在应用中没有给这个类发送消息,则它可能永远不会被调用。
Swizzling应该总是在dispatch_once中执行
与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。
选择器、方法与实现
在Objective-C中,选择器(selector)、方法(method)和实现(implementation)是运行时中一个特殊点,虽然在一般情况下,这些术语更多的是用在消息发送的过程描述中。
以下是Objective-C Runtime Reference中的对这几个术语一些描述:
Selector(typedef struct objc_selector *SEL):用于在运行时中表示一个方法的名称。一个方法选择器是一个C字符串,它是在Objective-C运行时被注册的。选择器由编译器生成,并且在类被加载时由运行时自动做映射操作。Method(typedef struct objc_method *Method):在类定义中表示方法的类型Implementation(typedef id (*IMP)(id, SEL, ...)):这是一个指针类型,指向方法实现函数的开始位置。这个函数使用为当前CPU架构实现的标准C调用规范。第一个参数是指向对象自身的指针(self),第二个参数是方法选择器。然后是方法的实际参数。
理解这几个术语之间的关系最好的方式是:一个类维护一个运行时可接收的消息分发表;分发表中的每个入口是一个方法(Method),其中key是一个特定名称,即选择器(SEL),其对应一个实现(IMP),即指向底层C函数的指针。
为了swizzle一个方法,我们可以在分发表中将一个方法的现有的选择器映射到不同的实现,而将该选择器对应的原始实现关联到一个新的选择器中。
调用 _cmd
我们回过头来看看前面新的方法的实现代码:
1 | - (void)xxx_viewWillAppear:(BOOL)animated { |
乍看上去是会导致无限循环的。但令人惊奇的是,并没有出现这种情况。在swizzling的过程中,方法中的[self xxx_viewWillAppear:animated]已经被重新指定到UIViewController类的-viewWillAppear:中。在这种情况下,不会产生无限循环。不过如果我们调用的是[self viewWillAppear:animated],则会产生无限循环,因为这个方法的实现在运行时已经被重新指定为xxx_viewWillAppear:了。
注意事项
Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。虽然它不是最安全的,但如果遵从以下几点预防措施的话,还是比较安全的:
- 总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。
- 避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
- 明白是怎么回事:简单地拷贝粘贴swizzle代码而不理解它是如何工作的,不仅危险,而且会浪费学习Objective-C运行时的机会。阅读
Objective-C Runtime Reference和查看<objc/runtime.h>头文件以了解事件是如何发生的。 - 小心操作:无论我们对Foundation, UIKit或其它内建框架执行Swizzle操作抱有多大信心,需要知道在下一版本中许多事可能会不一样。
Objective-C Runtime 中的协议与分类
Objective-C 中的分类允许我们通过给一个类添加方法来扩充它(但是通过category不能添加新的实例变量),并且我们不需要访问类中的代码就可以做到。
Objective-C 中的协议是普遍存在的接口定义方式,即在一个类中通过@protocol定义接口,在另外类中实现接口,这种接口定义方式也称为“delegation”模式,@protocol声明了可以被其他任何方法类实现的方法,协议仅仅是定义一个接口,而由其他的类去负责实现。
基础数据类型
Category
Category 是表示一个指向分类的结构体的指针。
1 | typedef struct objc_category *Category; |
这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。
Protocol
Protocol 的定义如下
1 | typedef struct objc_object Protocol; |
可以看到,Protocol 其实就是一个对象结构体。
操作函数
Runtime并没有在<objc/runtime.h>头文件中提供针对分类的操作函数。因为这些分类中的信息都包含在objc_class中,我们可以通过针对objc_class的操作函数来获取分类的信息。
而对于Protocol,runtime提供了一系列函数来对其进行操作,这些函数包括:
1 | // 返回指定的协议 |
objc_getProtocol函数,需要注意的是如果仅仅是声明了一个协议,而未在任何类中实现这个协议,则该函数返回的是nil。objc_copyProtocolList函数,获取到的数组需要使用free()来释放objc_allocateProtocol函数,如果同名的协议已经存在,则返回nilobjc_registerProtocol函数,创建一个新的协议后,必须调用该函数以在运行时中注册新的协议。协议注册后便可以使用,但不能再做修改,即注册完后不能再向协议添加方法或协议
需要强调的是,协议一旦注册后就不可再修改,即无法再通过调用protocol_addMethodDescription、protocol_addProtocol和protocol_addProperty往协议中添加方法等。
Objective-C Runtime 补充知识
super
在Objective-C中,如果我们需要在类的方法中调用父类的方法时,通常都会用到super。
如何使用super我们都知道。现在的问题是,它是如何工作的呢?
首先我们需要知道的是super与self不同。self是类的一个隐藏参数,每个方法的实现的第一个参数即为self。而super并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用viewDidLoad方法时,去调用父类的方法,而不是本类中的方法。而它实际上与self指向的是相同的消息接收者。为了理解这一点,我们先来看看super的定义:
1 | struct objc_super { id receiver; Class superClass; }; |
这个结构体有两个成员:
- receiver:即消息的实际接收者
- superClass:指针当前类的父类
当我们使用super来接收消息时,编译器会生成一个objc_super结构体。就上面的例子而言,这个结构体的receiver就是当前对象,与self相同;superClass指向当前类的父类。
接下来,发送消息时,不是调用objc_msgSend函数,而是调用objc_msgSendSuper函数,其声明如下:
1 | id objc_msgSendSuper ( struct objc_super *super, SEL op, ... ); |
该函数第一个参数即为前面生成的objc_super结构体,第二个参数是方法的selector。该函数实际的操作是:从objc_super结构体指向的superClass的方法列表开始查找viewDidLoad 的selector,找到后以objc->receiver去调用这个selector,而此时的操作流程就是如下方式了
1 | objc_msgSend(objc_super->receiver, @selector(viewDidLoad)) |
由于objc_super->receiver就是self本身,所以该方法实际与下面这个调用是相同的:
1 | objc_msgSend(self, @selector(viewDidLoad)) |
库相关操作
库相关的操作主要是用于获取由系统提供的库相关的信息,主要包含以下函数:
1 | // 获取所有加载的Objective-C框架和动态库的名称 |
通过这几个函数,我们可以了解到某个类所有的库,以及某个库中包含哪些类。
块操作
我们都知道block给我们带到极大的方便,苹果也不断提供一些使用block的新的API。同时,苹果在runtime中也提供了一些函数来支持针对block的操作,这些函数包括:
1 | // 创建一个指针函数的指针,该函数调用时会调用特定的block |
imp_implementationWithBlock函数:参数block的签名必须是method_return_type ^(id self, method_args …)形式的。该方法能让我们使用block作为IMP。
弱引用操作
1 | // 加载弱引用指针引用的对象并返回 |
objc_loadWeak函数:该函数加载一个弱指针引用的对象,并在对其做retain和autoreleasing操作后返回它。这样,对象就可以在调用者使用它时保持足够长的生命周期。该函数典型的用法是在任何有使用__weak变量的表达式中使用。●
objc_storeWeak函数:该函数的典型用法是用于__weak变量做为赋值对象时。
宏定义
在Runtime 中,还定义了一些宏定义供我们使用,有些值我们会经常用到,如表示BOOL值的YES/NO;而有些值不常用,如OBJC_ROOT_CLASS。在此我们做一个简单的介绍。
布尔值
1 | #define YES (BOOL)1 |
这两个宏定义定义了表示布尔值的常量,需要注意的是YES的值是1,而不是非0值。
空值
1 | #define nil __DARWIN_NULL |
其中nil用于空的实例对象,而Nil用于空类对象。
分发函数原型
1 | #define OBJC_OLD_DISPATCH_PROTOTYPES 1 |
该宏指明分发函数是否必须转换为合适的函数指针类型。当值为0时,必须进行转换
Objective-C根类
1 | #define OBJC_ROOT_CLASS |
如果我们定义了一个Objective-C根类,则编译器会报错,指明我们定义的类没有指定一个基类。这种情况下,我们就可以使用这个宏定义来避过这个编译错误。该宏在iOS 7.0后可用。
其实在NSObject的声明中,我们就可以看到这个宏的身影,如下所示:
1 | __OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0) |
我们可以参考这种方式来定义我们自己的根类。
局部变量存储时长
1 | #define NS_VALID_UNTIL_END_OF_SCOPE |
该宏表明存储在某些局部变量中的值在优化时不应该被编译器强制释放。
我们将局部变量标记为id类型或者是指向ObjC对象类型的指针,以便存储在这些局部变量中的值在优化时不会被编译器强制释放。相反,这些值会在变量再次被赋值之前或者局部变量的作用域结束之前都会被保存。
关联对象行为
1 | enum { |
Objective-C Runtime 实际应用
动态交换两个方法的实现
当第三方框架或系统原生方法功能不能满足我们的需求的时候,可以通过交换方法实现在保持原有方法功能的基础上,添加额外的功能。
1 | + (void)load { |
动态添加属性
属性赋值的本质就是让属性与一个对象产生关联。
1 | @interface NSObject (AssociatedProperty) |
实现字典转模型的动态转换
利用 Runtime 遍历模型中所有属性,根据模型的属性名,去字典中查找 key,去除对应的值,给模型的属性赋值。
1 | // 最简单实现,不考虑字典中含有数组或对象的情况 |
动态添加方法
如果一个类的方法很多,加载类到内存的时候比较耗费资源,使用动态给类添加方法的方式可以解决。
1 | void methodAddWhenRun(id self, SEL _cmd, id argument ···) {} |
动态变量控制
1 | // 假设要修改 `_propertyName` |
实现 NSCoding 的自动归解档
1 | - (void)encodeWithCoder:(NSCoder *)encoder { |
转载整理自 南风子的技术博客
参考 百度百科 动态语言
参考 白水ln的简书
Objective-C Runtime