摘录自:
http://www.cocoachina.com/ios/20141018/9960.html
http://blog.zuics.com/runloop-runtime/
http://www.cocoachina.com/ios/20141008/9844.html
http://southpeak.github.io/blog/2014/10/25/objective-c-runtime-yun-xing-shi-zhi-lei-yu-dui-xiang/
RunTime简称运行时。就是系统在运行的时候的一些机制,其中最主要的是消息机制。
OC的函数调用称为消息发送,属于动态调用过程。在编译的时候并不能决定真正调用哪个函数,只有在真正运行的时候才会根据函数的名称找到对应的函数来调用。事实上,在编译阶段,OC可以调用任何函数,即使这个函数并未实现,只要申明过就不会报错。
第一部分:初识RunTime
1、示例代码
Father.h
1 | @interface Father : NSObject |
Father.m
1 | #import "Father.h" |
main.m
1 | #import <Foundation/Foundation.h> |
2、方法调用如何变成了消息发送
Objective-C之所以能做到运行时才查找要执行的函数主要归功于runTime的SDK。下面我们来看看Objective-C是怎么让程序拥有运行时特性的:
在runTime的SDK下有一个objc_msgSend()的方法
1 | OBJC_EXPORT id objc_msgSend(id self, SEL op, …) |
在main函数中对于[obj callMe]
这样一个简单的调用。在编译时RunTime会把代码转化成
1 | objc_msgSend(obj,@selector(callMe)); |
3、理解对象的真实面目
command+鼠标左键,依次进入下面几个类
(1)NSObject
1 | @interface NSObject <NSObject> { |
发现在NSObjcet中存在一个Class的isa指针,指向对象对应的Class类
(2)Class
1 | typedef struct objc_class *Class; |
我们发现Class是一个指向objc_class
的结构体的指针。
(3)objc_class
1 | struct objc_class { |
(4)解释结构体objc_class的成员
总览:
1 | Class isa; // 指向metaclass |
详解:
- Class isa:指向metaclass,也就是静态的Class。一般一个Obj对象中的isa会指向普通的Class,这个Class中存储普通成员变量和对 象方法(“-”开头的方法),普通Class中的isa指针指向静态Class,静态Class中存储static类型成员变量和类方法(“+”开头的方 法)。
需要注意的是在Objective-C中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类) Class super_class:指向父类,如果这个类是根类(如NSObject或NSProxy),则为nil。
下面一张图片很好的描述了类和对象的继承关系:
注意:所有metaclass中isa指针都指向根metaclass。而根metaclass则指向自身。Root metaclass是通过继承Root class产生的。与root class结构体成员一致,也就是前面提到的结构。不同的是Root metaclass的isa指针指向自身。
cache:用于缓存最近使用的方法。一个接收者对象接收到一个消息时,它会根据isa指针去查找能够响应这个消息的对象。在实际使用中,这个对象只有一部分方法是常用的,很多方法其实很少用或者根本用不上。这种情况下,如果每次消息来时,我们都是methodLists中遍历一遍,性能势必很差。这时,cache就派上用场了。在我们每次调用过一个方法后,这个方法就会被缓存到cache列表中,下次调用的时候runtime就会优先去cache中查找,如果cache没有,才去methodLists中查找方法。这样,对于那些经常用到的方法的调用,但提高了调用的效率。
针对cache,我们用下面例子来说明其执行过程:
1
NSArray *array = [[NSArray alloc] init];
其流程是:
[NSArray alloc]先被执行。因为NSArray没有+alloc方法,于是去父类NSObject去查找。
检测NSObject是否响应+alloc方法,发现响应,于是检测NSArray类,并根据其所需的内存空间大小开始分配内存空间,然后把isa指针指向NSArray类。同时,+alloc也被加进cache列表里面。
接着,执行-init方法,如果NSArray响应该方法,则直接将其加入cache;如果不响应,则去父类查找。
在后期的操作中,如果再以[[NSArray alloc] init]这种方式来创建数组,则会直接从cache中取出相应的方法,直接调用。
version:我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。
4、理解调用方法的实质
@selector(callMe):这是一个SEL方法选择器。SEL其主要作用是快速的通过方法名字(callMe)查找到对应方法的函数指针,然后调用其函 数。SEL其本身是一个int类型的一个地址,地址中存放着方法的名字。对于一个类中。每一个方法对应着一个SEL。所以iOS类中不能存在2个名称相同 的方法,即使参数类型不同,因为SEL是根据方法名字生成的,相同的方法名称只能对应一个SEL。
下面我们就来看看具体消息发送之后是怎么来动态查找对应的方法的。
- 首先,编译器将代码[obj callMe]转化为objc_msgSend(obj, @selector (callMe))
- 在objc_msgSend函数中,首先通过obj的isa指针找到obj对应的class。
在Class中先去cache中 通过SEL查找对应函数指针method_imp
(猜测cache中method列表是以SEL为key通过hash表来存储的,这样能提高函数查找速度),若 cache中未找到。再去methodList中查找,若methodlist中未找到,则取superClass中查找。若能找到,则将method加 入到cache中,以方便下次查找,并通过method中的函数指针跳转到对应的函数中去执行。
根据函数指针method_imp调用响应的函数。
另一种解释
1、当我们调用某个对象的对象方法时,它会首先在自身isa指针指向的类(class)methodLists中查找该方法,如果找不到则会通过class的super_class指针找到其父类,然后从其methodLists中查找该方法,如果仍然找不到,则继续通过 super_class向上一级父类结构体中查找,直至根class;
2、当我们调用某个类方法时,它会首先通过自己的isa指针找到metaclass,并从其methodLists中查找该类方法,如果找不到则会通过metaclass的super_class指针找到父类的metaclass结构体,然后从methodLists中查找该方法,如果仍然找不到,则继续通过super_class向上一级父类结构体中查 找,直至根metaclass;
估计大家和我一样,看到这里,好像明白了一些,但许多地方似懂非懂,没关系,第一部分内容就是热热身,不理解没关系,我们继续耐心往下学习。
第二部分:RunTime解惑
1、Runtime 是什么?
Objective-C 的 Runtime 是一个运行时库(Runtime Library),它是一个主要使用 C 和汇编写的库,为 C 添加了面相对象的能力并创造了 Objective-C。这就是说它在类信息(Class information) 中被加载,完成所有的方法分发,方法转发,等等。Objective-C runtime 创建了所有需要的结构体,让 Objective-C 的面相对象编程变为可能。
2、Runtime 做什么?
Runtime库主要做下面几件事:
封装:在这个库中,对象可以用C语言中的结构体表示,而方法可以用C函数来实现,另外再加上了一些额外的特性。这些结构体和函数被runtime函数封装后,我们就可以在程序运行时创建,检查,修改类、对象和它们的方法了。
找出方法的最终执行代码:当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),runtime会根据消息接收者是否能响应该消息而做出不同的反应。
3、Modern Runtime与Legacy Runtime
Modern Runtime(现代的 Runtime) 和 Legacy Runtime(过时的 Runtime)。Modern Runtime:覆盖所有 64 位的 Mac OS X 应用和所有 iPhone OS 的应用。
Legacy Runtime: 覆盖其他的所有应用(所有 32 位的 Mac OS X 应用)。
4、Selector
在 Objective-C 中 selector 只是一个 C 的数据结构,用于表示一个你想在一个对象上执行的 Objective-C 方法。在 runtime 中的定义像这样
1 | typedef struct objc_selector *SEL; |
像这样使用
1 | SEL aSel = @selector(callMe); |
5、Message
消息是方括号 ‘[]’ 中的那部分,由你要向其发送消息的对象(target),你想要在上面执行的方法(method)还有你发送的参数(arguments)组成。 Objective-C 的消息和 C 函数调用是不同的。事实上,你向一个对象发送消息并不意味着它会执行它。Object(对象)会检查消息的发送者,基于这点再决定是执行一个不同的方法还是转发消息到另一个目标对象上。
6、Class、objc_class、objc_object
先看看objc.h中Class定义部分:1
2
3
4
5
6
7
8
9
10
11
12#if !OBJC_TYPES_DEFINED
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;
/// Represents an instance of a class.
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
/// A pointer to an instance of a class.
typedef struct objc_object *id;
#endif
Objective-C类是由Class类型来表示的,它实际上是一个指向objc_class结构体的指针
objc_object: 可以看到,这个结构体只有一个指向其类(Class)的isa指针。当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类(Class)。Runtime库会在类(Class)的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法。找到后即运行这个方法。
当创建一个特定类的实例对象时,分配的内存包含一个objc_object数据结构,然后是类的实例变量的数据。NSObject类的alloc和allocWithZone:方法使用函数class_createInstance来创建objc_object数据结构。
id 指针:是一个objc_object结构类型的指针。默认情况下 id 指针除了告诉我们它们是 Objective-C 对象外没有其他用了。当你有一个 id 指针,然后你就可以问这个对象是什么类的,看看它是否响应一个方法,等等,然后你就可以在知道这个指针指向的是什么对象后执行更多的操作了。
7、objc_cache
第一部分解释结构体objc_class的成员时提到了objc_cache1
struct objc_cache *cache; // 类中方法列表 Cache cache;
objc_cache结构如下:
1 | struct objc_cache { |
mask:一个整数,指定分配的缓存bucket的总数。在方法查找过程中,Objective-C runtime使用这个字段来确定开始线性查找数组的索引位置。指向方法selector的指针与该字段做一个AND位操作(index = (mask & selector))。这可以作为一个简单的hash散列算法。
occupied:一个整数,指定实际占用的缓存bucket的总数。
buckets:指向Method数据结构指针的数组。这个数组可能包含不超过mask+1个元素。需要注意的是,指针可能是NULL,表示这个缓存bucket没有被占用,另外被占用的bucket可能是不连续的。这个数组可能会随着时间而增长。
8、元类(Meta Class)
第一部分解释结构体objc_class的成员时,提到了isa指向metaclass1
Class isa; // 指向metaclass
我们已经提到过多次:所有的类自身也是一个对象。因此我们可以向这个对象发送消息(即调用类方法)。如:
1 | NSArray *array = [NSArray array]; |
这个例子中,+array消息发送给了NSArray类,而这个NSArray也是一个对象。既然是对象,那么它也是一个objc_object指针,它包含一个指向其类(Class)的一个isa指针。那么这些就有一个问题了,这个isa指针指向什么呢?为了调用+array方法,这个类的isa指针必须指向一个包含这些类方法的一个objc_class结构体。这就引出了meta-class的概念:meta-class是一个类对象的类。
当我们向一个对象发送消息时,runtime会在这个对象所属的这个类的方法列表中查找方法;而向一个类发送消息时,会在这个类的meta-class的方法列表中查找。
meta-class之所以重要,是因为它存储着一个类的所有“类方法”。每个类都会有一个单独的meta-class,因为每个类的类方法基本不可能完全相同。
再深入一下,meta-class也是一个类,也可以向它发送一个消息,那么它的isa又是指向什么呢?为了不让这种结构无限延伸下去,Objective-C的设计者让所有的meta-class的isa指向基类的meta-class,以此作为它们的所属类。即,任何NSObject继承体系下的meta-class都使用NSObject的meta-class作为自己的所属类,而基类的meta-class的isa指针是指向它自己。这样就形成了一个完美的闭环。
示例:测试meta-class1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37#import <Foundation/Foundation.h>
#import <objc/runtime.h>
void TestMetaClass(id self, SEL _cmd) {
NSLog(@"This objcet is %p", self);
NSLog(@"Class is %@, super class is %@", [self class], [self superclass]);
Class currentClass = [self class];
for (int i = 0; i < 4; i++) {
NSLog(@"Following the isa pointer %d times gives %p", i, currentClass);
currentClass = objc_getClass((__bridge void *)currentClass);
}
NSLog(@"NSObject's class is %p", [NSObject class]);
NSLog(@"NSObject's meta class is %p", objc_getClass((__bridge void *)[NSObject class]));
}
void ex_registerClassPair() {
Class newClass = objc_allocateClassPair([NSError class], "TestClass", 0);
class_addMethod(newClass, @selector(testMetaClass2), (IMP)TestMetaClass, "v@:");
objc_registerClassPair(newClass);
id instance = [[newClass alloc] initWithDomain:@"some domain" code:0 userInfo:nil];
[instance performSelector:@selector(testMetaClass2)];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
ex_registerClassPair();
}
return 0;
}
注意:记住
#import <objc/runtime.h>
1 | //打印输出 |
这个例子是在运行时创建了一个NSError的子类TestClass,然后为这个子类添加一个方法testMetaClass2,这个方法的实现是TestMetaClass函数。
我们在for循环中,我们通过objc_getClass来获取对象的isa,并将其打印出来,依此一直回溯到NSObject的meta-class。分析打印结果,可以看到最后指针指向的地址是0x0,即NSObject的meta-class的类地址。
这里需要注意的是:我们在一个类对象调用class方法是无法获取meta-class,它只是返回类而已。
第三部分:RunTime-类与对象操作函数
runtime提供了大量的函数来操作类与对象。类的操作方法大部分是以class为前缀的,而对象的操作方法大部分是以objc或object_为前缀。
一、类相关操作函数
1、类名
1 | // 获取类的类名 |
2、父类(super_class)和元类(meta-class)
1 | // 获取类的父类 |
class_getSuperclass函数,当cls为Nil、nil或者cls为根类时,返回nil(不是Nil)。不过通常我们可以使用NSObject类的superclass方法来达到同样的目的。
class_isMetaClass函数,如果是cls是元类,则返回YES;如果否或者传入的cls为Nil,则返回NO。
3、实例变量大小(instance_size)
1 | // 获取实例大小 |
4、成员变量(ivars)及属性
在objc_class中,所有的成员变量、属性的信息是放在链表ivars中的。ivars是一个数组,数组中每个元素是指向Ivar(变量信息)的指针。runtime提供了丰富的函数来操作这一字段。大体上可以分为以下几类:
(1)、成员变量操作函数,主要包含以下函数:
1 | // 获取类中指定名称实例成员变量的信息 |
- class_getInstanceVariable函数,它返回一个指向包含name指定的成员变量信息的objc_ivar结构体的指针(Ivar)。
Ivar
1
2
3 > /// An opaque type that represents an instance variable.
> typedef struct objc_ivar *Ivar;
>
1
2
3
4
5
6
7
8
9 > struct objc_ivar {
> char *ivar_name OBJC2_UNAVAILABLE;
> char *ivar_type OBJC2_UNAVAILABLE;
> int ivar_offset OBJC2_UNAVAILABLE;
> #ifdef __LP64__
> int space OBJC2_UNAVAILABLE;
> #endif
> }
>
class_getClassVariable函数,目前没有找到关于Objective-C中类变量的信息,一般认为Objective-C不支持类变量。注意,返回的列表不包含父类的成员变量和属性。
Objective-C不支持往已存在的类中添加实例变量,因此不管是系统库提供的提供的类,还是我们自定义的类,都无法动态添加成员变量。但如果我们通过运行时来创建一个类的话,又应该如何给它添加成员变量呢?这时我们就可以使用class_addIvar函数了。不过需要注意的是,这个方法只能在objc_allocateClassPair函数与objc_registerClassPair之间调用。另外,这个类也不能是元类。成员变量的按字节最小对齐量是1<<alignment。这取决于ivar的类型和机器的架构。如果变量的类型是指针类型,则传递log2(sizeof(pointer_type))。
class_copyIvarList函数,它返回一个指向成员变量信息的数组,数组中每个元素是指向该成员变量信息的objc_ivar结构体的指针。这个数组不包含在父类中声明的变量。outCount指针返回数组的大小。需要注意的是,我们必须使用free()来释放这个数组。
(2)、属性操作函数:
1 | // 获取指定的属性 |
这一种方法也是针对ivars来操作,不过只操作那些是属性的值。
(3)、在MAC OS X系统中,我们可以使用垃圾回收器。runtime提供了几个函数来确定一个对象的内存区域是否可以被垃圾回收器扫描,以处理strong/weak引用。这几个函数定义如下:
1 | const uint8_t * class_getIvarLayout ( Class cls ); |
但通常情况下,我们不需要去主动调用这些方法;在调用objc_registerClassPair时,会生成合理的布局。在此不详细介绍这些函数。
5、方法(methodLists)
方法操作主要有以下函数:
1 | // 添加方法 |
class_addMethod的实现会覆盖父类的方法实现,但不会取代本类中已存在的实现,如果本类中包含一个同名的实现,则函数会返回NO。如果要修改已存在实现,可以使用method_setImplementation。一个Objective-C方法是一个简单的C函数,它至少包含两个参数_self和_cmd。所以,我们的实现函数(IMP参数指向的函数)至少需要两个参数,如下所示:
1
2
3
4void myMethodIMP(id self, SEL _cmd)
{
// implementation ....
}与成员变量不同的是,我们可以为类动态添加方法,不管这个类是否已存在。
另外,参数types是一个描述传递给方法的参数类型的字符数组,这就涉及到类型编码,我们将在后面介绍。
class_getInstanceMethod、class_getClassMethod函数,与class_copyMethodList不同的是,这两个函数都会去搜索父类的实现。
class_copyMethodList函数,返回包含所有实例方法的数组,如果需要获取类方法,则可以使用
class_copyMethodList(object_getClass(cls), &count)
(一个类的实例方法是定义在元类里面)。该列表不包含父类实现的方法。outCount参数返回方法的个数。在获取到列表后,我们需要使用free()方法来释放它。class_replaceMethod函数,该函数的行为可以分为两种:如果类中不存在name指定的方法,则类似于class_addMethod函数一样会添加方法;如果类中已存在name指定的方法,则类似于method_setImplementation一样替代原方法的实现。
class_getMethodImplementation函数,该函数在向类实例发送消息时会被调用,并返回一个指向方法实现函数的指针。这个函数会比
method_getImplementation(class_getInstanceMethod(cls, name))
更快。返回的函数指针可能是一个指向runtime内部的函数,而不一定是方法的实际实现。例如,如果类实例无法响应selector,则返回的函数指针将是运行时消息转发机制的一部分。class_respondsToSelector函数,我们通常使用NSObject类的respondsToSelector:或instancesRespondToSelector:方法来达到相同目的。
5、协议(objc_protocol_list)
1 | // 添加协议 |
class_conformsToProtocol函数可以使用NSObject类的conformsToProtocol:方法来替代。
class_copyProtocolList函数返回的是一个数组,在使用后我们需要使用free()手动释放。
6、版本(version)
1 | // 获取版本号 |
7、其它
runtime还提供了两个函数来供CoreFoundation的tool-free bridging使用,即:
1 | Class objc_getFutureClass ( const char *name ); |
通常我们不直接使用这两个函数。
8、Example
MyClass.h
1 | #import <Foundation/Foundation.h> |
MyClass.m
1 | #import "MyClass.h" |
main.m
1 | #import <Foundation/Foundation.h> |
二、动态创建类和对象
1、动态创建类
1 | // 创建一个新类和元类 |
objc_allocateClassPair函数:如果我们要创建一个根类,则superclass指定为Nil。extraBytes通常指定为0,该参数是分配给类和元类对象尾部的索引ivars的字节数。
为了创建一个新类,我们需要调用
objc_allocateClassPair
。然后使用诸如class_addMethod
,class_addIvar
等函数来为新创建的类添加方法、实例变量和属性等。完成这些后,我们需要调用objc_registerClassPair
函数来注册类,之后这个新类就可以在程序中使用了。实例方法和实例变量应该添加到类自身上,而类方法应该添加到类的元类上。
objc_disposeClassPair
函数用于销毁一个类,不过需要注意的是,如果程序运行中还存在类或其子类的实例,则不能调用针对类调用该方法。
补充:Method 与 IMP
Method
1 | /// An opaque type that represents a method in a class definition. |
一个方法 Method,其包含一个方法选择器 SEL – 表示该方法的名称,一个types – 表示该方法参数的类型,一个 IMP - 指向该方法的具体实现的函数指针。
IMP
1 | typedef id (*IMP)(id, SEL, ...); |
id是一个指向 objc_object 结构体的指针,该结构体只有一个成员isa,所以任何继承自 NSObject 的类对象都可以用id 来指代,因为 NSObject 的第一个成员实例就是isa。
IMP 是一个函数指针,这个被指向的函数包含一个接收消息的对象id(self 指针), 调用方法的选标 SEL (方法名),以及不定个数的方法参数,并返回一个id。也就是说 IMP 是消息最终调用的执行代码,是方法真正的实现代码 。
NSObject 类中的methodForSelector:方法就是这样一个获取指向方法实现IMP 的指针,methodForSelector:返回的指针和赋值的变量类型必须完全一致,包括方法的参数类型和返回值类型。
1 | IMP imp = [self methodForSelector:selector]; |
NSObject里面的这两个方法:
1 | - (IMP)methodForSelector:(SEL)aSelector; |
用法举例
1 | // SEL 与 IMP 用法 |
在前面介绍元类时,我们已经有接触到这几个函数了,在此我们再举个实例来看看这几个函数的使用。
1 | void ex_registerClassPair() { |
2、动态创建对象
1 | // 创建类实例 |
- class_createInstance函数:创建实例时,会在默认的内存区域为类分配内存。extraBytes参数表示分配的额外字节数。这些额外的字节可用于存储在类定义中所定义的实例变量之外的实例变量。该函数在ARC环境下无法使用。
- objc_constructInstance函数:在指定的位置(bytes)创建类实例。
- objc_destructInstance函数:销毁一个类的实例,但不会释放并移除任何与其相关的引用。
调用class_createInstance的效果与+alloc方法类似。不过在使用class_createInstance时,我们需要确切的知道我们要用它来做什么。在下面的例子中,我们用NSString来测试一下该函数的实际效果:
注意:需关闭ARC
1 | #import <Foundation/Foundation.h> |
可以看到,使用class_createInstance
函数获取的是NSString实例,而不是类簇中的默认占位符类__NSCFConstantString
。
三、实例操作函数
实例操作函数主要是针对我们创建的实例对象的一系列操作函数,我们可以使用这组函数来从实例对象中获取我们想要的一些信息,如实例对象中变量的值。这组函数可以分为三小类:
1.针对整个对象进行操作的函数,这类函数包含
1 | // 返回指定对象的一份拷贝 |
有这样一种场景,假设我们有类A和类B,且类B是类A的子类。类B通过添加一些额外的属性来扩展类A。现在我们创建了一个A类的实例对象,并希望在运行时将这个对象转换为B类的实例对象,这样可以添加数据到B类的属性中。这种情况下,我们没有办法直接转换,因为B类的实例会比A类的实例更大,没有足够的空间来放置对象。此时,我们就要以使用以上几个函数来处理这种情况,如下代码所示:
1 | NSObject *a = [[NSObject alloc] init]; |
2.针对对象实例变量进行操作的函数,这类函数包含:
1 | // 修改类实例的实例变量的值 |
如果实例变量的Ivar已经知道,那么调用object_getIvar会比object_getInstanceVariable函数快,相同情况下,object_setIvar也比object_setInstanceVariable快。
3.针对对象的类进行操作的函数,这类函数包含:
1 | // 返回给定对象的类名 |
四、获取类定义
Objective-C动态运行库会自动注册我们代码中定义的所有的类。我们也可以在运行时创建类定义并使用objc_addClass函数来注册它们。runtime提供了一系列函数来获取类定义相关的信息,这些函数主要包括:
1 | // 获取已注册的类定义的列表 |
- objc_getClassList函数:获取已注册的类定义的列表。我们不能假设从该函数中获取的类对象是继承自NSObject体系的,所以在这些类上调用方法是,都应该先检测一下这个方法是否在这个类中实现。
下面代码演示了该函数的用法(需关闭ARC):
1 | #import <Foundation/Foundation.h> |
获取类定义的方法有三个:objc_lookUpClass, objc_getClass和objc_getRequiredClass。如果类在运行时未注册,则objc_lookUpClass会返回nil,而objc_getClass会调用类处理回调,并再次确认类是否注册,如果确认未注册,再返回nil。而objc_getRequiredClass函数的操作与objc_getClass相同,只不过如果没有找到类,则会杀死进程。
objc_getMetaClass函数:如果指定的类没有注册,则该函数会调用类处理回调,并再次确认类是否注册,如果确认未注册,再返回nil。不过,每个类定义都必须有一个有效的元类定义,所以这个函数总是会返回一个元类定义,不管它是否有效。
第四部分:RunTime-成员变量与属性
一、类型编码(Type Encoding)
作为对Runtime的补充,编译器将每个方法的返回值和参数类型编码为一个字符串,并将其与方法的selector关联在一起。这种编码方案在其它情况下也是非常有用的,因此我们可以使用@encode编译器指令来获取它。当给定一个类型时,@encode返回这个类型的字符串编码。这些类型可以是诸如int、指针这样的基本类型,也可以是结构体、类等类型。事实上,任何可以作为sizeof()操作参数的类型都可以用于@encode()。
在Objective-C Runtime Programming Guide中的Type Encoding一节中,列出了Objective-C中所有的类型编码。需要注意的是这些类型很多是与我们用于存档和分发的编码类型是相同的。但有一些不能在存档时使用。
注:Objective-C不支持long double类型。@encode(long double)返回d,与double是一样的。
一个数组的类型编码位于方括号中;其中包含数组元素的个数及元素类型。如以下示例:
1 | float a[] = {1.0, 2.0, 3.0}; |
另外,还有些编码类型,@encode虽然不会直接返回它们,但它们可以作为协议中声明的方法的类型限定符。
对于属性而言,还会有一些特殊的类型编码,以表明属性是只读、拷贝、retain等等,详情可以参考Property Type String。
二、Category的关联对象、成员变量、属性
Runtime中关于成员变量和属性的相关数据结构并不多,只有三个,并且都很简单。不过还有个非常实用但可能经常被忽视的特性,即关联对象,我们将在这小节中详细讨论。
1、基础数据类型
Ivar
Ivar是表示实例变量的类型,其实际是一个指向objc_ivar结构体的指针,其定义如下:
1 | typedef struct objc_ivar *Ivar; |
objc_property_t
objc_property_t是表示Objective-C声明的属性的类型,其实际是指向objc_property结构体的指针,其定义如下:
1 | typedef struct objc_property *objc_property_t; |
objc_property_attribute_t
objc_property_attribute_t定义了属性的特性(attribute),它是一个结构体,定义如下:
1 | typedef struct { |
2、关联对象(Associated Object)
关联对象类似于成员变量,不过是在运行时添加的。我们通常会把成员变量(Ivar)放在类声明的头文件中,或者放在类实现的@implementation后面。但这有一个缺点,我们不能在分类中添加成员变量。如果我们尝试在分类中添加新的成员变量,编译器会报错。
我们可能希望通过使用(甚至是滥用)全局变量来解决这个问题。但这些都不是Ivar,因为他们不会连接到一个单独的实例。因此,这种方法很少使用。
Objective-C针对这一问题,提供了一个解决方案:即关联对象(Associated Object)。
我们可以把关联对象想象成一个Objective-C对象(如字典),这个对象通过给定的key连接到类的一个实例上。不过由于使用的是C接口,所以key是一个void指针(const void *)。我们还需要指定一个内存管理策略,以告诉Runtime如何管理这个对象的内存。这个内存管理的策略可以由以下值指定:
1 | OBJC_ASSOCIATION_ASSIGN |
当宿主对象被释放时,会根据指定的内存管理策略来处理关联对象。如果指定的策略是assign,则宿主释放时,关联对象不会被释放;而如果指定的是retain或者是copy,则宿主释放时,关联对象会被释放。我们甚至可以选择是否是自动retain/copy。当我们需要在多个线程中处理访问关联对象的多线程代码时,这就非常有用了。
我们将一个对象连接到其它对象所需要做的就是下面两行代码:
1 | static char myKey; |
在这种情况下,self对象将获取一个新的关联的对象anObject,且内存管理策略是自动retain关联对象,当self对象释放时,会自动release关联对象。另外,如果我们使用同一个key来关联另外一个对象时,也会自动释放之前关联的对象,这种情况下,先前的关联对象会被妥善地处理掉,并且新的对象会使用它的内存。
1 | // id objc_getAssociatedObject ( |
我们可以使用objc_removeAssociatedObjects函数来移除一个关联对象,或者使用objc_setAssociatedObject函数将key指定的关联对象设置为nil。
我们下面来用实例演示一下关联对象的使用方法。
问题:
我们相对项目中所有UIView都添加一个Tap手势,并传入一个block参数,使得UIView在点击时可以执行block中德内容。
思路:
毫无疑问我们会考虑使用基于UIView的Category,增加一个方法:
UIView+TapAction.h1
2
3
4
5
6
7
8
9
10
11#import <UIKit/UIKit.h>
@interface UIView (TapAction)
/**
Attaches the given block for a single tap action to the receiver.
@param block The block to execute.
*/
- (void)setTapActionWithBlock:(void (^)(void))block;
@end
然后开始信心十足的写实现代码:
UIView+TapAction.m
1 | #import "UIView+TapAction.h" |
写到这里你遇见问题了,你可能马上会想到用全局变量来储存block,但前文说过:“……使用(甚至是滥用)全局变量来解决这个问题,但这些都不是Ivar,因为他们不会连接到一个单独的实例。因此,这种方法很少使用。”
所以说放弃吧,骚年,用用我们的关联对象,轻松搞定:
1 | #import "UIView+TapAction.h" |
ViewController.m 中测试
1 | #import "ViewController.h" |
注:
原文中setTapActionWithBlock的实现如下:
1 | - (void)setTapActionWithBlock:(void (^)(void))block |
当时我不清楚为什么要设置gesture关联对象,后来想到了:既然有[self addGestureRecognizer:gesture],那可能还需要[self removeGestureRecognizer:gesture],此时就需要取得gesture这个关联对象。
此处示例代码,南峰子大神也是参考了这里代码来自 DTFoundation project on GitHub, look for UIView+DTActionHandlers
3、成员变量、属性的操作方法
(1)成员变量
成员变量操作包含以下函数:
1 | // 获取成员变量名 |
- ivar_getOffset函数,对于类型id或其它对象类型的实例变量,可以调用object_getIvar和object_setIvar来直接访问成员变量,而不使用偏移量。
(2)关联对象
关联对象操作函数包括以下:
1 | // 设置关联对象 |
(3)属性
属性操作相关函数包括以下:
1 | // 获取属性名 |
property_copyAttributeValue函数,返回的char *在使用完后需要调用free()释放。
property_copyAttributeList函数,返回值在使用完后需要调用free()释放。
4、示例
假定这样一个场景,我们从服务端两个不同的接口获取相同的字典数据,但这两个接口是由两个人写的,相同的信息使用了不同的字段表示。我们在接收到数据时,可将这些数据保存在相同的对象中。对象类如下定义:
1 | @interface MyObject: NSObject |
接口A、B返回的字典数据如下所示:
1 | @{@"name1": "张三", @"status1": @"start"} |
先看看通常的实现方法,是写两个方法分别做转换:
main.m
1 | #import <Foundation/Foundation.h> |
这种方法十分原始,当属性较多时,这么写显然十分麻烦。
也许你会想到,可以利用KVC批量设置:
1 | [myObj1 setValuesForKeysWithDictionary:dic1]; |
但是KVC批量转的时候,有个致命的缺点,就是当字典中的键,在对象属性中找不到对应的属性的时候会报错。这个时候,我们不妨反过来想一下:我们先获取到对象所有的属性名,然后加入到一个数组里面,然后再遍历,赋值。在程序运行的时候,抓取对象的属性,这时候,要利用到Runtime机制了:
灵活运用Runtime的实现方法:
main.m1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42#import <Foundation/Foundation.h>
#import "MyObject.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSDictionary *dic1 = @{@"name1": @"张三", @"status1": @"start"};
NSDictionary *dic2 = @{@"name2": @"李四", @"status2": @"end"};
/*//通常的实现方法
MyObject *myObj1 = [[MyObject alloc] init];
MyObject *myObj2 = [[MyObject alloc] init];
//方法1:转换dic1
[dic1 enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if ([key isEqualToString:@"name1"]) {
myObj1.name = obj;
}else if ([key isEqualToString:@"status1"]) {
myObj1.status = obj;
}
}];
//方法2:转换dic2
[dic2 enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if ([key isEqualToString:@"name2"]) {
myObj2.name = obj;
}else if ([key isEqualToString:@"status2"]) {
myObj2.status = obj;
}
}];
NSLog(@"myObj1`s name is %@, status is %@",myObj1.name,myObj1.status);
NSLog(@"myObj2`s name is %@, status is %@",myObj2.name,myObj2.status);*/
//运用runtime实现
MyObject *myObj1 = [[MyObject alloc] initWithDic:dic1];
MyObject *myObj2 = [[MyObject alloc] initWithDic:dic2];
NSLog(@"myObj1`s name is %@, status is%@",myObj1.name,myObj1.status);
NSLog(@"myObj2`s name is %@, status is%@",myObj2.name,myObj2.status);
}
return 0;
}
MyObject.h
1 | #import <Foundation/Foundation.h> |
MyObject.m
1 | #import "MyObject.h" |
第五部分:RunTime-消息发送及消息转发
一、基础数据类型
1、SEL
SEL又叫选择器,是表示一个方法的selector的指针,其定义如下:
1 | /// An opaque type that represents a method selector. |
objc_selector结构体的详细定义没有在
1 | SEL sel1 = @selector(method1); |
两个类之间,不管它们是父类与子类的关系,还是之间没有这种关系,只要方法名相同,那么方法的SEL就是一样的。每一个方法都对应着一个SEL。所以在Objective-C同一个类(及类的继承体系)中,不能存在2个同名的方法,即使参数类型不同也不行。相同的方法只能对应一个SEL。
例如:新建两个类,MyObjectA,MyObjectB,内容都如下
.h
1 | - (void)addressOfSelector_myTest1; |
.m
1 | - (void)addressOfSelector_myTest1 { |
然后在main.m中调用;
1 | #import <Foundation/Foundation.h> |
会发现打印结果相同:
1 | sel : 0x100000f1e |
这也就导致Objective-C在处理相同方法名且参数个数相同但类型不同的方法方面的能力很差。如在某个类中定义以下两个方法:
1 | - (void)setWidth:(int)width; |
这样的定义被认为是一种编译错误,所以我们不能像C++, C#那样。而是需要像下面这样来声明:
1 | -(void)setWidthIntValue:(int)width; |
当然,不同的类可以拥有相同的selector,这个没有问题。不同类的实例对象执行相同的selector时,会在各自的方法列表中去根据selector去寻找自己对应的IMP。
工程中的所有的SEL组成一个Set集合,Set的特点就是唯一,因此SEL是唯一的。因此,如果我们想到这个方法集合中查找某个方法时,只需要去找到这个方法对应的SEL就行了,SEL实际上就是根据方法名hash化了的一个字符串,而对于字符串的比较仅仅需要比较他们的地址就可以了,可以说速度上无语伦比!!但是,有一个问题,就是数量增多会增大hash冲突而导致的性能下降(或是没有冲突,因为也可能用的是perfect hash)。但是不管使用什么样的方法加速,如果能够将总量减少(多个方法可能对应同一个SEL),那将是最犀利的方法。那么,我们就不难理解,为什么SEL仅仅是函数名了。
本质上,SEL只是一个指向方法的指针(准确的说,只是一个根据方法名hash化了的KEY值,能唯一代表一个方法),它的存在只是为了加快方法的查询速度。这个查找过程我们将在下面讨论。
我们可以在运行时添加新的selector,也可以在运行时获取已存在的selector,我们可以通过下面三种方法来获取SEL:
- sel_registerName函数
- Objective-C编译器提供的@selector()
- NSSelectorFromString()方法
2、IMP
IMP实际上是一个函数指针,指向方法实现的首地址。其定义如下:
1 | id (*IMP)(id, SEL, ...) |
这个函数使用当前CPU架构实现的标准的C调用约定。第一个参数是指向self的指针(如果是实例方法,则是类实例的内存地址;如果是类方法,则是指向元类的指针),第二个参数是方法选择器(selector),接下来是方法的实际参数列表。
前面介绍过的SEL就是为了查找方法的最终实现IMP的。由于每个方法对应唯一的SEL,因此我们可以通过SEL方便快速准确地获得它所对应的IMP,查找过程将在下面讨论。取得IMP后,我们就获得了执行这个方法代码的入口点,此时,我们就可以像调用普通的C语言函数一样来使用这个函数指针了。
IMP可以从NSObject类提供了
methodForSelector:
方法得到。通过取得IMP,我们可以跳过Runtime的消息传递机制,直接执行IMP指向的函数实现,这样省去了Runtime消息传递过程中所做的一系列查找操作,会比直接向对象发送消息高效一些。例如,可把前面的列子改为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25 > #import <Foundation/Foundation.h>
> #import <objc/runtime.h>
> #import "MyObjectA.h"
> #import "MyObjectB.h"
>
> int main(int argc, const char * argv[]) {
> @autoreleasepool {
> /*
> MyObjectA *objA = [[MyObjectA alloc] init];
> MyObjectA *objB = [[MyObjectA alloc] init];
> [objA addressOfSelector_myTest1];
> [objB addressOfSelector_myTest1];
> */
>
> MyObjectA *objA = [[MyObjectA alloc] init];
> MyObjectA *objB = [[MyObjectA alloc] init];
> SEL sel = NSSelectorFromString(@"addressOfSelector_myTest1");
> IMP impA = [objA methodForSelector:sel];
> IMP impB = [objB methodForSelector:sel];
> impA();
> impB();
> }
> return 0;
> }
>
3、Method
介绍完SEL和IMP,我们就可以来讲讲Method了。Method用于表示类定义中的方法,则定义如下:
1 | typedef struct objc_method *Method; |
我们可以看到该结构体中包含一个SEL和IMP,实际上相当于在SEL和IMP之间作了一个映射。有了SEL,我们便可以找到对应的IMP,从而调用方法的实现代码。具体操作流程我们将在下面讨论。
objc_method_description:
objc_method_description定义了一个Objective-C方法,其定义如下:
1 | struct objc_method_description { SEL name; char *types; }; |
二、方法相关操作函数
Runtime提供了一系列的方法来处理与方法相关的操作。包括方法本身及SEL。本节我们介绍一下这些函数。
1、方法
方法操作相关函数包括下以:
1 | // 调用指定方法的实现 |
method_invoke函数,返回的是实际实现的返回值。参数receiver不能为空。这个方法的效率会比method_getImplementation和method_getName更快。
method_getName函数,返回的是一个SEL。如果想获取方法名的C字符串,可以使用sel_getName(method_getName(method))。
method_getReturnType函数,类型字符串会被拷贝到dst中。
method_setImplementation函数,注意该函数返回值是方法之前的实现。
2、方法选择器
选择器相关的操作函数包括:
1 | // 返回给定选择器指定的方法的名称 |
- sel_registerName函数:在我们将一个方法添加到类定义时,我们必须在Objective-C Runtime系统中注册一个方法名以获取方法的选择器。
三、方法调用流程
1、流程
在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_msgSend结构体中的指向父类的指针找到其父类,并在父类的分发表里面查找方法的selector。依此,会一直沿着类的继承体系到达NSObject类。一旦定位到selector,函数会就获取到了实现的入口点,并传入相应的参数来执行方法的具体实现。如果最后没有定位到selector,则会走消息转发流程,这个我们在后面讨论。
为了加速消息的处理,运行时系统缓存使用过的selector及对应的方法的地址。这点我们在前面讨论过,不再重复。
2、隐藏参数
objc_msgSend有两个隐藏参数:
消息接收对象
方法的selector
这两个参数为方法的实现提供了调用者的信息。之所以说是隐藏的,是因为它们在定义方法的源代码中没有声明。它们是在编译期被插入实现代码的。
虽然这些参数没有显示声明,但在代码中仍然可以引用它们。我们可以使用self来引用接收者对象,使用_cmd来引用选择器。如下代码所示:
1 | - strange |
当然,这两个参数我们用得比较多的是self,_cmd在实际中用得比较少。
3、获取方法地址
Runtime中方法的动态绑定让我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。不过灵活性的提升也带来了性能上的一些损耗。毕竟我们需要去查找方法的实现,而不像函数调用来得那么直接。当然,方法的缓存一定程度上解决了这一问题。
我们上面提到过,如果想要避开这种动态绑定方式,我们可以获取方法实现的地址,然后像调用函数一样来直接调用它。特别是当我们需要在一个循环内频繁地调用一个特定的方法时,通过这种方式可以提高程序的性能。
NSObject类提供了methodForSelector:
方法,让我们可以获取到方法的指针,然后通过这个指针来调用实现代码。我们需要将methodForSelector:
返回的指针转换为合适的函数类型,函数参数和返回值都需要匹配上。
我们通过以下代码来看看methodForSelector:
的使用:
示例:完整示例请参考上文讲解IMP的部分,在此重新列一下关键代码:
1 | int main(int argc, const char * argv[]) { |
注:此处原文如下
原文开始》》》》
1
2
3
4
5
6
7 > void (*setter)(id, SEL, BOOL);
> int i;
>
> setter = (void (*)(id, SEL, BOOL))[target methodForSelector:@selector(setFilled:)];
> for (i = 0 ; i < 1000 ; i++)
> setter(targetList[i], @selector(setFilled:), YES);
>这里需要注意的就是函数指针的前两个参数必须是id和SEL。
《《《《原文结束
但经过实测,并不这样,求解释
四、消息转发
当一个对象能接收一个消息时,就会走正常的方法调用流程。但如果一个对象无法接收指定消息时,又会发生什么事呢?默认情况下,如果是以[object message]的方式调用方法,如果object无法响应message消息时,编译器会报错。但如果是以perform…的形式来调用,则需要等到运行时才能确定object是否能接收message消息。如果不能,则程序崩溃。
通常,当我们不能确定一个对象是否能接收某个消息时,会先调用respondsToSelector:来判断一下。如下代码所示:
1 | if ([self respondsToSelector:@selector(method)]) { |
不过,我们这边想讨论下不使用respondsToSelector:判断的情况。这才是我们这一节的重点。
当一个对象无法接收某一消息时,就会启动所谓”消息转发(message forwarding)“机制,通过这一机制,我们可以告诉对象如何处理未知的消息。默认情况下,对象接收到未知的消息,会导致程序崩溃,通过控制台,我们可以看到以下异常信息:
1 | -[SUTRuntimeMethod method]: unrecognized selector sent to instance 0x100111940 |
这段异常信息实际上是由NSObject的”doesNotRecognizeSelector”方法抛出的。不过,我们可以采取一些措施,让我们的程序执行特定的逻辑,而避免程序的崩溃。
消息转发机制基本上分为三个步骤:
- 动态方法解析
- 备用接收者
- 完整转发
下面我们详细讨论一下这三个步骤。
1、动态方法解析
对象在接收到未知的消息时,首先会调用所属类的类方法:
1 | + (BOOL)resolveClassMethod:(SEL)sel;//sel为 实例方法 |
在这个方法中,我们有机会为该未知消息新增一个“处理方法”。不过使用该方法的前提是我们已经实现了该”处理方法”,只需要在运行时通过class_addMethod函数动态添加到类里面就可以了。如下代码所示:
MyObjectA.h
1 | #import <Foundation/Foundation.h> |
MyObjectA.m
1 | #import "MyObjectA.h" |
main.m
1 | #import <Foundation/Foundation.h> |
不过这种方案更多的是为了实现@dynamic属性。
2、备用接收者
如果在上一步无法处理消息,则Runtime会继续调以下方法:
1 | - (id)forwardingTargetForSelector:(SEL)aSelector |
如果一个对象实现了这个方法,并返回一个非nil的结果,则这个对象会作为消息的新接收者,且消息会被分发到这个对象。当然这个对象不能是self自身,否则就是出现无限循环。当然,如果我们没有指定相应的对象来处理aSelector,则应该调用父类的实现来返回结果。
使用这个方法通常是在对象内部,可能还有一系列其它对象能处理该消息,我们便可借这些对象来处理消息并返回,这样在对象外部看来,还是由该对象亲自处理了这一消息。如下代码所示:
MyObjectB.h1
2
3
4
5#import <Foundation/Foundation.h>
@interface MyObjectB : NSObject
@end
MyObjectB.m1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46#import "MyObjectB.h"
//------------------------------------------
@interface MyObjectBHelper : NSObject
//这里很有趣,注释掉.h文件中的方法声明,照样执行
- (void)instanceMethord2;
@end
@implementation MyObjectBHelper
- (void)instanceMethord2 {
NSLog(@"MyObjectBHelper---self:%@, _cmd地址:%p", self, _cmd);
}
@end
//------------------------------------------
@implementation MyObjectB{
MyObjectBHelper *_helper;
}
- (instancetype)init {
self = [super init];
if (self != nil) {
_helper = [[MyObjectBHelper alloc] init];
}
return self;
}
- (id)forwardingTargetForSelector:(SEL)aSelector {
NSLog(@"forwardingTargetForSelector");
NSString *selectorString = NSStringFromSelector(aSelector);
// 将消息转发给_helper来处理
if ([selectorString isEqualToString:@"instanceMethord2"]) {
return _helper;
}
return [super forwardingTargetForSelector:aSelector];
}
main.m
1 | #import <Foundation/Foundation.h> |
这一步合适于我们只想将消息转发到另一个能处理该消息的对象上。但这一步无法对消息进行处理,如操作消息的参数和返回值。
3、完整消息转发
如果在上一步还不能处理未知消息,则唯一能做的就是启用完整的消息转发机制了。此时会调用以下方法:
1 | - (void)forwardInvocation:(NSInvocation *)anInvocation |
运行时系统会在这一步给消息接收者最后一次机会将消息转发给其它对象。对象会创建一个表示消息的NSInvocation对象,把与尚未处理的消息有关的全部细节都封装在anInvocation中,包括selector,目标(target)和参数。我们可以在forwardInvocation方法中选择将消息转发给其它对象。
forwardInvocation:方法的实现有两个任务:
- 定位可以响应封装在anInvocation中的消息的对象。这个对象不需要能处理所有未知消息。
- 使用anInvocation作为参数,将消息发送到选中的对象。anInvocation将会保留调用结果,运行时系统会提取这一结果并将其发送到消息的原始发送者。
不过,在这个方法中我们可以实现一些更复杂的功能,我们可以对消息的内容进行修改,比如追回一个参数等,然后再去触发消息。另外,若发现某个消息不应由本类处理,则应调用父类的同名方法,以便继承体系中的每个类都有机会处理此调用请求。
还有一个很重要的问题,我们必须重写以下方法:
1 | - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector |
消息转发机制使用从这个方法中获取的信息来创建NSInvocation对象。因此我们必须重写这个方法,为给定的selector提供一个合适的方法签名。
完整的示例如下所示:
MyObjectC.h
1 | #import <Foundation/Foundation.h> |
MyObjectC.m
1 | #import "MyObjectC.h" |
main.m
1 | #import <Foundation/Foundation.h> |
3-1、补充知识点:NSMethodSignature
NSMethodSignature顾名思义应该就是“方法签名”,类似于C++中的编译器时的函数签名。
官方定义该类为对方法的参数、返回类似进行封装,协同NSInvocation实现消息转发。
运行上面示例,打印signature中的内容如下:
1 | ArgumentTypeAtIndex:0 is @ |
@、:、v 这些奇怪的符号为Objective-C类型编码,含义如下:
编码 | 含义 |
---|---|
c | char |
i | int |
s | short |
l | long ,在64位程序中,l为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 *) |
@ | 对象(无论是静态指定的还是通过id引用的) |
# | 类(Class) |
: | 方法选标(SEL) |
[array type] | 数组 |
{name=type…} | 结构体 |
(name=type…) | 联合体 |
bnum | num个bit的位域 |
^type | type类型的指针 |
? | 未知类型(其它时候,一般用来指函数指针) |
3-2、补充知识点:NSInvocation
在 iOS中可以直接调用某个对象的消息方式有2种:
第一种方式是使用NSObject类提供的performSelector系列方法,
还有一种方式就是使用NSInvocation进行动态运行时的消息分发。
MyObjectD.h
1 | #import <Foundation/Foundation.h> |
MyObjectD.m
1 | #import "MyObjectD.h" |
main.m
1 | #import <Foundation/Foundation.h> |
NSObject的forwardInvocation:方法实现只是简单调用了doesNotRecognizeSelector:方法,它不会转发任何消息。这样,如果不在以上所述的三个步骤中处理未知消息,则会引发一个异常。
从某种意义上来讲,forwardInvocation:就像一个未知消息的分发中心,将这些未知的消息转发给其它对象。或者也可以像一个运输站一样将所有未知消息都发送给同一个接收对象。这取决于具体的实现。
4、消息转发与多重继承
回过头来看第二和第三步,通过这两个方法我们可以允许一个对象与其它对象建立关系,以处理某些未知消息,而表面上看仍然是该对象在处理消息。通过这种关系,我们可以模拟“多重继承”的某些特性,让对象可以“继承”其它对象的特性来处理一些事情。不过,这两者间有一个重要的区别:
- 多重继承将不同的功能集成到一个对象中,它会让对象变得过大,涉及的东西过多;
- 而消息转发将功能分解到独立的小的对象中,并通过某种方式将这些对象连接起来,并做相应的消息转发。
不过消息转发虽然类似于继承,但NSObject的一些方法还是能区分两者。如respondsToSelector:和isKindOfClass:只能用于继承体系,而不能用于转发链。如果我们想让这种消息转发看起来像是继承,则可以重写这些方法,如以下代码所示:
1 | - (BOOL)respondsToSelector:(SEL)aSelector |
5、小结
在此,我们已经了解了Runtime中消息发送和转发的基本机制。这也是Runtime的强大之处,通过它,我们可以为程序增加很多动态的行为,虽然我们在实际开发中很少直接使用这些机制(如直接调用objc_msgSend),但了解它们有助于我们更多地去了解底层的实现。其实在实际的编码过程中,我们也可以灵活地使用这些机制,去实现一些特殊的功能,如hook操作等。
第六部分:RunTime-Method Swizzling
理解Method Swizzling是学习runtime机制的一个很好的机会。在此不多做整理,仅翻译由Mattt Thompson发表于nshipster的Method Swizzling一文。
Method Swizzling是改变一个selector的实际实现的技术。通过这一技术,我们可以在运行时通过修改类的分发表中selector对应的函数,来修改方法的实现。
例如,我们想跟踪在程序中每一个view controller展示给用户的次数:当然,我们可以在每个view controller的viewDidAppear中添加跟踪代码;但是这太过麻烦,需要在每个view controller中写重复的代码。创建一个子类可能是一种实现方式,但需要同时创建UIViewController, UITableViewController, UINavigationController及其它UIKit中view controller的子类,这同样会产生许多重复的代码。
这种情况下,我们就可以使用Method Swizzling,如在代码所示
1 | #import "UIViewController+Tracking.h" |
在这里,我们通过method swizzling修改了UIViewController的@selector(viewWillAppear:)对应的函数指针,使其实现指向了我们自定义的xxx_viewWillAppear的实现。这样,当UIViewController及其子类的对象调用viewWillAppear时,都会打印一条日志信息。
一、method swizzling注意事项
上面的例子很好地展示了使用method swizzling来一个类中注入一些我们新的操作。当然,还有许多场景可以使用method swizzling,在此不多举例。在此我们说说使用method swizzling需要注意的一些问题:
1、Swizzling应该总是在+load中执行
在Objective-C中,运行时会自动调用每个类的两个方法:
+load
会在类初始加载时调用+initialize
会在第一次调用类的类方法或实例方法之前被调用
这两个方法是可选的,且只有在实现了它们时才会被调用。由于method swizzling会影响到类的全局状态,因此要尽量避免在并发处理中出现竞争的情况。+load能保证在类的初始化过程中被加载,并保证这种改变应用级别的行为的一致性。相比之下,+initialize在其执行时不提供这种保证—事实上,如果在应用中没为给这个类发送消息,则它可能永远不会被调用。
2、Swizzling应该总是在dispatch_once中执行
与上面相同,因为swizzling会改变全局状态,所以我们需要在运行时采取一些预防措施。原子性就是这样一种措施,它确保代码只被执行一次,不管有多少个线程。GCD的dispatch_once可以确保这种行为,我们应该将其作为method swizzling的最佳实践。
3、选择器、方法与实现
在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一个方法,我们可以在分发表中将一个方法的现有的选择器映射到不同的实现,而将该选择器对应的原始实现关联到一个新的选择器中。
4、调用_cmd
我们回过头来看看前面新的方法的实现代码:
1 | - (void)xxx_viewWillAppear:(BOOL)animated { |
咋看上去是会导致无限循环的。但令人惊奇的是,并没有出现这种情况。在swizzling的过程中,方法中的[self xxx_viewWillAppear:animated]已经被重新指定到UIViewController类的-viewWillAppear:中。在这种情况下,不会产生无限循环。不过如果我们调用的是[self viewWillAppear:animated],则会产生无限循环,因为这个方法的实现在运行时已经被重新指定为xxx_viewWillAppear:了。
5、注意事项小结
Swizzling通常被称作是一种黑魔法,容易产生不可预知的行为和无法预见的后果。虽然它不是最安全的,但如果遵从以下几点预防措施的话,还是比较安全的:
- 总是调用方法的原始实现(除非有更好的理由不这么做):API提供了一个输入与输出约定,但其内部实现是一个黑盒。Swizzle一个方法而不调用原始实现可能会打破私有状态底层操作,从而影响到程序的其它部分。
- 避免冲突:给自定义的分类方法加前缀,从而使其与所依赖的代码库不会存在命名冲突。
- 明白是怎么回事:简单地拷贝粘贴swizzle代码而不理解它是如何工作的,不仅危险,而且会浪费学习Objective-C运行时的机会。阅读Objective-C Runtime Reference和查看
头文件以了解事件是如何发生的。 - 小心操作:无论我们对Foundation, UIKit或其它内建框架执行Swizzle操作抱有多大信心,需要知道在下一版本中许多事可能会不一样。
二、Method Swizzling 原理
在Objective-C中调用一个方法,其实是向一个对象发送消息,查找消息的唯一依据是selector的名字。利用Objective-C的动态特性,可以实现在运行时偷换selector对应的方法实现,达到给方法挂钩的目的。
每个类都有一个方法列表,存放着selector的名字和方法实现的映射关系。IMP有点类似函数指针,指向具体的Method实现。
1 | graph LR |
我们可以利用 method_exchangeImplementations 来交换2个方法中的IMP,
我们可以利用 class_replaceMethod 来修改类,
我们可以利用 method_setImplementation 来直接设置某个方法的IMP,
……
归根结底,都是偷换了selector的IMP,如下图所示:
1 | graph LR |
举个例子好了,我想钩一下NSArray的lastObject 方法,只需两个步骤。
第一步:给NSArray加一个我自己的lastObject
1 | #import "NSArray+Swizzle.h" |
乍一看,这不递归了么?别忘记这是我们准备调换IMP的selector,[self myLastObject] 将会执行真的 [self lastObject] 。
第二步:调换IMP
1 | #import <Foundation/Foundation.h> |
三、Method Swizzling 的封装
之前在github上找到的RNSwizzle,推荐给大家,可以搜一下。
1 | // |
四、Method Swizzling 危险不危险
针对这个问题,我在stackoverflow上看到了满意的答案,这里翻译一下,总结记录在本文中,以示分享:
使用 Method Swizzling 编程就好比切菜时使用锋利的刀,一些人因为担心切到自己所以害怕锋利的刀具,可是事实上,使用钝刀往往更容易出事,而利刀更为安全。
Method swizzling 可以帮助我们写出更好的,更高效的,易维护的代码。但是如果滥用它,也将会导致难以排查的bug。
背景
好比设计模式,如果我们摸清了一个模式的门道,使用该模式与否我们自己心里有数。单例模式就是一个很好的例子,它饱受争议但是许多人依旧使用它。Method Swizzling也是一样,一旦你真正理解它的优势和弊端,使用它与否你应该就有你自己的观点。
讨论
这里是一些 Method Swizzling的陷阱:
- Method swizzling is not atomic
- Changes behavior of un-owned code
- Possible naming conflicts
- Swizzling changes the method’s arguments
- The order of swizzles matters
- Difficult to understand (looks recursive)
- Difficult to debug
我将逐一分析这些点,增进对Method Swizzling的理解的同时,并搞懂如何应对。
1、Method swizzling is not atomic
我所见过的使用method swizzling实现的方法在并发使用时基本都是安全的。95%的情况里这都不会是个问题。通常你替换一个方法的实现,是希望它在整个程序的生命周期里有效的。也就是说,你会把 method swizzling 修改方法实现的操作放在一个加号方法 +(void)load里,并在应用程序的一开始就调用执行。你将不会碰到并发问题。假如你在 +(void)initialize初始化方法中进行swizzle,那么……rumtime可能死于一个诡异的状态。
2、Changes behavior of un-owned code
这是swizzling的一个问题。我们的目标是改变某些代码。swizzling方法是一件灰常灰常重要的事,当你不只是对一个NSButton类的实例进行了修改,而是程序中所有的NSButton实例。因此在swizzling时应该多加小心,但也不用总是去刻意避免。
想象一下,如果你重写了一个类的方法,而且没有调用父类的这个方法,这可能会引起问题。大多数情况下,父类方法期望会被调用(至少文档是这样说的)。如果你在swizzling实现中也这样做了,这会避免大部分问题。还是调用原始实现吧,如若不然,你会费很大力气去考虑代码的安全问题。
3、Possible naming conflicts
命名冲突贯穿整个Cocoa的问题. 我们常常在类名和类别方法名前加上前缀。不幸的是,命名冲突仍是个折磨。但是swizzling其实也不必过多考虑这个问题。我们只需要在原始方法命名前做小小的改动来命名就好,比如通常我们这样命名:
1 | @interface NSView : NSObject |
这段代码运行正确,但是如果my_setFrame: 在别处被定义了会发生什么呢?
这个问题不仅仅存在于swizzling,这里有一个替代的变通方法:
1 | @implementation NSView (MyViewAdditions) |
看起来不那么Objectice-C了(用了函数指针),这样避免了selector的命名冲突。
最后给出一个较完美的swizzle方法的定义:
1 | typedef IMP *IMPPointer; |
4、Swizzling changes the method’s arguments
我认为这是最大的问题。想正常调用method swizzling 将会是个问题。
1 | [self my_setFrame:frame]; |
直接调用my_setFrame:
, runtime做的是
1 | objc_msgSend(self, @selector(my_setFrame:), frame); |
runtime去寻找my_setFrame:的方法实现, _cmd参数为 my_setFrame: ,但是事实上runtime找到的方法实现是原始的 setFrame: 的。
一个简单的解决办法:使用上面介绍的swizzling定义。
5、The order of swizzles matters
多个swizzle方法的执行顺序也需要注意。假设 setFrame: 只定义在NSView中,想像一下按照下面的顺序执行:
1 | [NSButton swizzle:@selector(setFrame:) with:@selector(my_buttonSetFrame:)]; |
What happens when the method on NSButton is swizzled? Well most swizzling will ensure that it’s not replacing the implementation of setFrame: for all views, so it will pull up the instance method. This will use the existing implementation to re-define setFrame: in the NSButton class so that exchanging implementations doesn’t affect all views. The existing implementation is the one defined on NSView. The same thing will happen when swizzling on NSControl (again using the NSView implementation).
When you call setFrame: on a button, it will therefore call your swizzled method, and then jump straight to the setFrame: method originally defined on NSView. The NSControl and NSView swizzled implementations will not be called.
But what if the order were:
1 | [NSView swizzle:@selector(setFrame:) with:@selector(my_viewSetFrame:)]; |
Since the view swizzling takes place first, the control swizzling will be able to pull up the right method. Likewise, since the control swizzling was before the button swizzling, the button will pull up the control’s swizzled implementation of setFrame:. This is a bit confusing, but this is the correct order. How can we ensure this order of things?
Again, just use load to swizzle things. If you swizzle in load and you only make changes to the class being loaded, you’ll be safe. The load method guarantees that the super class load method will be called before any subclasses. We’ll get the exact right order!
这段贴了原文,硬翻译太拗口……总结一下就是:多个有继承关系的类的对象swizzle时,从子类对象开始 。 如果先swizzle父类对象,那么后面子类对象swizzle时就无法拿到真正的原始方法实现了。
(感谢评论中 qq373127202 的提醒,在此更正一下,十分感谢)
多个有继承关系的类的对象swizzle时,先从父对象开始。 这样才能保证子类方法拿到父类中的被swizzle的实现。在+(void)load中swizzle不会出错,就是因为load类方法会默认从父类开始调用。
6、Difficult to understand (looks recursive)
(新方法的实现)看起来像递归,但是看看上面已经给出的 swizzling 封装方法, 使用起来就很易读懂.
这个问题是已完全解决的了!
7、Difficult to debug
debug时打出的backtrace,其中掺杂着被swizzle的方法名,一团糟啊!上面介绍的swizzle方案,使backtrace中打印出的方法名还是很清晰的。但仍然很难去debug,因为很难记住swizzling影响过什么。给你的代码写好文档(即使只有你一个人会看到)。养成一个好习惯,不会比调试多线程问题还难的。
第七部分:RunTime-协议与分类
一、基础数据类型
1、Category
Category是表示一个指向分类的结构体的指针,其定义如下:
1 | typedef struct objc_category *Category; |
这个结构体主要包含了分类定义的实例方法与类方法,其中instance_methods列表是objc_class中方法列表的一个子集,而class_methods列表是元类方法列表的一个子集。
2、Protocol
Protocol的定义如下:
1 | typedef struct objc_object Protocol; |
我们可以看到,Protocol其中实就是一个对象结构体。
二、操作函数
Runtime并没有在
1 | #import <Foundation/Foundation.h> |
其输出是:
1 | 测试objc_class中的方法列表是否包含分类中的方法 |
而对于Protocol,runtime提供了一系列函数来对其进行操作,这些函数包括:
1 | // 返回指定的协议 |
objc_getProtocol函数,需要注意的是如果仅仅是声明了一个协议,而未在任何类中实现这个协议,则该函数返回的是nil。
objc_copyProtocolList函数,获取到的数组需要使用free来释放
objc_allocateProtocol函数,如果同名的协议已经存在,则返回nil
objc_registerProtocol函数,创建一个新的协议后,必须调用该函数以在运行时中注册新的协议。协议注册后便可以使用,但不能再做修改,即注册完后不能再向协议添加方法或协议
需要强调的是,协议一旦注册后就不可再修改,即无法再通过调用protocol_addMethodDescription、protocol_addProtocol和protocol_addProperty往协议中添加方法等。
小结
Runtime并没有提供过多的函数来处理分类。对于协议,我们可以动态地创建协议,并向其添加方法、属性及继承的协议,并在运行时动态地获取这些信息。
第八部分:RunTime-拾遗
一、super
在Objective-C中,如果我们需要在类的方法中调用父类的方法时,通常都会用到super,如下所示:
1 | @interface MyViewController: UIViewController |
如何使用super我们都知道。现在的问题是,它是如何工作的呢?
首先我们需要知道的是super与self不同。self是类的一个隐藏参数,每个方法的实现的第一个参数即为self。而super并不是隐藏参数,它实际上只是一个”编译器标示符”,它负责告诉编译器,当调用viewDidLoad方法时,去调用父类的方法,而不是本类中的方法。而它实际上与self指向的是相同的消息接收者。为了理解这一点,我们先来看看super的定义:
1 | struct objc_super { id receiver; Class superClass; }; |
这个结构体有两个成员:
- receiver:即消息的实际接收者
- superClass:指针当前类的父类
当我们使用super来接收消息时,编译器会生成一个objc_super结构体。就上面的例子而言,这个结构体的receiver就是MyViewController对象,与self相同;superClass指向MyViewController的父类UIViewController。
接下来,发送消息时,不是调用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 | @interface MyClass : NSObject |
调用MyClass的test方法后,其输出是:
1 | self class: MyClass |
从上例中可以看到,两者的输出都是MyClass。大家可以自行用上面介绍的内容来梳理一下。
二、库相关操作
库相关的操作主要是用于获取由系统提供的库相关的信息,主要包含以下函数:
1 | // 获取所有加载的Objective-C框架和动态库的名称 |
通过这几个函数,我们可以了解到某个类所有的库,以及某个库中包含哪些类。如下代码所示:
1 | #import "ViewController.h" |
其输出结果如下:
1 | 2015-08-13 20:38:05.865 Runtile12-ku[16098:1482104] 获取指定类所在动态库 |
三、Block
我们都知道block给我们带到极大的方便,苹果也不断提供一些使用block的新的API。同时,苹果在runtime中也提供了一些函数来支持针对block的操作,这些函数包括:
1 | // 创建一个指针函数的指针,该函数调用时会调用特定的block |
- imp_implementationWithBlock函数:参数block的签名必须是method_return_type ^(id self, method_args …)形式的。该方法能让我们使用block作为IMP。如下代码所示:
1 | @interface MyRuntimeBlock : NSObject |
四、弱引用操作
1 | // 加载弱引用指针引用的对象并返回 |
objc_loadWeak函数:该函数加载一个弱指针引用的对象,并在对其做retain和autoreleasing操作后返回它。这样,对象就可以在调用者使用它时保持足够长的生命周期。该函数典型的用法是在任何有使用__weak变量的表达式中使用。
objc_storeWeak函数:该函数的典型用法是用于__weak变量做为赋值对象时。
这两个函数的具体实施在此不举例,有兴趣的小伙伴可以参考《Objective-C高级编程:iOS与OS X多线程和内存管理》中对__weak实现的介绍。
五、宏定义
在runtime中,还定义了一些宏定义供我们使用,有些值我们会经常用到,如表示BOOL值的YES/NO;而有些值不常用,如OBJC_ROOT_CLASS。在此我们做一个简单的介绍。
1、布尔值
1 | #define YES (BOOL)1 |
这两个宏定义定义了表示布尔值的常量,需要注意的是YES的值是1,而不是非0值。
2、空值
1 | #define nil __DARWIN_NULL |
其中nil用于空的实例对象,而Nil用于空类对象。
3、分发函数原型
1 | #define OBJC_OLD_DISPATCH_PROTOTYPES 1 |
该宏指明分发函数是否必须转换为合适的函数指针类型。当值为0时,必须进行转换
4、Objective-C根类
1 | #define OBJC_ROOT_CLASS |
如果我们定义了一个Objective-C根类,则编译器会报错,指明我们定义的类没有指定一个基类。这种情况下,我们就可以使用这个宏定义来避过这个编译错误。该宏在iOS 7.0后可用。
其实在NSObject的声明中,我们就可以看到这个宏的身影,如下所示:
1 | __OSX_AVAILABLE_STARTING(__MAC_10_0, __IPHONE_2_0) |
我们可以参考这种方式来定义我们自己的根类。
5、局部变量存储时长
1 | #define NS_VALID_UNTIL_END_OF_SCOPE |
该宏表明存储在某些局部变量中的值在优化时不应该被编译器强制释放。
我们将局部变量标记为id类型或者是指向ObjC对象类型的指针,以便存储在这些局部变量中的值在优化时不会被编译器强制释放。相反,这些值会在变量再次被赋值之前或者局部变量的作用域结束之前都会被保存。
6、关联对象行为
1 | enum { |
这几个值在前面已介绍过,在此不再重复。