该系列文章系个人读书笔记及总结性内容,任何组织和个人不得转载进行商业活动!
第八章 运行时系统的结构
运行时系统是OC平台的关键元素;
OC语言的动态特性和面向对象功能就是由它实现的;
运行时系统提供了公用API,是你编写的代码能够直接调用运行时系统服务;
8.1 运行时系统的组成部分
OC运行时系统的两个组成部分:编译器 和 运行时系统库;
8.1.1 编译器
第五章简述了源代码常规编译过程;
编译进程会接收OC源文件并进行处理(该处理过程由词法分析、语法分析、代码生成和优化、汇编以及链接操作等阶段构成),生成构成可执行程序的二进制文件;
和C语言标准库会为C语言程序提供标准API和实现代码一样,运行时系统库也会为OC的面向对象特性 提供标准的API和实现代码;
这种库与所有OC程序链接(在链接阶段);
编译器的作用是接收输入的源代码,生成使用了运行时系统库的代码,从而得到合法的可执行的OC程序;
OC语言中的面向对象元素和动态特性都是通过运行时系统实现的;
运行时系统组成部分:
1)类元素(接口、实现代码、协议、分类、方法、属性、实例变量);
2)类实例(对象);
3)对象消息传递(包括动态类型和动态绑定);
4)动态方法决议;
5)动态加载;
6)对象内省;
简单来说,当编译器解析使用了这些语言元素和特性的OC源代码时,就会使用适当的运行时系统库数据结构和实现该语言特定行为的函数,生成可执行代码;
我们来看看编译器如何为OC类和对象生成可执行代码,以及如何实现对象消息;
1.生成对象消息传递代码
当编译器解析对象消息(发送消息的表达式)时,如:
[接收器 消息]
它会生成 调用运行时系统库中函数objc_msgSend()的代码;
该函数将接收器、选择器和消息传递的参数作为输入参数;
因此,编译器会将源代码中的所有消息传递表达式([接收器 消息]形式的),转换为调用运行时系统库函数objc_msgSend(...)的代码,并为这些调用代码提供源代码所提供的参数;
每条消息都是以动态的方式处理的,这意味着接收器的类型和方法的实际实现代码都是在运行程序时决定的;
对于源代码中的类和对象来说,编译器创建了执行对象消息操作所需要的数据结构;
2.生成类和对象的代码
当编译器解析含有类定义和对象的OC源码时;
它会生成 相应的运行时数据结构;
OC中的类与运行时系统库中的Class数据结构对应;
Class数据类型是指向带objc_class标识符的不透明数据类型的指针,如:
typedef struct objc_class * Class;
不透明数据类型是一种接口定义不完整的C语言结构类型;
不透明类型提供了一种数据隐藏模式;
因为其变量只能由专门为它们定义的函数访问;
使用运行时系统库中的函数可以访问Class(即objc_class)数据类型的变量;
OC类拥有运行时的数据类型,与之类似,OC对象也有;
编译器在解析OC对象的源代码时,会生成创建运行时对象类型的可执行代码;
这种数据类型是一种带有objc_object标识符的C语言结构;
可以大致看下objc_object数据类型:
struct objc_object{
Class isa;
'/'*...含有实例变量值的长度可变数据...'*'/
}
当你编写的程序创建对象时,系统会为objc_object类型数据分配内存;
可以看到,这种数据由isa指针后跟实例变量的数据组成;
我们注意到,上述结构中isa是Class结构的;
像Class数据类型一样,objc_object类型也含有Class类型的isa变量,换言之,该变量就是指向objc_class类型变量的指针;
事实上,所有OC对象和类的运行时类型都是以isa指针开头的;
与OC中id数据类型对应的运行时数据类型是一种C语言结构,该结构被定义为指向objc_object的指针;
typedef struct objc_object{
Class isa;
} * id;
也就是说,id就是一个带有objc_object标识符的、指向C语言结构的指针;
同样,OC块对象也拥有相应的运行时数据结构,因此运行时系统也能够以适当的方式管理他们;
3.查看运行时系统的数据结构
掌握了以上概念,我们来看一个例子;
新建C8TestClass1类;
(Code1)
#import <objc/runtime.h>
//
@interface C8TestClass1:NSObject{
@public
int myInt;
}
@end
@implementation C8TestClass1
@end
测试代码如下:
C8TestClass1 * tc1A = [[C8TestClass1 alloc] init];
tc1A->myInt = 0x5a5a5a;
C8TestClass1 * tc1B = [[C8TestClass1 alloc] init];
tc1B->myInt = 0xc3c3c3;
long tc1Size = class_getInstanceSize([C8TestClass1 class]);
NSData * obj1Data = [NSData dataWithBytes:(__bridge const void *)(tc1A) length:tc1Size];
NSData * obj2Data = [NSData dataWithBytes:(__bridge const void *)(tc1B) length:tc1Size];
NSLog(@"C8TestClass1 object tc1 contains %@",obj1Data);
NSLog(@"C8TestClass1 object tc2 contains %@",obj2Data);
NSLog(@"C8TestClass1 memory address = %p",[C8TestClass1 class]);
log:
2017-12-07 16:38:41.888771+0800 精通Objective-C[69375:18798951] C8TestClass1 object tc1 contains <88096101 01000000 5a5a5a00 00000000>
2017-12-07 16:38:41.888945+0800 精通Objective-C[69375:18798951] C8TestClass1 object tc2 contains <88096101 01000000 c3c3c300 00000000>
2017-12-07 16:38:41.889044+0800 精通Objective-C[69375:18798951] C8TestClass1 memory address = 0x101610988
分析下这段代码:
1)导入运行时头文件;
2)新建测试类,定义了一个全局的实例变量;
3)使用NSData类获取已经创建对象的数据(以字节为单位);
这里还是用了运行时系统库函数class_getInstanceSize获取类实例的尺寸(以字节为单位);
运行我们得到了上述log;
编译器解析对象时,就会生成objc_object类型的实例;
该实例由一个isa指针和对象实例变量的值构成;
因此上述log中对象tc1包含的两项内容:
一个是isa指针(88096101 01000000)
一个是对象实例变量的值(5a5a5a00 00000000)
同样的对象tc2也包含的两项内容:
一个是isa指针(88096101 01000000)
一个是对象实例变量的值(c3c3c300 00000000)
注意:
对象objc_object数据结构中的第一项就是其isa指针;
两个对象的isa指针都是相同的,因为他们是同一个类的实例,因此拥有相同的指针值;
这个isa指针,指向该类的内存地址;
这里可能会一会我们打印的[C8TestClass1 class]的内存地址是0x101610988,与前面显示的指针值不一样;
实际是相同的,这个程序是在MacPro上运行的,而这种计算机使用的是低字节序(little-endian),换言之它们会使用翻转的字节顺序存储数据;
这个0x101610988是以翻转的字节顺序显示的;
我们接着看
@interface C8TestClass2:NSObject{
@public
int myInt;
}
@end
@implementation C8TestClass2
@end
id testClz = objc_getClass("C8TestClass1");
long tcSize = class_getInstanceSize([testClz class]);
NSData * tcData = [NSData dataWithBytes:(__bridge const void * _Nullable)(testClz) length:tcSize];
NSLog(@"C8TestClass1 class contains %@",tcData);
NSLog(@"C8TestClass1 superclass memory address = %p",[C8TestClass1 superclass]);
id testClr = objc_getClass("C8TestClass2");
long tcrSize = class_getInstanceSize([testClr class]);
NSData * tcrData = [NSData dataWithBytes:(__bridge const void * _Nullable)(testClr) length:tcrSize];
NSLog(@"C8TestClass2 class contains %@",tcrData);
NSLog(@"C8TestClass2 superclass memory address = %p",[C8TestClass2 superclass]);
log:
2017-12-07 17:59:13.679936+0800 精通Objective-C[70080:18859295] C8TestClass1 object tc1 contains <e87a9a0c 01000000 5a5a5a00 00000000>
2017-12-07 17:59:13.680099+0800 精通Objective-C[70080:18859295] C8TestClass1 object tc2 contains <e87a9a0c 01000000 c3c3c300 00000000>
2017-12-07 17:59:13.680194+0800 精通Objective-C[70080:18859295] C8TestClass1 memory address = 0x10c9a7ae8
2017-12-07 17:59:13.680303+0800 精通Objective-C[70080:18859295] C8TestClass1 class contains <c07a9a0c 01000000 a81e960d 01000000>
2017-12-07 17:59:13.680387+0800 精通Objective-C[70080:18859295] C8TestClass1 superclass memory address = 0x10d961ea8
2017-12-07 17:59:13.680515+0800 精通Objective-C[70080:18859295] C8TestClass2 class contains <107b9a0c 01000000 a81e960d 01000000>
2017-12-07 17:59:13.680631+0800 精通Objective-C[70080:18859295] C8TestClass2 superclass memory address = 0x10d961ea8
再来看一下这段log:
前三个打印还是之前对象包含的isa指针和实例变量的值;
C8TestClass1类含有的数据是一个isa指针(c07a9a0c 01000000)和另一个值(a81e960d 01000000);
这个值实际上是指向该类的父类的指针;
我们之前介绍过:
Class数据类型和objc_object类型都含有Class类型的isa变量,这里也可以看到类的数据结构确实拥有isa指针;
这里我又新建了一个类C8TestClass2,做了同样的操作,由于和C8TestClass1都继承了NSObject类,所以可以看到打印类中含有的后一个数据是相同的,都是指向父类的指针;
这里大家可能会有一个疑问:
对象的isa指针是指向类的地址,那么类的isa指针指向的又是什么呢?
我们先留下这个问题,一会会解答;
现在我们已经了解到了编译器在运行时系统中的作用,使用运行时系统API检查程序来观察编译器生成的数据结构;
接下来再看看运行时系统库及其实现细节,相信我,你会学到很多!