离久的小站

简述Runtime

2018/05/13 Share

简介

Objective-C 是一门动态语言,它会将一些工作放在代码运行时才处理而并非编译时。也就是说,有很多类和成员变量在我们编译的时是不知道的,而在运行时,我们所编写的代码会转换成完整的确定的代码运行。

因此,编译器是不够的,我们还需要一个运行时系统(Runtime system)来处理编译后的代码。

Runtime 基本是用 C 和汇编写的,由此可见苹果为了动态系统的高效而做出的努力。苹果和 GNU 各自维护一个开源的 Runtime 版本,这两个版本之间都在努力保持一致。

点击这里可以下载苹果维护的开源代码。

id 和 Class 的定义

id

id 是一个参数类型,它是指向某个类的实例的指针。定义如下:

1
2
typedef struct objc_object *id;
struct objc_object { Class isa; };

以上定义,看到 objc_object 结构体包含一个 isa 指针,根据 isa 指针就可以找到对象所属的类。

注意:isa 指针在代码运行时并不总指向实例对象所属的类型,所以不能依靠它来确定类型,要想确定类型还是需要用对象的 -class 方法。

Class

typedef struct objc_class *Class;
Class 其实是指向 objc_class 结构体的指针。objc_class 的数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;

从 objc_class 可以看到,一个运行时类中关联了它的父类指针、类名、成员变量、方法、缓存以及附属的协议。

其中 objc_ivar_list 和 objc_method_list 分别是成员变量列表和方法列表:

成员变量列表

1
2
3
4
5
6
7
8
struct objc_ivar_list {
int ivar_count OBJC2_UNAVAILABLE;
#ifdef __LP64__
int space OBJC2_UNAVAILABLE;
#endif
/* variable length structure */
struct objc_ivar ivar_list[1] OBJC2_UNAVAILABLE;
} OBJC2_UNAVAILABLE;

方法列表

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;
}

由此可见,我们可以动态修改 *methodList 的值来添加成员方法,这也是 Category 实现的原理,同样解释了 Category 不能添加属性的原因。

objc_ivar_list 结构体用来存储成员变量的列表,而 objc_ivar 则是存储了单个成员变量的信息;同理,objc_method_list 结构体存储着方法数组的列表,而单个方法的信息则由 objc_method 结构体存储。

值得注意的时,objc_class 中也有一个 isa 指针,这说明 Objc 类本身也是一个对象。为了处理类和对象的关系,Runtime 库创建了一种叫做 Meta Class(元类) 的东西,类对象所属的类就叫做元类。Meta Class 表述了类对象本身所具备的元数据。

我们所熟悉的类方法,就源自于 Meta Class。我们可以理解为类方法就是类对象的实例方法。每个类仅有一个类对象,而每个类对象仅有一个与之相关的元类。

当你发出一个类似 NSObject alloc 的消息时,实际上,这个消息被发送给了一个类对象(Class Object),这个类对象必须是一个元类的实例,而这个元类同时也是一个根元类(Root Meta Class)的实例。所有元类的 isa 指针最终都指向根元类。

所以当 [NSObject alloc] 这条消息发送给类对象的时候,运行时代码 objc_msgSend() 会去它元类中查找能够响应消息的方法实现,如果找到了,就会对这个类对象执行方法调用。

上图实现是 super_class 指针,虚线时 isa 指针。而根元类的父类是 NSObject,isa指向了自己。而 NSObject 没有父类。

最后 objc_class 中还有一个 objc_cache ,缓存,它的作用很重要,后面会提到。

Method

讲解 Method(方法)之前,我们先讲解一下 SEL 。

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;
}

objc_method 存储了方法名,方法类型和方法实现:

方法名类型为 SEL
方法类型 method_types 是个 char 指针,存储方法的参数类型和返回值类型
method_imp 指向了方法的实现,本质是一个函数指针

SEL

