初识 Runloop

神经病院 Objective-C Runtime 入院第一天—— isa 和 Class 阅读笔记

前言

Runtime 是一套底层的 C 语言 API,是 iOS 系统的核心之一。开发者在编码过程中,可以给任意一个对象发送消息,在编译阶段只是确定了要向接收者发送这条消息,而接受者将要如何响应和处理这条消息,那就要看运行时来决定了。

C语言中,在编译期,函数的调用就会决定调用哪个函数。而OC的函数,属于动态调用过程,在编译期并不能决定真正调用哪个函数,只有在真正运行时才会根据函数的名称找到对应的函数来调用。

Objective-C 是一个动态语言,这意味着它不仅需要一个编译器,也需要一个运行时系统来动态得创建类和对象、进行消息传递和转发。

使用Runtime

Objc 在三种层面上与 Runtime 系统进行交互:

1. 通过 Objective-C 源代码

一般情况开发者只需要编写 OC 代码即可,Runtime 系统自动在幕后把我们写的源代码在编译阶段转换成运行时代码,在运行时确定对应的数据结构和调用具体哪个方法。

2. 通过 Foundation 框架的 NSObject 类定义的方法

Foundation框架下,NSObjectNSProxy两个基类定义了类层次结构中该类下方所有类的公共接口和行为。
在iOS 11.2 的 objc/NSObject.h 中,与Runtime相关的方法有5个:

1
2
3
4
5
6
@protocol NSObject
- (Class)class OBJC_SWIFT_UNAVAILABLE("use 'type(of: anObject)' instead");
- (BOOL)isKindOfClass:(Class)aClass; //检查对象是否存在于类的继承体系中
- (BOOL)isMemberOfClass:(Class)aClass; //检查对象是否为类的成员
- (BOOL)conformsToProtocol:(Protocol *)aProtocol; //检查对象能否响应指定的消息
- (BOOL)respondsToSelector:(SEL)aSelector; //检查对象是否实现了指定协议类的方法

在NSObject.h中还有一个方法会返回指定方法实现的地址IMP

1
2
@interface NSObject <NSObject>
- (IMP)methodForSelector:(SEL)aSelector;

3. 通过对 Runtime 库函数的直接调用

objc_class

本文所有源码来自 objc4-680

在objc.h中定义了Class

1
2
/// An opaque type that represents an Objective-C class.
typedef struct objc_class *Class;

objc_class定义在runtime.h中,Objc 2.0之前 objc_class 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;

#if !__OBJC2__
Class _Nullable 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;

methodLists是指向方法列表的指针。这里可以动态修改methodLists的值来添加成员方法,这也是Category实现的原理,同样解释了Category不能添加属性的原因。

Objc 2.0之后 objc_class 定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
struct objc_object {  
private:
isa_t isa;
};

struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags

...
};

其中 isa 是一个联合体,定义如下:

1
2
3
4
5
6
7
union isa_t  
{
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
Class cls;
uintptr_t bits;
}

cache_t 的作用主要是为了优化方法调用的性能。其结构如下:

1
2
3
4
5
struct cache_t {  
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}

NSObject中,isa 是一个 objc_class 结构体,id 是一个 objc_object 结构:

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct objc_class *Class;  
typedef struct objc_object *id;

@interface Object {
Class isa;
}
@end

@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
@end

用一张图来更加详细看到 objc_class 的结构:

objc_class继承于objc_object,在objc_class中也包含isa_t类型的结构体isa。从代码中可以看出来:Objective-C 中类也是一个对象

IMP是一个函数指针,指向了一个方法的具体实现。

现在,我们再仔细看一下 objc_class 的结构,除了 isa 之外,还有3个成员变量,一个是父类的指针 superclass ,一个是方法缓存 cache_t ,最后一个这个类的实例方法链表 class_data_bits_t

1
2
3
4
5
6
struct objc_class : objc_object {
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
...
};

superclass 和 isa

当一个对象的实例方法被调用的时候,会通过 isa 找到相应的类,然后在该类的 class_data_bits_t 中去查找方法。 class_data_bits_t 是指向了类对象的数据区域。

为了和对象查找方法的机制一致,引入了元类(meta-class)的概念在类对象中查找。

  • 对象的实例方法调用时,通过对象的 isa 在中获取方法的实现。
  • 类对象的类方法调用时,通过的 isa 在元类中获取方法的实现。

对象,类,元类之间的关系可以表示为:

  1. Root class (class)其实就是NSObjectNSObject是没有超类的,所以Root class(class)superclass指向nil
  2. 每个Class都有一个isa指针指向唯一的Meta class
  3. Root class(meta)superclass指向Root class(class),也就是NSObject,形成一个回路。
  4. 每个Meta classisa指针都指向Root class (meta)

类对象和元类对象是唯一的,而对象是可以在运行时创建无数个的。在main方法执行之前,从 dyldruntime 这期间,类对象和元类对象在这期间被创建。

cache_t

Cache的作用主要是为了优化方法调用的性能。使用Cache来缓存经常调用的方法,当调用方法时,优先在Cache查找,如果没有找到,再到methodLists查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct cache_t {
struct bucket_t *_buckets;
mask_t _mask;
mask_t _occupied;
}

typedef unsigned int uint32_t;
typedef uint32_t mask_t; // x86_64 & arm64 asm are less efficient with 16-bits

typedef unsigned long uintptr_t;
typedef uintptr_t cache_key_t;

struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
}

cache_t 中存储了一个 bucket_t 的结构体,和两个 unsigned int 的变量。

  • mask:分配用来缓存bucket的总数。
  • occupied:表明目前实际占用的缓存bucket的个数。

bucket_t 的结构体中存储了一个 unsigned long 和一个 IMPIMP是一个函数指针,指向了一个方法的具体实现。

cache_t 中的 bucket_t *_buckets 其实就是一个散列表,用来存储Method的链表。

class_data_bits_t

objc_class 结构体中的注释写到 class_data_bits_t 相当于 class_rw_t 指针加上 rr/alloc 的标志。

Objc的类的属性、方法、以及遵循的协议在obj 2.0的版本之后都放在class_rw_t中。class_ro_t是一个指向常量的指针,存储来编译器决定了的属性、方法和遵守协议。

在编译期类的结构中的 class_data_bits_t *data指向的是一个 class_ro_t *指针:

在运行时调用 realizeClass 方法,会做以下3件事情:

  1. class_data_bits_t 调用 data 方法,将结果从 class_rw_t 强制转换为 class_ro_t 指针
  2. 初始化一个 class_rw_t 结构体
  3. 设置结构体 ro 的值以及 flag

最后调用methodizeClass方法,把类里面的属性,协议,方法都加载进来。

1
2
3
4
5
struct method_t {
SEL name;
const char *types;
IMP imp;
}

方法method的定义如上,里面包含3个成员变量。SEL是方法的名字name。types是Type Encoding类型编码,IMP是一个函数指针,指向的是函数的具体实现。在runtime中消息传递和转发的目的就是为了找到IMP,并执行函数。

总结

最后用一张图来总结整个运行时的过程: