Objective-C语言是一门动态语言,它将很多静态语言在编译和链接时期做的事放到了运行时来处理。这种动态语言的优势在于:我们写代码时更具灵活性,如我们可以把消息转发给我们想要的对象,或者随意交换一个方法的实现等。
这种特性意味着Objective-C不仅需要一个编译器,还需要一个运行时系统来执行编译的代码。对于Objective-C来说,这个运行时系统就像一个操作系统一样:它让所有的工作可以正常的运行。这个运行时系统即Objc Runtime。Objc Runtime其实是一个Runtime库,它基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。
Objc runtime是开源的,我已fork了一份用于添加注释和学习,点击objc runtime 723。在本文中,我们将对照源码,先来介绍一下类与对象,这是面向对象的基础,我们看看在Runtime中,类是如何实现的。
类与对象数据结构
Class
Objective-C类是由Class类型表示的,它实际上是一个指定objc_class结构体的指针。它的定义如下:
1 | typedef struct objc_class *Class; |
查看objc/runtime.h中的objc_class结构体的定义如下:
1 | struct objc_class { |
在这个定义中,面几个字段是我们感兴趣的:
isa: 需要注意的是在Objective-C中,所有的类自身也是一个对象,这个对象的Class里面也有一个isa指针,它指向metaClass(元类)。super_class: 指向该类的父类,如果该类已经是最顶层的根类(如NSObject或NSProxy),则super_class为NULL。name: 类名version: 我们可以使用这个字段来提供类的版本信息。这对于对象的序列化非常有用,它可是让我们识别出不同类定义版本中实例变量布局的改变。ivars: 成员变量列表methodLists: 方法列表protocols: 遵循的协议列表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中取出相应的方法,直接调用。
Meta-Class(元类)
metaclass是一个类对象的类,所有的metaclass的isa指针指向的是根元类。 而根元类的isa指针指向自己,形成一个闭环。在NSObject继承体系下的metaclass都使用NSObject的metaclass作为根元类。
通过上面的描述,再加上对objc_class结构体中super_class指针的分析,我们就可以描绘出类及相应metaclass类的一个继承体系了,如下图所示:
当我们向一个对象发送消息时,runtime会在这个对象所属的类的方法列表中查找方法;而向一个类方法发送消息时,会在这个类的metaclass的方法列表中查找。
对于NSObject继承体系来说,其实例方法对体系中的所有实例对象、类和metaclass都是有效的;而类方法对于体系内的所有类和metaclass都是有效的。
注意:
类方法存储在metaclass中
获取类的某一实例的所有的实例方法列表:
1 | - (void)methodListAction { |
获取类的所有类方法列表:
1 | - (void)classMethodListAction { |
objc_object与id
objc_object是表示一个类的实例的结构体,它的定义如下(objc/objc.h):
1 | struct objc_object { |
可以看到,这个结构体只有一个字体,即指向其类的isa指针。这样,当我们向一个Objective-C对象发送消息时,运行时库会根据实例对象的isa指针找到这个实例对象所属的类。Runtime库会在类的方法列表及父类的方法列表中去寻找与消息对应的selector指向的方法。找到后即运行这个方法。
当创建一个特定类的实例对象时,分配的内存包含一个objc_object数据结构,然后是类的实例变量的数据。NSObject类的alloc和allocWithZone:方法使用函数class_createInstance来创建objc_object数据结构。
另外还有我们常见的id,它是一个objc_object结构类型的指针。它的存在可以让我们实现类似于C++中泛型的一些操作。该类型的对象可以转换为任何一种对象,有点类似于C语言中void *指针类型的作用。
类与对象的操作函数
runtime提供了大量的函数来操作类与对象。类的操作方法大部分是以class_为前缀的,而对象的操作方法大部分是以objc_或object_为前缀。下面我们将根据这些方法的用途来分类讨论这些方法的使用。
类相关操作函数
我们可以回过头去看看objc_class的定义,runtime提供的操作类的方法主要就是针对这个结构体中的各个字段的。下面我们分别介绍这一些的函数。并在最后以实例来演示这些函数的具体用法。
类名(name)
类名操作的函数主要有:
1 | const char * class_getName(Class cls); |
父类(super_class)和元类(meta-class)
父类和元类操作的函数主要有:
1 | // 获取类的父类 |
class_getSuperclass函数,当cls为Nil或者cls为根类时,返回Nil。不过通常我们可以使用NSObject类的superclass方法来达到同样的目的。
class_isMetaClass函数,如果是cls是元类,则返回YES;如果否或者传入的cls为Nil,则返回NO。
实例变量大小(instance_size)
实例变量大小操作的函数有:
1 | // 获取实例大小 |
成员变量(ivars)及属性
在objc_class中,所有的成员变量、属性的信息是放在链表ivars中的。ivars是一个数组,数组中每个元素是指向Ivar(变量信息)的指针。runtime提供了丰富的函数来操作这一字段。大体上可以分为以下几类:
1.成员变量操作函数,主要包含以下函数:
1 | // 获取类中指定名称实例成员变量的信息 |
class_getInstanceVariable函数,它返回一个指向包含name指定的成员变量信息的objc_ivar结构体的指针(Ivar)。
class_getClassVariable函数,目前没有找到关于Objective-C中类变量的信息,一般认为Objective-C不支持类变量。注意,返回的列表不包含父类的成员变量和属性。
Objective-C不支持往已存在的类中添加实例变量,因此不管是系统库提供的提供的类,还是我们自定义的类,都无法动态添加成员变量。但如果我们通过运行时来创建一个类的话,又应该如何给它添加成员变量呢?这时我们就可以使用class_addIvar函数了。不过需要注意的是,这个方法只能在objc_allocateClassPair函数与objc_registerClassPair之间调用。另外,这个类也不能是元类。
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时,会生成合理的布局。在此不详细介绍这些函数。
方法(methodLists)
方法操作主要有以下函数:
1 | // 添加方法 |
class_addMethod的实现会覆盖父类的方法实现,但不会取代本类中已存在的实现,如果本类中包含一个同名的实现,则函数会返回NO。如果要修改已存在实现,可以使用method_setImplementation。一个Objective-C方法是一个简单的C函数,它至少包含两个参数–self和_cmd。所以,我们的实现函数(IMP参数指向的函数)至少需要两个参数,如下所示:
1 | void myMethodIMP(id self, SEL _cmd) |
与成员变量不同的是,我们可以为类动态添加方法,不管这个类是否已存在。
另外,参数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:方法来达到相同目的。
协议(objc_protocol_list)
协议相关的操作包含以下函数:1
2
3
4
5
6// 添加协议
BOOL class_addProtocol ( Class cls, Protocol *protocol );
// 返回类是否实现指定的协议
BOOL class_conformsToProtocol ( Class cls, Protocol *protocol );
// 返回类实现的协议列表
Protocol * class_copyProtocolList ( Class cls, unsigned int *outCount );
class_conformsToProtocol函数可以使用NSObject类的conformsToProtocol:方法来替代。
class_copyProtocolList函数返回的是一个数组,在使用后我们需要使用free()手动释放。
版本(version)
版本相关的操作包含以下函数:1
2
3
4// 获取版本号
int class_getVersion ( Class cls );
// 设置版本号
void class_setVersion ( Class cls, int version );
其它
runtime还提供了两个函数来供CoreFoundation的tool-free bridging使用,即:1
2Class objc_getFutureClass ( const char *name );
void objc_setFutureClass ( Class cls, const char *name );
通常我们不直接使用这两个函数。
实例
上面列举了大量类操作的函数,下面我们写个实例,来看看这些函数的实例效果: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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52- (void)classAssociatedOption
{
NSLog(@"类相关操作===========\n");
NSMutableString *str = [NSMutableString stringWithString:@""];
//获取类名
const char *charName = class_getName(self.ps.class);
NSString *name = [NSString stringWithCString:charName encoding:NSUTF8StringEncoding];
[str appendFormat:@"获取类名:%@\n",name];
NSLog(@"=================\n");
//获取父类
const char *superCharName = class_getName(class_getSuperclass(self.ps.class));
NSString *superName = [NSString stringWithCString:superCharName encoding:NSUTF8StringEncoding];
NSLog(@"获取父类类名:%@",superName);
[str appendFormat:@"获取父类类名:%@\n",superName];
NSLog(@"=================\n");
//是否是元类
BOOL isMeta = class_isMetaClass(self.ps.class);
[str appendFormat:@"%@是否是元类:%@\n",name,isMeta?@"是":@"否"];
NSLog(@"=================\n");
//获取元类(objc_方法)
Class metaClass = objc_getMetaClass(charName);
const char *charMetaClassName = class_getName(metaClass);
NSString *metaName = [NSString stringWithCString:charMetaClassName encoding:NSUTF8StringEncoding];
[str appendFormat:@"%@的元类名:%@;是否是元类:%@\n",name,metaName,class_isMetaClass(metaClass)?@"是":@"否"];
NSLog(@"=================\n");
//变量实例大小
[str appendFormat:@"%@的变量实例大小:%zu\n",name,class_getInstanceSize(self.ps.class)];
NSLog(@"=================\n");
//获取指定名称的实例
Ivar ivar = class_getInstanceVariable(self.ps.class, "_name");
if (ivar != NULL) {
[str appendFormat:@"获取%@类中指定_name的实例:%s\n",name,ivar_getName(ivar)];
}
NSLog(@"=================\n");
// 协议
unsigned outCount;
Protocol * __unsafe_unretained * protocols = class_copyProtocolList(self.ps.class, &outCount);
Protocol * protocol;
for (int i = 0; i < outCount; i++) {
protocol = protocols[i];
[str appendFormat:@"获取%@类中遵循的协议名%s\n",name,protocol_getName(protocol)];
}
BOOL isConforms = class_conformsToProtocol(self.ps.class, protocol);
[str appendFormat:@"获取%@类是否遵循了%s协议:%@\n",name,protocol_getName(protocol),isConforms ? @"是":@"否"];
NSLog(@"=================\n");
self.textView.text = [str copy];
}
结果:
获取类名:Person
获取父类类名:NSObject
Person是否是元类:否
Person的元类名:Person;是否是元类:是
Person的变量实例大小:48
获取Person类中指定_name的实例:_name
获取Person类中遵循的协议名NSCopying
获取Person类是否遵循了NSCopying协议:是1
2
动态创建类和对象
runtime的强大之处在于它能在运行时创建类和对象。
动态创建类
动态创建类涉及到以下几个函数:
1 | // 创建一个新类和元类 |
objc_allocateClassPair函数:如果我们要创建一个根类,则superclass指定为Nil。extraBytes通常指定为0,该参数是分配给类和元类对象尾部的索引ivars的字节数。
为了创建一个新类,我们需要调用objc_allocateClassPair。然后使用诸如class_addMethod,class_addIvar等函数来为新创建的类添加方法、实例变量和属性等。完成这些后,我们需要调用objc_registerClassPair函数来注册类,之后这个新类就可以在程序中使用了。
实例方法和实例变量应该添加到类自身上,而类方法应该添加到类的元类上。
objc_disposeClassPair函数用于销毁一个类,不过需要注意的是,如果程序运行中还存在类或其子类的实例,则不能调用针对类调用该方法。
在前面介绍元类时,我们已经有接触到这几个函数了,在此我们再举个实例来看看这几个函数的使用。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
26
27
28
29
30
31- (void)dynamicCreateClass
{
//创建一个继承自self.ps的类SubClass
Class subClass = objc_allocateClassPair(self.ps.class, "SubClass", 0);
void(^block)(void) = ^(){
NSLog(@"block 调用");
};
IMP imp_subMethod = imp_implementationWithBlock(block);
//向SubClass中添加方法
class_addMethod(subClass, @selector(addMethod), imp_subMethod, "v@:");
//添加实例变量
class_addIvar(subClass, "_ivar1", sizeof(NSString *), log(sizeof(NSString *)), @encode(NSString *));
//添加属性
objc_property_attribute_t type = {"T", "@\"NSString\""};
objc_property_attribute_t ownership = { "C", "" };
objc_property_attribute_t backingivar = { "V", "_ivar1"};
objc_property_attribute_t attrs[] = {type, ownership, backingivar};
class_addProperty(subClass, "property2", attrs, 3);
//注册
objc_registerClassPair(subClass);
//创建类的实例
id instance = [[subClass alloc] init];
[instance performSelector:@selector(addMethod)];
[instance setValue:@"hello world" forKey:@"_ivar1"];
NSLog(@"_ivar1======%@===",[instance valueForKey:@"_ivar1"]);
}
动态创建对象
动态创建对象的函数如下:1
2
3
4
5
6// 创建类实例
id class_createInstance ( Class cls, size_t extraBytes );
// 在指定位置创建类实例
id objc_constructInstance ( Class cls, void *bytes );
// 销毁类实例
void * objc_destructInstance ( id obj );
class_createInstance函数:创建实例时,会在默认的内存区域为类分配内存。extraBytes参数表示分配的额外字节数。这些额外的字节可用于存储在类定义中所定义的实例变量之外的实例变量。该函数在ARC环境下无法使用。
调用class_createInstance的效果与+alloc方法类似。不过在使用class_createInstance时,我们需要确切的知道我们要用它来做什么。在下面的例子中,我们用NSString来测试一下该函数的实际效果:1
2
3
4
5
6
7
8
9- (void)dynamicInstance
{
id theObject = class_createInstance(NSString.class, sizeof(unsigned));
id str1 = [theObject init];
NSLog(@"%@", [str1 class]);
id str2 = [[NSString alloc] initWithString:@"test"];
NSLog(@"%@", [str2 class]);
}
输出结果是:
1 | NSString |
可以看到,使用class_createInstance函数获取的是NSString实例,而不是类簇中的默认占位符类__NSCFConstantString。
objc_constructInstance函数:在指定的位置(bytes)创建类实例。
objc_destructInstance函数:销毁一个类的实例,但不会释放并移除任何与其相关的引用。
实例操作函数
实例操作函数主要是针对我们创建的实例对象的一系列操作函数,我们可以使用这组函数来从实例对象中获取我们想要的一些信息,如实例对象中变量的值。这组函数可以分为三小类:
1.针对整个对象进行操作的函数,这类函数包含1
2
3
4// 返回指定对象的一份拷贝
id object_copy ( id obj, size_t size );
// 释放指定对象占用的内存
id object_dispose ( id obj );
上面个两个方法在ARC模式下不可用
2.针对对象实例变量进行操作的函数,这类函数包含:1
2
3
4
5
6
7
8
9
10// 修改类实例的实例变量的值
Ivar object_setInstanceVariable ( id obj, const char *name, void *value ); //ARC模式下不可用
// 获取对象实例变量的值
Ivar object_getInstanceVariable ( id obj, const char *name, void **outValue );//ARC模式下不可用
// 返回指向给定对象分配的任何额外字节的指针
void * object_getIndexedIvars ( id obj );//ARC模式下不可用
// 返回对象中实例变量的值
id object_getIvar ( id obj, Ivar ivar );
// 设置对象中实例变量的值
void object_setIvar ( id obj, Ivar ivar, id value );
3.针对对象的类进行操作的函数,这类函数包含:1
2
3
4
5
6// 返回给定对象的类名
const char * object_getClassName ( id obj );
// 返回对象的类
Class object_getClass ( id obj );
// 设置对象的类
Class object_setClass ( id obj, Class cls );
获取类定义
Objective-C动态运行库会自动注册我们代码中定义的所有的类。我们也可以在运行时创建类定义并使用objc_addClass函数来注册它们。runtime提供了一系列函数来获取类定义相关的信息,这些函数主要包括:1
2
3
4
5
6
7
8
9
10// 获取已注册的类定义的列表
int objc_getClassList ( Class *buffer, int bufferCount );
// 创建并返回一个指向所有已注册类的指针列表
Class * objc_copyClassList ( unsigned int *outCount );
// 返回指定类的类定义
Class objc_lookUpClass ( const char *name );
Class objc_getClass ( const char *name );
Class objc_getRequiredClass ( const char *name );
// 返回指定类的元类
Class objc_getMetaClass ( const char *name );
objc_getClassList函数:获取已注册的类定义的列表。我们不能假设从该函数中获取的类对象是继承自NSObject体系的,所以在这些类上调用方法是,都应该先检测一下这个方法是否在这个类中实现。
下面代码演示了该函数的用法:1
2
3
4
5
6
7
8
9
10
11
12
13int numClasses;
Class * classes = NULL;
numClasses = objc_getClassList(NULL, 0);
if (numClasses > 0) {
classes = malloc(sizeof(Class) * numClasses);
numClasses = objc_getClassList(classes, numClasses);
NSLog(@"number of classes: %d", numClasses);
for (int i = 0; i < numClasses; i++) {
Class cls = classes[i];
NSLog(@"class name: %s", class_getName(cls));
}
free(classes);
}
获取类定义的方法有三个:objc_lookUpClass, objc_getClass和objc_getRequiredClass。如果类在运行时未注册,则objc_lookUpClass会返回nil,而objc_getClass会调用类处理回调,并再次确认类是否注册,如果确认未注册,再返回nil。而objc_getRequiredClass函数的操作与objc_getClass相同,只不过如果没有找到类,则会杀死进程。
objc_getMetaClass函数:如果指定的类没有注册,则该函数会调用类处理回调,并再次确认类是否注册,如果确认未注册,再返回nil。不过,每个类定义都必须有一个有效的元类定义,所以这个函数总是会返回一个元类定义,不管它是否有效。
小结
在这一章中我们介绍了Runtime运行时中与类和对象相关的数据结构,通过这些数据函数,我们可以管窥Objective-C底层面向对象实现的一些信息。另外,通过丰富的操作函数,可以灵活地对这些数据进行操作。
QA
1、用分类给NSObject添加一个实例方法(例如- (void)dm_instanceMethod),用类调用的方式调用该方法([NSObject dm_instanceMethod])会发生什么?
答案:正常调用
回答要点:
1、类即对象
2、NSObject的isa指向metaClass,即NSObject是metaClass的实例,即NSObject 是一个类对象
3、metaClass继承自NSObject
NSObject类对象 调用了从NSObject继承下来的dm_instanceMethod方法
结论:
对于NSObject继承体系来说,其实例方法对体系中的所有实例、类和meta-class都是有效的;而类方法对于体系内的所有类和meta-class都是有效的。
验证代码如下:
1 | @implementation NSObject (DM) |
可以通过[NSObject dm_instanceMethod]调用,也可以通过其子类[Father dm_instanceMethod]调用
而Father中的方法只能通过Father的实例来调用[[Father new] getSex],不可以通过[Father getSex]调用。
参考
Objective-C Runtime 运行时之一:类与对象
Objective-C Runtime Programming Guide
Objective-C Runtime