它是selector在 Objc 中的表示(Swift 中是 Selector 类)。selector 是方法选择器,其实作用就和名字一样,日常生活中,我们通过人名辨别谁是谁,注意 Objc 在相同的类中不会有命名相同的两个方法。selector 对方法名进行包装,以便找到对应的方法实现。它的数据结构是:

1
typedef struct objc_selector *SEL;

我们可以看出它是个映射到方法的 C 字符串,你可以通过 Objc 编译器器命令@selector() 或者 Runtime 系统的 sel_registerName 函数来获取一个 SEL 类型的方法选择器。

注意:不同类中相同名字的方法所对应的 selector 是相同的,由于变量的类型不同,所以不会导致它们调用方法实现混乱。

Method 代表类中某个方法的类型:

IMP

IMP在objc.h中的定义是:

1
typedef id (*IMP)(id, SEL, ...);

它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。

如果得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法,这在后面 Cache 中会提到。

你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 id 和 SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例对象中的 SEL 对应的方法实现肯定是唯一的,通过一组 id和 SEL 参数就能确定唯一的方法实现地址。

而一个确定的方法也只有唯一的一组 id 和 SEL 参数。

Ivar

Ivar 是表示成员变量的类型。

1
2
3
4
5
6
7
8
9
10
typedef struct objc_ivar *Ivar;

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
}

其中 ivar_offset 是基地址偏移字节。

##Cache

Cache 定义如下:

1
2
3
4
5
6
7
typedef struct objc_cache *Cache

struct objc_cache {
unsigned int mask /* total = mask + 1 */ OBJC2_UNAVAILABLE;
unsigned int occupied OBJC2_UNAVAILABLE;
Method buckets[1] OBJC2_UNAVAILABLE;
};

Cache 为方法调用的性能进行优化,每当实例对象接收到一个消息时,它不会直接在 isa 指针指向的类的方法列表中遍历查找能够响应的方法,因为每次都要查找效率太低了,而是优先在 Cache 中查找。

Runtime 系统会把被调用的方法存到 Cache 中,如果一个方法被调用,那么它有可能今后还会被调用,下次查找的时候就会效率更高。就像计算机组成原理中 CPU 绕过主存先访问 Cache 一样。

Property

1
2
3
4
5
6
typedef struct objc_property *objc_property_t;

typedef struct {
const char *name;
const char *value;
} objc_property_attribute_t;

对应的方法接口:

1
2
3
4
5
6
7
8
9
10
11
12
// 获取指定的属性
objc_property_t class_getProperty ( Class cls, const char *name );
// 获取属性列表
objc_property_t * class_copyPropertyList ( Class cls, unsigned int *outCount );
//获取属性名称
const char * property_getName(objc_property_t _Nonnull property)
//获取属性特性
const char *property_getAttributes(objc_property_t _Nonnull property)
//获取属性特性列表
objc_property_attribute_t *property_copyAttributeList(objc_property_t property,unsigned int * outCount)
//获取根据特定键获取想得到的属性特性
char *property_copyAttributeValue(objc_property_t property, const char * attributeName)

注意:返回的是属性列表,列表中每个元素都是一个 objc_property_t 指针。

关于 objc_property_attribute_t ,它记录着对应属性的类型。

举个例子,我们声明三个属性。

1
2
3
4
5
@property (strong, nonatomic) NSString *name;

@property (assign, nonatomic) int age;

@property (assign, nonatomic) double weight;

我们通过接口获取属性的名字、类型

1
2
3
4
5
6
7
8
9
unsigned int outCount = 0;

objc_property_t *properties = class_copyPropertyList([Person class], &outCount);

for (NSInteger i = 0; i < outCount; i++) {
NSString *name = [[NSString alloc] initWithUTF8String:property_getName(properties[i])];
NSString *attributes = [[NSString alloc] initWithUTF8String:property_getAttributes(properties[i])];
NSLog(@"%@--------%@", name, attributes);
}

打印结果为

1
2
3
name--------T@"NSString",&,N,V_name
age--------Ti,N,V_age
weight--------Td,N,V_weight

objc_property_attribute_t 里面记录着属性的类型。详情看 官方文档

