运行时系统是Objective-C平台的关键元素,它实现了语言的动态特点和面向对象的能力。它的结构能让你开发代码时不用接触到运行时的内部,但是提供了一个公共的API,让你写代码直接激活运行时服务。
下面,我们来探索下运行时系统的架构和设计以及它如何实现动态特点的。你将学习运行时的主要组件,关键的实现细节,然后研究下你的代码在编译和运行期间是如何与运行时进行交互的。
运行时组件(Runtime Components)
Objective-C运行时系统有两个主要的组件:编译器和运行时库。
一.编译器(Compiler)
就像C标准库提供了标准API和实现了C编程语言一样,运行时库提供了标准API并实现了Objective-C面向对象的特点。(在链接阶段)运行时库被链接到所有的Objective-C程序上。而编译器负责你的输入源代码,然后使用运行时库来产生有效、可执行的Objective-C程序。
Objective-C语言的面向对象元素和动态特点都是由运行时系统来实现的,包含下面:
当编译器使用这些语言元素和特点进行解析Objective-C源代码时,它会利用正确的运行时库的数据结构和函数调用来产生代码,从而实现特定的行为。
1.对象消息代码产生:
当编译器解析对象消息(即消息发送表达式)时,如:[receiver message]
,它会调用运行时库的函数objc_msgSend()
来产生代码。这个函数会将接收者、消息的selector以及消息中的参数作为它的输入参数。因此,编译器会将源代码中的每个发送消息表达式(类似[receiver message]
的形式)转化为对运行时库的函数objc_msgSend(...)
的调用。每个消息是动态被解析的,意味着消息接收者类型和被激活的实际方法实现是在运行时决定的。而对于源代码中的每个类和对象,编译器构造数据结构来执行对象消息。
2.类和对象代码产生
当编译器解析包含类定义和对象的Objective-C代码时,它会产生对应的运行时数据结构。一个Objective-C类对应一个运行时库的Class
数据类型。Class
数据类型是一个指向具有objc_class标识的不透明类型(opaque type)的指针。
typedef struct objc_class *Class;
不透明类型(opaque type)是C结构体类型,没有完全地定义在接口中。不透明类型提供了数据隐藏,它们的变量只能通过特殊指定的函数来获取。运行时库函数被用来获取Class
数据类型的变量。
就像Objective-C类有一个运行时数据类型一样,Objective-C对象也有一个对应的运行时数据类型。当编译器解析Objective-C对象时,它会产生运行时的对象类型。这个数据类型,是一个拥有objc_object
标识的C结构体。
struct objc_object {
Class isa OBJC_ISA_AVAILABILITY;
};
当你创建一个对象时,内存就会分配给包含isa指针的objc_object
类型。
注意:objc_object
类型包含了名字为isa的Class类型的变量,也就是说,isa是一个指向类型为objc_class的变量的指针。实际上,所有Objective-C的类和对象所对象的运行时数据类型都是以isa指针开始的。Objective-C的id类型对应的运行时是一个C结构体,作为一个指向objc_object
的指针。
typedef struct objc_object *id;
可以看出,id就是一个指向拥有objc_object
标识符的C结构体的指针。Objective-C代码块对象对应的运行时数据结构也是一样的道理。
3.看一下运行时数据结构
从代码看起:
#import
#import
@interface TestClass : NSObject
{
@public
int myInt;
}
@end
@implementation TestClass
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
TestClass* tcA=[[TestClass alloc] init];
tcA->myInt=0xa5a5a5a5;
TestClass* tcB=[[TestClass alloc] init];
tcB->myInt=0xc3c3c3c3;
long tcSize=class_getInstanceSize([TestClass class]);
NSData* obj1Data=[NSData dataWithBytes:(__bridge const void *)(tcA) length:tcSize];
NSData* obj2Data=[NSData dataWithBytes:(__bridge const void *)(tcB) length:tcSize];
NSLog(@"TestClass object tcA contains %@",obj1Data);
NSLog(@"TestClass object tcB contains %@",obj2Data);
NSLog(@"TestClass memory address =%p",[TestClass class]);
}
return 0;
}
首先,引入头文件:#import
,这样才能在文件中包含运行时库的APIs。接着创建了一个类TestClass,这个类没有方法,只有一个实例变量。在main()函数中,创建了两个TestClass类的对象,并且为各自的实例变量赋了值。NSData类被用来获取每个对象的数据,利用它的类方法:+ (instancetype)dataWithBytes:(const void *)bytes length:(NSUInteger)length;
,运行时函数class_getInstanceSize()
被用来获取class实例的大小。
代码运行结果为:
我们来分析下输入的结果:当编译器解析一个对象时,它会产生一个objc_object
类型的实例,该实例包含一个isa指针和对象的实例变量的值。注意到:TestClass的一个对象tcA的数据包含两个项目:一个isa指针(d1110000
01801d00
)和分配它的实例变量的值(a5a5a5a5
00000000
);TestClass的一个对象tcA也是一样的。两个实例的isa指针是相同的,因为它们是同一个类的实例,所偶一应当有相同的指针值。
现在看一下指向class内存地址的isa指针:TestClass memory address =0x1000011d0
。你可能会想,isa指针的内存地址怎么与以上的不一样?原因是这样的:Mac Pro电脑是little-endian(低字节序),意味着它会将内存中存储的字节顺序颠倒。
下面更新一下main()函数,添加下面的代码:
id testC=objc_getClass("TestClass");
long tCSize=class_getInstanceSize(testC);
NSData* tcData=[NSData dataWithBytes:(__bridge const void *)(testC) length:tCSize];
NSLog(@"TestClass class contains %@",tcData);
NSLog(@"TestClass superclass memory address =%p",[TestClass superclass]);
运行时的Class objc_getClass(const char *name)
函数用来获取TestClass的class。
输出结果如下:
分析一下:TestClass对象的数据与之前看到的是一致的:每个包含一个isa指针和分配给它的实例变量的值。TestClass的class对象是由一个isa指针(f9110000
01801d00
)和另一个值(f0e0f872
ff7f0000
)构成的。实际上,这个额外的值是指向对象的父类的指针。之前,我们知道,一个类对应的数据结构有一个isa指针。
运行时库:
苹果的Objective-C运行时库实现了Objective-C语言的面向对象特点和动态属性。大不多数情况下,运行库在程序背后工作,但它也包含了我们在代码中可以直接使用的公共API。
运行时库API是用C写的,由一系列函数、数据类型、语言常量构成的。在前面,你学习了一些运行时数据类型(如objc_object,objc_class),使用了一些函数。
总体上说,运行时库数据类型可以分成下面这些:
函数可以分为下面的类别:
运行时库也定义了几个Boolean常量(YES,NO)和null值(NULL,nil,Nil)
运行时库的公共API是在runtime.h头文件中声明的。随着Objective-C语言的发展,运行时库结合了一系列设计元素和系统服务来提供完美的性能和扩展性。
一.使用运行时库APIs来创建一个Class
看下面代码:
main.m文件
#import
#import
#import
NSString *greeting(id self,SEL _cmd)
{
return [NSString stringWithFormat:@"Hello,World"];
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
//动态创建一个类
Class dynaClass=objc_allocateClassPair([NSObject class], "DynaClass", 0);
//动态添加一个方法,使用存在的方法来获取签名
Method description=class_getInstanceMethod([NSObject class], @selector(description));
const char *types=method_getTypeEncoding(description);
class_addMethod(dynaClass, @selector(greeting), (IMP)greeting, types);
objc_registerClassPair(dynaClass);
id dynaObj=[[dynaClass alloc] init];
NSLog(@"%@",objc_msgSend(dynaObj,NSSelectorFromString(@"greeting")));
}
return 0;
}
当然,还有进行工程配置,请参照我的另一篇博客: 使用objc_msgSend要进行配置
首先要导入文件:#import
,这样就在文件中包含了运行时库的发送消息API。接下来定义了greeting()函数,这个函数(前两个参数是固定的)用来对方法的实现,而该方法将会被动态地添加到类中。main()函数中使用运行时API创建了一个新类,它创建了一个class pair(类对,即class和metaclass),接着向类中添加了一个方法,该方法指向之前定义的greeting(函数),然后在运行时注册了class pair。注意到,方法签名是通过使用NSObject的description的方法签名来获得的。
二.实现运行时对象消息(Object Messaging)
运行库中包含能够获取以下信息的函数:
Class objc_getClass(const char *name)
:获取对象的类定义Class class_getSuperclass(Class cls)
:获取一个类的父类Class objc_getMetaClass(const char *name)
:获取对象的metaclass定义const char *class_getName(Class cls)
:获取一个类的名字int class_getVersion(Class cls)
:获取类的版本信息size_t class_getInstanceSize(Class cls)
:类的大小,以字节为单位Ivar *class_copyIvarList(Class cls, unsigned int *outCount)
:获取类的实例变量的list(列表)Method *class_copyMethodList(Class cls, unsigned int *outCount)
:获取类的方法的list(列表)Protocol * __unsafe_unretained *class_copyProtocolList(Class cls, unsigned int *outCount)
:获取类的协议的list(列表)objc_property_t class_getProperty(Class cls, const char *name)
:获取类的属性的list(列表)运行时的数据类型和函数给运行时库足够的信息来实现各种Objective-C特点。
运行时库包含大量的设计机制来实现对象消息(object messaging),下面会介绍几种:
1.通过vtable来查找方法(Method)
运行时库定义了一个方法数据类型(object_method),如下:
struct objc_method {
SEL method_name
char *method_types
IMP method_imp
}
typedef struct objc_method *Method;
其中,method_name是SEL类型的变量,描述了方法名字;method_types描述了方法的参数的数据类型;method_imp是IMP类型的变量,提供了被调用的函数的地址。
因为方法调用在程序运行期间可能会执行成千上万次,所以运行时系统需要一个快速、有效的机制来进行方法查找和调用。vtable
,也叫dispatch table
(调度表),就是一种用来在程序中支持动态绑定的机制。vtable是一组IMP,每个运行时Class实例都有一个指向vtable的指针,当然,每个Class实例也包含了指向最近使用过的方法的指针的缓存。
如图,运行时库首先搜寻缓存来查找方法的IMP,如果没找到,让后在vtable中查找方法,如果找到,IMP就会被存储在缓存中;这种设计让运行时能执行一个快速、有效的方法查找。
2.通过dyld Shared Cache来实现Selector Uniquing
如果不懂这几个术语,请看dyld Shared Cache 和Selector Uniquing
selector名称在可执行程序中必须是唯一的,但是程序中自定义的类和共享的库都会包含selector名称,许多可能就会重复。运行时需要为每个selector名称选择一个单独的SEL指针值,然后更新元数据。这个处理必须要在程序启动时执行,为了让这个处理更加有效,运行时通过使用dyld Shared Cache来实现Selector Uniquing。
3.获取[Class实例](即类)方法
每个Objective-C类其实上也是一个对象,因此它也能够接收消息,例如:[NSObject alloc]
。运行时如何发现和调用类方法呢?运行时库通过metaclass(元类)来实现的。
元类是一种特殊的类,它存储的信息能让运行时寻找和调用Objective-C的类方法。每个Objective-C都有一个独特的元类,因为每个类都有可能有一系列类方法。运行时API提供了函数来获取元类。
4.
代码如下:
#import
#import
@interface TestClass : NSObject
{
@public
int age;
}
@end
@implementation TestClass
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
id metaClass=objc_getMetaClass("TestClass");
long mcSize=class_getInstanceSize([metaClass class]);
NSData* mcData=[NSData dataWithBytes:(__bridge const void *)(metaClass) length:mcSize];
NSLog(@"TestClass metaclass contains %@",mcData);
class_isMetaClass(metaClass) ? NSLog(@"Class %s is a metaclass",class_getName(metaClass)):NSLog(@"Class %s is a metaclass",class_getName(metaClass));
}
return 0;
}
代码使用了运行时函数objc_getMetaClass()
来获取元类定义,然后使用运行时函数class_isMetaClass来判断对象是否是一个元类。
结果如下:
元类数据包含了isa指针,父类的指针和其它额外的信息。TestClass的父类是NSObject,因为这个类没有自定义类方法,所以它的isa指针也是NSObject,因此,元类的isa指针和父类指针是相同的。
二.与运行时交互
Objective-C程序通过与运行时系统交互来实现语言的动态特点,这种交互有三种等级:
NSObject运行时方法:
Foundation 框架中的NSObject类提供了一系列方法,能够实现运行时API所拥有的大量功能。因为你自定义的类和Cocoa 框架中的几乎所有类都继承自NSObject,所以你可以直接使用这些方法。由NSObject运行时方法提供的功能包括: