runtime 运行时--篇章二

运行时系统 – 篇章二

[TOC]

这一章讲,运行时系统的结构。
runtime开源代码可以点击这里下载,已经编译好了,下载即可直接运行。

运行时的组成部分

OC运行时系统由2个主要部分构成:编译器和运行时系统库

编译器

像C语言标准函数库为C语言程序提供标准API和实现代码一样。运行时系统库也会为OC面向对象的特性提供标准API和实现代码。这种库与所有OC程序链接。

编译器的作用是接受输入的源代码,生成使用了运行时系统库的代码,从而得到合法、可执行的OC程序。而不管是OC还是Swift,都是采用Clang作为编译器前端,LLVM(Low level vritual machine)作为编译器后端。

  • 编译器前端
    编译器前端的任务是进行:语法分析,语义分析,生成中间代码(intermediate representation )。在这个过程中,会进行类型检查,如果发现错误或者警告会标注出来在哪一行。

  • 编译器后端
    编译器后端会进行机器无关的代码优化,生成机器语言,并且进行机器相关的代码优化。

且看编译器如何为OC的类和对象生成可执行的代码,以及如何实现对象消息:

生成对象消息传递代码

当编译器解析对象消息(发送消息的表达式)时,如:

1
[接收器  消息]

它会生成并调用运行时系统库中函数objc_msgSend()的代码。该函数将接收器、选择器、消息传递的参数 作为输入参数。
编译器会将所有的消息传递表达式转换为运行时系统函数代码,并提供相应的参数。而每条消息都是以动态方式处理的,所以又回到了之前所讲的:接收器的类型和方法实现代码都是在运行时决定的。

1
objc_msgSend(receiver, selector)
  • 首先它找到selector对应的方法实现。因为同一个方法可能在不同的类中有不同的实现,所以我们需要依赖于接收者的类来找到的确切的实现。
  • 它调用方法实现,并将接收者对象及方法的所有参数传给它。
  • 最后,它将实现返回的值作为它自己的返回值。

对于源代码中的类和对象来说,编译器创建了执行对象消息操作所需的数据结构。

生成类和对象代码

当编译器解析含有类定义和对象的OC源代码时,会生成相应的运行时数据结构。

OC的类与运行时系统库中的Class数据结构对应。在objc.h中,Class是指向objc_class类型的结构体指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// An opaque type that represents an Objective-C class. 
// 指向带有objc_class标识符的不透明数据类型的指针。
typedef struct objc_class *Class;

struct objc_class {
Class isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE;
const char *name OBJC2_UNAVAILABLE;
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE;
struct objc_method_list **methodLists OBJC2_UNAVAILABLE;
struct objc_cache *cache OBJC2_UNAVAILABLE;
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;

不透明数据类型是一种接口定义不完整的C语言结构类型。提供了一种数据隐藏模式,其变量只能由专门为它们定义的函数访问。使用运行时系统库中的函数就可以访问Class(即objc_class)数据类型

同样,OC对象也拥有,编译器创建的运行时数据类型:

1
2
3
4
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
// 后面还有实例变量
};

当我们创建对象时,系统会为objc_object类型的结构体分配内存,这种数据由isa指针后面跟实例变量的数据构成。

objc_object类型也有Class类型的isa指针(也是就是说每个对象都有一个isa指针,指向类对象)。所有的OC对象和类都是以isa指针开头,isa指针是指向objc_class结构体的指针。

让我们来看看id数据类型的定义:

1
2
3
4
5
6
// 在objc.h中
typedef struct objc_object *id;
// 我们可以理解成这样
typedef struct objc_object {
Class _Nonnull isa;
} *id;

显而易见,id 就是指向objc_object结构体的指针。

查看运行时系统的数据结构

OC 对象运行时会转化为objc_object类型的结构体,类会转化为objc_class的结构体。下面我们通过OC代码来观察一下:

1
2
3
4
5
6
7
@interface ANPerson : NSObject
{
@public
int age;
}

@end
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
    // 对象
ANPerson *person1 = [[ANPerson alloc] init];
person1->age = 0xa5a5a5a5;

ANPerson *person2 = [[ANPerson alloc] init];
person2->age = 0xc3c3c3c3;

NSInteger person1Size = class_getInstanceSize([person1 class]);
NSData *person1Data = [NSData dataWithBytes:(__bridge const void *)(person1) length:person1Size];
NSData *person2Data = [NSData dataWithBytes:(__bridge const void *)(person2) length:person1Size];

NSLog(@"\r\n person1 %p -- %@",person1, person1Data);
NSLog(@"\r\n person2 %p -- %@",person2, person2Data);
NSLog(@"\r\n person %p",[person1 class]);
// 类
id perClass = objc_getClass("ANPerson");
NSInteger perClassSize = class_getInstanceSize([perClass class]);
NSData *perClassData = [NSData dataWithBytes:(__bridge const void *)(perClass) length:perClassSize];
NSLog(@"\r\n person class %@ -- %@",perClass, perClassData);
NSLog(@"\r\n person superclass -- %p", [ANPerson superclass]);

id perMetaClass = objc_getMetaClass("ANPerson");
NSLog(@"\r\n person Mateclass %p",perMetaClass);

// 打印结果

person1 0x600000009fd0 -- <40869707 01000000 a5a5a5a5 00000000>
person2 0x600000009eb0 -- <40869707 01000000 c3c3c3c3 00000000>

person 0x107978640

person class 0x107978640 -- <18869707 01000000 a83e9208 01000000>
person superclass -- 0x108923ea8
person Mateclass 0x107978618
  • 上述代码中,我们将ANPerson 实例化对象person1的内容打印了出来。person1对象的第一个值竟然和类的首地址相同。这样正附和之前介绍的objc_object结构体,objc_object第一个成员就是Class 类型的 isa指针(isa指针是指向该类的内存地址)。而第二个值从打印值来看,正是该对象的实例变量。当编译器解析对象时,就会转化为objc_object的结构体,这一下子是不是很明了了呢?

  • 对于类结构,从打印结果来看,是有2个值,第一个值和该类的元类地址一样,objc_class数据结构的第一个成员正是isa指针,是一个指向元类的指针。而另外一个值是该类的父类的首地址(也就是指向该类的父类指针)。

  • 你可能发现,打印isa指针的值(40869707 01000000)为啥和类的首地址(0x107978640)不一样?
    原因:程序在模拟器上运行的,而MAC是以低字节序存储数据的。

    内存地址从左到右按照由低到高的顺序
    低字节序:将低序字节存储在起始地址(地址低位存储值的低位,地址高位存储值的高位)
    高字节序:将高序字节存储在起始地址(地址低位存储值的高位,地址高位存储值的低位)

    person1一共占16个字节,isa占8个字节,一个字节2个十六进制,反转过来,(07978640)刚刚好.

    对于整个类的层次结构,我们会更清晰:

运行时系统库(runtime)

类相关操作函数

对象的类定义 – objc_getClass
对象的元类 – objc_getMetaClass
类的父类 – class_getSuperclass
类的名字 – class_getName
类的版本信息 – class_getVersion
以字节为单位的类尺寸 – class_getInstanceSize
类的实例变量列表 – class_copyIvarList
类的方法列表 – class_copyMethodList
类的协议列表 – class_copyProtocolList
类的属性列表 – class_copyPropertyList

使用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
26
27
28
void testAddMethod(id self , IMP _cmd)
{
NSLog(@"动态添加方法");
}
- (void)testClass
{
Class testClass = objc_allocateClassPair([NSObject class], "Test", 0);

// 指定类的指定方法
Method des = class_getInstanceMethod([NSObject class], @selector(description));
// 返回描述方法参数和返回类型的字符串
const char *types = method_getTypeEncoding(des);
/**
向具有给定名称和实现的类添加新方法,会覆盖父类的实现,但对该类以实现的方法无效,想要覆盖当前类的实现,需要使用method_setImplementation
@ param cls#> 要添加方法的类
@ param name#> 一个选择器,指定要添加的方法的名称
@ param imp#> 一个函数,它是新方法的实现。 该函数必须至少有两个参数 - self和_cmd。
@ param types#> 一组字符,用于描述方法参数的类型。
@ return YES如果成功添加方法,否则为NO
*/
BOOL isSuc = class_addMethod(testClass, @selector(testAddMethod), (IMP)testAddMethod, types);
if (isSuc) {
objc_registerClassPair(testClass);
}
id obj = [[testClass alloc] init];

((void(*)(id, SEL))objc_msgSend)(obj, NSSelectorFromString(@"testAddMethod"));
}

关于Type Encodings的介绍,可以上官网查看。

运行时对象消息的传递

篇章一我们已经认识了SELIMP,现在再来认识Method

  • Method用于表示类定义中的方法
1
2
3
4
5
6
7
typedef struct objc_method *Method; struct 

objc_method {
SEL method_name OBJC2_UNAVAILABLE; // 方法名
char *method_types OBJC2_UNAVAILABLE;
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
}

method_name 是一个类型为SEL的变量,是方法的名称。
method_types 方法参数的数据类型(具体类型可看Type Encodings)
method_imp 是一个IMP的变量,在方法调用时提供方法的地址

该结构体中包含一个SELIMP,实际上相当于在SELIMP之间作了一个映射。一点找到了SEL就找到了IMP.

为了加速消息的处理,运行时系统不仅缓存(objc_cache)使用过的selector及对应的方法的地址。而且还实现了分发表分派机制。

每个objc_class(类实例) 都有一个objc_method_list类型的二维指针。而每一个objc_method_list(分发表)的结构体都含有一个objc_method结构体类型的一维数组method_list(指向objc_method数据结构指针的数组),且只有一个元素。

objc_method_list的定义如下图:

1
2
3
4
5
6
7
8
9
10
struct objc_method_list {
struct objc_method_list *obsolete OBJC2_UNAVAILABLE;

int method_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_method method_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

如下图,运行时会先根据selector搜索类方法缓存查找方法IMP函数指针。如果没有找到,就会在其类的分发表中寻找selector。并依此,一直沿着类的继承体系到达NSObject类,直到找到为止,它就会将会这个方法(Method)存储到缓存中,方便下次使用。如果还没有找到就会走消息转发流程。

调用类方法

OC中的类也是对象,即我们常说的类对象,所以它也能接受消息。

1
[NSObject alloc]
  • runtime是如何找到并调用类方法的呢?

    正如之前介绍的,对象的isa指针变量会指向描述该对象的类,类对象的isa指针变量是指向描述类(及其类方法)的元类。基类的元类的父类指针指向该基类本身。

    runtime是通过元类来实现的,元类是一个特殊的类对象。在元类中,每个类都有一个类方法列表。

    • 当源代码向对象发送消息时,runtime会通过想要的类实例方法分发表,获得合适的实例方法实现代码并跳转执行。
    • 当源代码向类发送消息时,runtime会通过该类的元类类方法分发表,获得合适的类方法实现代码并跳转执行。

参考:

感谢您的阅读,本文由 Anrue 版权所有。如若转载,请注明出处:Anrue(https://github.com/anru1314/2018/12/01/iOS/runtime-运行时-篇章二/
runtime 运行时--篇章一
iOS使用AES+RSA加密