消息

当我们调用调用方法时:

1
[object message];

底层运行时会被编译器转化为:

1
objc_msgSend(object, selector)

如果其还有参数比如:

1
[object message:(id)arg...];

底层运行时会被编译器转化为:

1
objc_msgSend(object, selector, arg1, arg2, ...)

流程

objc_msgSend 方法看清来好像返回了数据,其实objc_msgSend 从不返回数据,而是你的方法在运行时实现被调用后才会返回数据。下面详细叙述消息发送的步骤(如下图):

  1. 首先检测这个 selector 是不是要忽略。比如 Mac OS X 开发,有了垃圾回收就不理会 retain,release 这些函数。
  2. 检测这个 selector 的 target 是不是 nil,Objc 允许我们对一个 nil 对象执行任何方法不会 Crash,因为运行时会被忽略掉。
  3. 如果上面两步都通过了,那么就开始查找这个类的实现 IMP,先从 cache 里查找,如果找到了就运行对应的函数去执行相应的代码。
  4. 如果 cache 找不到就找类的方法列表中是否有对应的方法。
  5. 如果类的方法列表中找不到就到父类的方法列表中查找,一直找到 NSObject 类为止。
  6. 如果还找不到,就要开始进入动态方法解析了,后面会提到。

在消息的传递中,编译器会根据情况在 objc_msgSend , objc_msgSend_stret , objc_msgSendSuper , objc_msgSendSuper_stret 这四个方法中选择一个调用。如果消息是传递给父类,那么会调用名字带有 Super 的函数,如果消息返回值是数据结构而不是简单值时,会调用名字带有 stret 的函数。

参数

self 是在方法运行时被动态传入的。

当 objc_msgSend 找到方法对应实现时,它将直接调用该方法实现,并将消息中所有参数都传递给方法实现,同时,它还将传递两个隐藏参数:

接受消息的对象(self 所指向的内容,当前方法的对象指针)
方法选择器(_cmd 指向的内容,当前方法的 SEL 指针)
因为在源代码方法的定义中,我们并没有发现这两个参数的声明。它们时在代码被编译时被插入方法实现中的。尽管这些参数没有被明确声明,在源代码中我们仍然可以引用它们。

这两个参数中, self更实用。它是在方法实现中访问消息接收者对象的实例变量的途径。

这时我们可能会想到另一个关键字 super ,实际上 super 关键字接收到消息时,编译器会创建一个 objc_super 结构体:

1
struct objc_super { id receiver; Class class; };

这个结构体指明了消息应该被传递给特定的父类。 receiver 仍然是 self 本身,当我们想通过 [super class] 获取父类时,编译器其实是将指向 self 的 id 指针和 class 的 SEL 传递给了 objc_msgSendSuper 函数。只有在 NSObject 类中才能找到 class 方法,然后 class 方法底层被转换为 object_getClass(), 接着底层编译器将代码转换为 objc_msgSend(objc_super->receiver, @selector(class)),传入的第一个参数是指向 self 的 id 指针,与调用 [self class] 相同,所以我们得到的永远都是 self 的类型。因此你会发现:

1
2
// 这句话并不能获取父类的类型,只能获取当前类的类型名
NSLog(@"%@", NSStringFromClass([super class]));

获取方法地址

NSObject 类中有一个实例方法:methodForSelector,你可以用它来获取某个方法选择器对应的 IMP ,举个例子:

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);

当方法被当做函数调用时,两个隐藏参数也必须明确给出,上面的例子调用了1000次函数,你也可以尝试给 target 发送1000次 setFilled: 消息会花多久。

虽然可以更高效的调用方法,但是这种做法很少用,除非时需要持续大量重复调用某个方法的情况,才会选择使用以免消息发送泛滥。

注意:
methodForSelector:方法是由 Runtime 系统提供的,而不是 Objc 自身的特性。

动态方法解析

我们可以通过分别重载 resolveInstanceMethod: 和 resolveClassMethod: 方法添加实例方法实现和类方法实现。

当 Runtime 系统在 Cache 和类的方法列表(包括父类)中找不到要执行的方法时,Runtime 会调用 resolveInstanceMethod: 或 resolveClassMethod: 来给我们一次动态添加方法实现的机会。我们需要用 class_addMethod 函数完成向特定类添加特定方法实现的操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
void dynamicMethodIMP(id self, SEL _cmd) {
// implementation ....
}
@implementation MyClass
+ (BOOL)resolveInstanceMethod:(SEL)aSEL
{
if (aSEL == @selector(resolveThisMethodDynamically)) {
class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:aSEL];
}
@end

上面的例子为 resolveThisMethodDynamically 方法添加了实现内容,就是 dynamicMethodIMP 方法中的代码。其中 “v@:” 表示返回值和参数。这个符号的含义和 objc_property_attribute_t 记录的属性类型一样。详情看 官方文档

注意:
动态方法解析会在消息转发机制侵入前执行,动态方法解析器将会首先给予提供该方法选择器对应的 IMP 的机会。如果你想让该方法选择器被传送到转发机制,就让 resolveInstanceMethod: 方法返回 NO。

消息转发

消息转发的流程:

重定向

消息转发机制执行前,Runtime 系统允许我们替换消息的接收者为其他对象。通过 forwardingTargetForSelector 方法。

1
2
3
4
5
6
7
- (id)forwardingTargetForSelector:(SEL)aSelector
{
if(aSelector == @selector(mysteriousMethod:)){
return alternateObject;
}
return [super forwardingTargetForSelector:aSelector];
}

如果此方法返回 nil 或者 self,则会计入消息转发机制(forwardInvocation:),否则将向返回的对象重新发送消息。

转发

当动态方法解析不做处理返回 NO 时,则会触发消息转发机制。这时 methodSignatureForSelector: 和 forwardInvocation: 方法会被执行,我们可以重写这个方法来自定义我们的转发逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (NSMethodSignature*)methodSignatureForSelector:(SEL)selector
{
return [super methodSignatureForSelector:selector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation
{
if ([anOtherObject respondsToSelector:
[anInvocation selector]])
[anInvocation invokeWithTarget:anOtherObject];
else
[super forwardInvocation:anInvocation];
}

forwardInvocation: 唯一参数是个 NSInvocation 类型的对象,该对象封装了原始的消息和消息的参数。我们可以实现 forwardInvocation: 方法来对不能处理的消息做一些处理。也可以将消息转发给其他对象处理,而不抛出错误。

注意:
在 forwardInvocation: 消息发送前,Runtime 系统会向对象发送 methodSignatureForSelector: 消息,并取到返回的方法签名用于生成 NSInvocation 对象。所以重写 forwardInvocation: 的同时也要重写 methodSignatureForSelector: 方法,否则会抛异常。

通过实现自己的 forwardInvocation: 方法,我们可以将消息转发给其他对象。

forwardInvocation: 方法就是一个不能识别消息的分发中心,将这些不能识别的消息转发给不同的接收对象,或者转发给同一个对象,再或者将消息翻译成另外的消息,亦或者简单的“吃掉”某些消息,因此没有响应也不会报错。这一切都取决于方法的具体实现。

注意:
forwardInvocation:方法只有在消息接收对象中无法正常响应消息时才会被调用。所以,如果我们向往一个对象将一个消息转发给其他对象时,要确保这个对象不能有该消息的所对应的方法。否则,forwardInvocation:将不可能被调用。

CATALOG
  1. 1. 简介
  2. 2. id 和 Class 的定义
    1. 2.1. id
    2. 2.2. Class
  3. 3. Method
    1. 3.1. SEL
    2. 3.2. IMP
  4. 4. Ivar
  5. 5. Property
  6. 6. 消息
    1. 6.1. 流程
    2. 6.2. 参数
    3. 6.3. 获取方法地址
    4. 6.4. 动态方法解析
  7. 7. 消息转发
    1. 7.1. 重定向
    2. 7.2. 转发