读 objc4 源码,深入理解 Objective-C Runtime
关于 objc4 源码的一些说明:
void _objc_init(void)
函数。
self
和
super
的本质
load
方法和
initialize
方法
1. Objective-C 对象是什么?Class 是什么?id 又是什么?
所有的类都继承 NSObject 或者 NSProxy,先来看看这两个类在各自的公开头文件中的定义:
1 |
@interface NSObject <NSObject> { |
1 |
@interface NSProxy <NSObject> { |
在 objc.h 文件中,对于 Class,id 以及 objc_object 的定义:
1 |
/// An opaque type that represents an Objective-C class. |
runtime.h 文件中对 objc_class 的定义:
1 |
struct objc_class { |
在 Objective-C 中,每一个对象是一个结构体,每个对象都有一个 isa 指针,类对象 Class 也是一个对象。所以,我们说,凡是包含 isa 指针的,都可以被认为是 Objective-C 中的对象。运行时可以通过 isa 指针,查找到该对象是属于什么类(Class)。
2. isa 是什么?为什么要有 isa?
在 Runtime 源码中,对于 objc_object 和 objc_class 的定义分别如下:
1 |
struct objc_object { |
1 |
struct objc_class : objc_object { |
objc_class 继承于 objc_object,所以 objc_class 也是一个 objc_object,objc_object 和 objc_class 都有一个成员变量 isa。isa 变量的类型是 isa_t,这个 isa_t 其实是一个联合体(union),其中包括成员量 cls。也就是说,每个 objc_object 通过自己持有的 isa,都可以查找到自己所属的类,对于 objc_class 来说,就是通过 isa 找到自己所属的元类(meta class)。
1 |
#define ISA_MASK 0x00007ffffffffff8ULL |
而在 Objective-C 中,对象的方法都是存储在类中,而不是对象中(如果每一个对象都保存了自己能执行的方法,那么对内存的占用有极大的影响)。
1 |
// Objective-C 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中 |
当一个对象的实例方法被调用时,它要通过自己持有的 isa 来查找对应的类,然后在这里的 class_data_bits_t 结构体中查找对应方法的实现(每个对象可以通过
cls->data()-> methods
来访问所属类的方法)。同时,每一个 objc_class 也有一个指向自己的父类的指针 super_class 用来查找继承的方法。
因为在 Objective-C 中,类其实也是一个对象,每个类也有一个 isa 指向自己所属的元类。所以无论是类还是对象都能通过相同的机制查找方法的实现。
isa 在方法调用时扮演的角色:
isa_t 中包含什么:
isa 的类型 isa_t 是一个 union 类型的结构体,也就是说其中的 isa_t、cls、 bits 还有结构体共用同一块地址空间,而 isa 总共会占据 64 位的内存空间(决定于其中的结构体)。其中包含的信息见上面的代码注释。
现在直接访问对象(objc_object)的 isa 已经不会返回类指针了,取而代之的是使用
ISA()
方法来获取类指针。其中 ISA_MASK 是宏定义,这里通过掩码的方式获取类指针。
结论
(1)isa 的作用:用于查找对象(或类对象)所属类(或元类)的信息,比如方法列表。
(2)isa 是什么:isa 的数据结构是一个 isa_t 联合体,其中包含其所属的 Class 的地址,通过访问对象的 isa,就可以获取到指向其所属 Class 的指针(针对 tagged pointer 的情况,也就是 non-pointer isa,有点不一样的是,除了指向 class 的指针,isa 中还会包含对象本身的一些信息,比如对象是否被弱引用)。
3. 为什么在 Objective-C 中,所以的对象都用一个指针来追踪?
内存中的数据类型分为两种:值类型和引用类型。指针就是引用类型,struct 类型就是值类型。
值类型在传值时需要拷贝内容本身,而引用类型在传递时,拷贝的是对象的地址。所以,一方面,值类型的传递占用更多的内存空间,使用引用类型更节省内存开销;另一方面,也是最主要的原因,很多时候,我们需要把一个对象交给另一个函数或者方法去修改其中的内容(比如说一个 Person 对象的 age 属性),显然如果我们想让修改方获取到这个对象,我们需要的传递的是地址,而不是复制一份。
对于像
int
这样的基本数据类型,拷贝起来更快,而且数据简单,方便修改,所以就不用指针了。
另一方面,对象的内存是分配在堆上的,而值类型是分配到栈上的。所以一般对象的生命周期会比普通的值类型要长,而且创建和销毁对象以及内存管理是要消耗性能的,所以通过指针来引用一个对象,比直接复制和创建对象要更有效率、更节省性能。
4. Objective-C 对象是如何被创建(alloc)和初始化(init)的?
整个对象的创建过程其实就做了两件事情:为对象分配内存空间,以及初始化 isa(一个联合体)。
(1)创建 NSObject 对象的过程
1 |
+ (id)alloc { |
最核心的逻辑就在
_class_createInstanceFromZone
函数中:
1 |
static id _class_createInstanceFromZone(Class cls, size_t extraBytes, void *zone, bool cxxConstruct = true, size_t *outAllocatedSize = nil) { |
获取对象内存空间大小:
1 |
size_t instanceSize(size_t extraBytes) { |
初始化 isa,isa 是一个 isa_t 联合体:
1 |
inline void objc_object::initIsa(Class cls, bool indexed, bool hasCxxDtor) { |
(2)NSObject 对象初始化的过程
NSObject 对象的初始化实际上就是返回
+alloc
执行后得到的对象本身:
1 |
- (id)init { |
5. Objective-C 对象的实例变量是什么?为什么不能给 Objective-C 对象动态添加实例变量?
->
操作符不是C语言指针操作
(2)Non Fragile ivars
在 Runtime 的现行版本中,最大的特点就是健壮的实例变量。
当一个类被 编译 时,实例变量的布局也就形成了,它表明访问类的实例变量的位置。用旧版OSX SDK 编译的 MyObject 类成员变量布局是这样的,MyObject的成员变量依次排列在基类NSObject 的成员后面。
当苹果发布新版本OSX SDK后,NSObject增加了两个成员变量。如果没有Non Fragile ivars特性,我们的代码将无法正常运行,因为MyObject类成员变量布局在编译时已经确定,有两个成员变量和基类的内存区域重叠了。此时,我们只能重新编译MyObject代码,程序才能在新版本系统上运行。
现在有了 Non Fragile ivars 之后,问题就解决了。 在程序启动后,runtime加载MyObject类的时候,通过计算基类的大小,runtime 动态调整了 MyObject 类成员变量布局,把MyObject成员变量的位置向后移动8个字节。于是我们的程序无需编译,就能在新版本系统上运行。
(3)Non Fragile ivars 是如何实现的呢?
当成员变量布局调整后,静态编译的native程序怎么能找到变量的新偏移位置呢?
根据 Runtime 源码可知,一个变量实际上就是一个 ivar_t 结构体。而每个 Objective-C 对象对应于
struct objc_object
,后者的 isa 指向类定义,即 struct objc_class:
1 |
typedef struct ivar_t *Ivar; |
沿着 objc_class 的
data()->ro->ivars
找下去,struct ivar_list_t是类所有成员变量的定义列表。通过
first
变量,可以取得类里任意一个类成员变量的定义。
1 |
struct class_rw_t { |
这里的 offset,应该就是用来记录这个成员变量在对象中的偏移位置。也就是说,runtime在发现基类大小变化时,通过修改offset,来更新子类成员变量的偏移值。
假如我们现在有一个继承于 NSError 的类 MyClass,在编译时,LLVM计算出基类 NSError 对象的大小为40字节,然后记录在MyClass的类定义中,如下是对应的C代码。在编译后的可执行程序中,写死了“40”这个魔术数字,记录了在此次编译时MyClass基类的大小。
1 |
class_ro_t class_ro_MyClass = { |
现在假如苹果发布了OSX 11 SDK,NSError类大小增加到48字节。当我们的程序启动后,runtime加载MyClass类定义的时候,发现基类的真实大小和MyClass的instanceStart不相符,得知基类的大小发生了改变。于是runtime遍历MyClass的所有成员变量定义,将offset指向的值增加8。具体的实现代码在runtime/objc-runtime-new.mm的
moveIvars()
函数中。
并且,MyClass类定义的instanceSize也要增加8。这样runtime在创建MyClass对象的时候,能分配出正确大小的内存块。
1 |
static void moveIvars(class_ro_t *ro, uint32_t superSize) |
(4)为什么Objective-C类不能动态添加成员变量
runtime 提供了一个 class_addIvar() 函数用于给类添加成员变量,但是根据文档中的注释,这个函数只能在“构建一个类的过程中”调用。一旦完成类定义,就不能再添加成员变量了。经过编译的类在程序启动后就被runtime加载,没有机会调用addIvar。程序在运行时动态构建的类需要在调用 objc_allocateClassPair 之后,调用 objc_registerClassPair 之前才可以添加成员变量。
这样做会带来严重问题,为基类动态增加成员变量会导致所有已创建出的子类实例都无法使用。那为什么runtime允许动态添加方法和属性,而不会引发问题呢?
因为方法和属性并不“属于”实例,而成员变量“属于”实例。我们所说的“类实例”概念,指的是一块内存区域,包含了isa指针和所有的成员变量。所以假如允许动态修改类成员变量布局,已经创建出的实例就不符合类定义了,变成了无效对象。但方法定义是在objc_class中管理的,不管如何增删类方法,都不影响实例的内存布局,已经创建出的实例仍然可正常使用。
美团的技术博客中给出的解释比较简单,其中“破坏破坏类的内部布局”这句话本身也是有些问题的:
extension在 编译期决议 ,它就是类的一部分,在编译期和头文件里的@interface以及实现文件里的@implement一起形成一个完整的类,它伴随类的产生而产生,亦随之一起消亡。extension一般用来隐藏类的私有信息,你必须有一个类的源码才能为一个类添加extension,所以你无法为系统的类比如NSString添加extension。
但是category则完全不一样,它是在
运行期决议的
。
就category和extension的区别来看,我们可以推导出一个明显的事实,extension可以添加实例变量,而category是无法添加实例变量的(
因为在运行期,对象的内存布局已经确定,如果添加实例变量就会破坏类的内部布局,这对编译型语言来说是灾难性的
)。
6. Objective-C 对象的属性是什么?属性跟实例变量的区别?
属性是一个结构体,其中包含属性名和属性本身的属性(attributes)。
我们一般是通过使用
@property
进行属性定义,编译时编译器会自动生成对应的实例变量(默认情况下生成的实例变量名是在对应的属性名前加了下划线“_”),同时还会自动合成对应的 setter 和 getter 方法用于存取属性值。
我们可以验证一下,先定义一个带有属性的类 NyanCat,如下:
1 |
|
然后再通过
clang -rewrite-objc NyanCat.m
将该类重写为 cpp 代码后,得到了下面这些内容:
1 |
|
从上面 clang 重写的代码中可以看到:
cost
对应的属性名和属性的 attributes(attributes 字符串所代表的含义可以在
官方文档
上查阅到)。
_method_list_t
中也有属性
cost
对应的变量信息,变量名为
_cost
,类型为
@"NSString"
。
_method_list_t
中有属性
cost
对应的 setter 和 getter 方法,这两个方法的实现分别对应的是两个函数——
_I_NyanCat_setCost_(NyanCat * self, SEL _cmd, NSString *cost)
和
_I_NyanCat_cost(NyanCat * self, SEL _cmd)
。
以上三条结果正好验证了我们一开始提出的结论。
实际上,在 runtime 源码 objc-runtime-new.h 的实现中,属性就是一个 property_t 类型的结构体,其中包含属性名以及属性自己的属性(attributes)。
1 |
typedef struct property_t *objc_property_t; |
在实际使用 runtime 时,通过下面两个函数分别可以获取 property 名字和 attributes 字符串。
1 |
// Returns the name of a property. |
7. Objective-C 对象的方法是什么?Objective-C 对象的方法在内存中的存储结构是什么样的?
1 |
// Objective-C 类是一个结构体,继承于 objc_object |
class_rw_t 中还有一个指向常量的指针 ro,其中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议。
1 |
/ Objective-C 类中的属性、方法还有遵循的协议等信息都保存在 class_rw_t 中 |
1 |
// 用于存储一个 Objective-C 类在编译期就已经确定的属性、方法以及遵循的协议 |
加载 ObjC 运行时的过程中在
realizeClass()
方法中:
1 |
... |
在上面这段代码运行之后
class_rw_t
中的方法,属性以及协议列表均为空。这时需要
realizeClass
调用
methodizeClass
方法来将类自己实现的方法(包括分类)、属性和遵循的协议加载到 methods、 properties 和 protocols 列表中。
方法的结构,与类和对象一样,方法在内存中也是一个结构体 method_t,其中包括成员变量 name(SEL 类型,实际上就是方法名)、types(一个C字符串方法类型,详见 Type Encodings )、imp(IMP 类型方法实现)。
1 |
struct method_t { |
(1) 在 runtime 初始化之后,realizeClass 之前,从 class_data_bits_t 结构体中获取的 class_rw_t 一直都不是 class_rw_t 结构体,而是class_ro_t。因为类的一些方法、属性和协议都是在编译期决定的(baseMethods 等成员以及类在内存中的位置都是编译期决定的)。
(2) 类在内存中的位置是在编译期间决定的,在之后修改代码,也不会改变内存中的位置。
类的方法、属性以及协议在编译期间存放到了“错误”的位置,直到 realizeClass 执行之后,才放到了 class_rw_t 指向的只读区域 class_ro_t,这样我们即可以在运行时为 class_rw_t 添加方法,也不会影响类的只读结构。
(3) 在 class_ro_t 中的属性在运行期间就不能改变了,再添加方法时,会修改 class_rw_t 中的 methods 列表,而不是 class_ro_t 中的 baseMethods。
(4)一个类(Class)持有一个分发表,在运行期分发消息,表中的每一个实体代表一个方法(Method),它的名字叫做选择子(SEL),对应着一种方法实现(IMP)。
8. 什么是 IMP?什么是选择器 selector ?
8.1 IMP
IMP 在 runtime 源码 objc.h 中的定义是:
1 |
/// A pointer to the function of a method implementation. |
它就是一个函数指针,这是由编译器生成的。当你发起一个 ObjC 消息之后,最终它会执行的那段代码,就是由这个函数指针指定的。而 IMP 这个函数指针就指向了这个方法的实现。既然得到了执行某个实例某个方法的入口,我们就可以绕开消息传递阶段,直接执行方法的实现,以达到更好的性能(在
Mantle
的
MTLModelAdapter.m
中可以看到这方面的应用)。
你会发现 IMP 指向的方法与 objc_msgSend 函数类型相同,参数都包含 id 和 SEL 类型。每个方法名都对应一个 SEL 类型的方法选择器,而每个实例对象中的 SEL 对应的方法实现肯定是唯一的,通过一组 id 和 SEL 参数就能确定唯一的方法实现地址;反之亦然。
8.2 选择器 selector
SEL 在 objc.h 中的定义是:
1 |
/// An opaque type that represents a method selector. |
SEL 看上去是一个指向结构体的指针,但是实际上是什么类型呢?objc.h 中提供了运行时向系统注册选择器的函数
sel_registerName()
。而在开源的
objc-sel.mm
中提供了
sel_registerName()
函数的实现,其中能找到一些蛛丝马迹:
1 |
SEL sel_registerName(const char *name) { |
从创建 SEL 的实现来看, SEL 实际上是一个
char *
类型,也就是一个字符串。
(1) 使用
@selector()
生成的选择子不会因为类的不同而改变(即使方法名字相同而变量类型不同也会导致它们具有相同的方法选择子),其内存地址在编译期间就已经确定了。也就是说向不同的类发送相同的消息时,其生成的选择子是完全相同的。
(2) 通过
@selector(方法名)
就可以返回一个选择子,通过
(void *)@selector(方法名)
, 就可以读取选择器的地址。
(3) 推断出的 selector 的特性:
@selector()
时会从这个选择子表中根据选择子的名字查找对应的 SEL。如果没有找到,则会生成一个
SEL
并添加到表中。
@selector()
生成的选择子加入到选择子表中。
9. 消息发送和消息转发
具体过程查看源码中
lookUpImpOrForward()
函数部分的注释发送 hello 消息后,编译器会将上面这行 [obj hello]; 代码转成 objc_msgSend()(注:objc_msgSend 是一个私有方法,而且是用汇编实现的,我们没有办法进入它的实现,但是我们可以通过 lookUpImpOrForward 函数断点拦截) 到当前类的缓存中去查找方法实现,如果找到了直接 done 如果没找到,就到当前类的方法列表中去查找,如果找到了直接 done 如果还没找到,就到父类的缓存中去查找方法实现,如果找到了直接 done 如果没找到,就到父类的方法列表中去查找,如果找到了直接 done 如果还没找到,就进行方法决议 最后还没找到的话,就走消息转发 从源代码看 ObjC 中消息的发送 10. Method Swizzling
- 什么是 Method Swizzling ?
- Method Swizzling 有什么注意点?
- Method Swizzling 的原理是什么?
- Method Swizzling 为什么要在 +load 方法中进行?
11. Category
- Category 是什么?
- Category 中的方法和属性以及协议是怎么存储和加载的?
- Category 和 Class 的关系
12. Associated Objects 的原理是什么?到底能不能在 Category 中给 Objective-C 类添加属性和实例变量?
- Associated Objects 的原理是什么?
- Associated Objects 的内存管理机制?
- 到底能不能在 Category 中给 Objective-C 类添加属性和实例变量?
13. Objective-C 中的 Protocol 是什么?
14.
self
和super
的本质我们可以看看 message.h 中提供的发消息给父类的函数:
1
2 OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ );当我们发送消息给 super 时,runtime 时就不是使用 objc_msgSend 方法了,而是 objc_msgSendSuper。函数的第一个参数也不再是 self 了,编译器会生成一个 objc_super 结构体。下面是 message.h 中 objc_super 结构体的定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained Class class;
#else
__unsafe_unretained Class super_class;
#endif
/* super_class is the first class to search */
};
#endifobjc_super 包含了两个变量,receiver 是消息的实际接收者,super_class 是指向当前类的父类。
通过
clang -rewrite-objc NyanCat.m
命令将下面定义的 NyanCat 类转成 cpp 代码。
1
2
3
4
5
6
7
8
9
10
11
12
13
14 #import <Foundation/Foundation.h>
@interface NyanCat : NSObject
@end
@implementation NyanCat
- (instancetype)init {
self = [super init];
return self;
}
@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 struct __rw_objc_super {
struct objc_object *object;
struct objc_object *superClass;
__rw_objc_super(struct objc_object *o, struct objc_object *s) : object(o), superClass(s) {}
};
#ifndef _REWRITER_typedef_NyanCat
#define _REWRITER_typedef_NyanCat
typedef struct objc_object NyanCat;
typedef struct {} _objc_exc_NyanCat;
#endif
struct NyanCat_IMPL {
struct NSObject_IMPL NSObject_IVARS;
};
/* @end */
// @implementation NyanCat
static instancetype _I_NyanCat_init(NyanCat * self, SEL _cmd) {
self = ((NyanCat *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("NyanCat"))}, sel_registerName("init"));
return self;
}
// ...由此可见,
[super xxxx]
在运行时确实被转成了 objc_msgSendSuper 函数。那么 objc_msgSendSuper 这个函数的内部实现是怎么样的呢?文档 objc_msgSendSuper 函数的注释中对 super 参数的注释是这样写的:
super - A pointer to an objc_super data structure. Pass values identifying the context the message was sent to, including the instance of the class that is to receive the message and the superclass at which to start searching for the method implementation.
我们可以推断出,objc_msgSendSuper 函数实现实际上就是: 从
objc_super
结构体指向的objc_super->superClass
的方法列表开始查找调用方法的 selector 对应的实现 ,找到后以objc_super->receiver
去调用这个 selector,最后就变成了调用 objc_msgSend 函数给 self 发消息的形式了。
1 objc_msgSend(objc_super->receiver, @selector(xxx));这里的 objc_super->receiver 就相当于 self,上面的操作其实就是:
1 objc_msgSend(self, @selector(xxx));
[self init]
和[super init]
的相同点在于消息接收者实际上都是 self(方法调用源头),区别就在于查找方法的实现时,前者是从 currentClass(self 所属的类)的方法列表中开始往上找,而后者是从 objc_super->spuerClass(也就是调用了 super 的地方的父类,这是在编译时就确定了的)的方法列表中开始往上查找。需要强调的地方是,
[self xxx]
要调用的实现是在运行时动态决定的,而[super xxx]
要调用的实现是编译时就确定了的(这里有个 例子 可以测试一下) 。从上面转换出来的 cpp 代码中也可以看出来,这其实是因为objc_msgSendSuper
函数的第一个参数objc_super
结构体中的receiver
是通过接收方法中的 self 参数得来的,所以动态决定的,而objc_super->superClass
是通过class_getSuperclass(objc_getClass("NyanCat"))
得到的,所以是静态的,在编译时就确定了的。
1
2
3
4
5 static instancetype _I_NyanCat_init(NyanCat * self, SEL _cmd) {
self = ((NyanCat *(*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("NyanCat"))}, sel_registerName("init"));
return self;
}- Objc Runtime
- Objective C: Difference between self and super
- What does it mean when you assign [super init] to self?
15. load 方法和 initialize 方法
+load
方法和+initialize
方法分别在什么时候被调用?- 这两个方法是用来干嘛的?
- ProtocolKit 的实现中为什么要在 main 函数执行前进行 Protocol 方法默认实现的注册?
- clang 命令的使用(比如
clang -rewrite-objc test.m
),clang -rewrite-objc
的作用是什么?clang rewrite 出来的文件跟 objc runtime 源码的实现有什么区别吗?- Understanding the Objective-C Runtime
- Objective-C Runtime - 玉令天下的博客
- Objective-C 中的类和对象 - ibireme 的博客
- Draveness 出品的 runtime 源码阅读系列文章(强烈推荐)
- 对象是如何初始化的(iOS) :介绍了 Objective-C 对象初始化的过程
- 从 NSObject 的初始化了解 isa :深入剖析了 isa 的结构和作用
- 深入解析 ObjC 中方法的结构 :介绍了在 ObjC 中是如何存储方法的
- 从源代码看 ObjC 中消息的发送 :通过逐步断点调试 objc 源码的方式,从 Objc 源代码中分析并合理地推测一些关于消息传递的过程
- 从 ObjC Runtime 源码分析一个对象创建的过程