引言
同事突然指着Runtime(objc4-723)的源码objc-private.h
中下面这段,问我:“objc_object
结构体中只有一个isa
变量,那对象中的实例变量去哪了?”
1 | struct objc_object { |
一下子有点儿懵逼,印象中的对象的内存布局总是如下图所示:
声明一个RoundedRectangle
类的实例对象变量,该变量指向对象中继承自NSObject
类的isa
实例变量,isa
后紧跟着继承自其父类Shape
中的fillColor
和bounds
实例变量,之后是它自己的radius
实例变量。
那么,这到底是怎么回事呢?
对象
我们在最初学习 Objective-C 时知道,所有的对象都包含一个叫isa
的变量,该变量是一个指向对象所属类的指针,其定义如下所示:
1 | typedef struct objc_class *Class; |
id
是一种特殊的类型,它能指代任意的 Objective-C 对象类型。对象是由objc_object
结构体来定义的。其中包含一个Class
类型的变量isa
,Class
是一个指向objc_class
结构体的指针,在这个结构体中存放着类的“元数据”。
Tagged Pointer
然而在处理器从32位迁移到64位后,对象指针扩大为64位的整数,为了使地址内存对齐,一些位将永远是零。出于节省内存和提高运行效率的目的,苹果爸爸在WWDC 2013 Session 404中提出了 Tagged Pointer 的概念,因此也修改了objc_object
的定义。
当对象指针的最低有效位(LSB)为 1 时,则该指针为 Tagged Pointer。其实这个指针的值本质上已经不是指向对象的地址了,而是保存着对象数据的值类型的变量。所以它的内存并不存储在堆中,也不需要 malloc/free。
我们再看一下objc_object
的定义的变化:
1 | struct objc_object { |
isa
变量的类型由Class
类型变为了isa_t
的类型,这个类型是一个联合(union
),在联合中,几个字段共用同一块内存,其长度为联合中最大字段的长度。在initIsa
方法中,当不启用 Tagged Pointer 时,就直接使用类的指针为isa.cls
赋值,它还是表示对象所属类的指针。当启用时,使用宏ISA_MAGIC_VALUE
来初始化isa.bits
,这样已经为结构体内的magic
、nonpointer
字段赋值了,然后根据函数参数设置has_cxx_dtor
,将类的指针右移 3 位以消除用于内存对齐所补的 0,使用非 0 位来为shiftcls
字段赋值。
类与元类
无论是否启用 Tagged Pointer,isa
变量中都保存了对象所属类的信息。接下来我们看一下定义类的结构体objc_class
:
1 | struct objc_class : objc_object { |
objc_class
是继承自objc_object
的,因此类本质上也是对象,称为类对象。类对象中包含继承来的isa
变量,与实例对象中isa
变量“指向”对象的所属类类似(这里的指向指的是变量中保存了对象所属类的信息,可以通过此信息找到类,下文中均如此表述),类对象的isa
指向该类的元类(metaclass)。这一点可以从创建类与元类的函数objc_allocateClassPair
中看出:
动态创建类与元类
1 | Class objc_allocateClassPair(Class superclass, const char *name, |
由cls->initClassIsa(meta);
可以看出类对象cls
确实使用元类对象meta
初始化了它的isa
变量。接下来的代码在该类有没有父类的情况下分别建立起了类与元类与他们各自父类与元类的联系。superclass
指针确立了继承关系,isa
描述了实例所属的类,通过这些联系建立起来了”类的继承体系”,如下图所示,通过这张布局关系图,我们可以查出对象能否响应某个选择子,是否遵从某项协议等,并且能够通过isMemberOfClass
和isKindOfClass
等类型信息查询方法来检视类的继承体系。
需要注意以下两点:
- 所有的 metaclass 的
isa
都指向根类的元类,包括根元类!这样就形成了一个闭环。当向对象(类对象/元类对象)发送消息时,runtime 会在对象所属的类的方法列表里面查找消息对应的方法,这样的闭环会保证这一步执行正确。 - 元类的父类指向类的父类的元类(绕口令啊(ノ ゚Д゚)ノ ┻━━┻),例外的是根类,根类的
superclass
为 nil,根类的元类的superclass
指向根类。当在对象所属类的方法列表中没有找到对应的方法时,runtime 会去类的父类中查找,如果找到了就跳转到方法的实现代码中,如果一路向上找到根类也没有找到时,runtime 的”消息传递机制”就结束了。接下来会启动“消息转发机制”,详情可查看这里:【翻译】Objective-C Runtime Programming Guide
我们说类对象结构体objc_class
中存放着类的“元数据”,例如类的实例实现了哪些方法,具备多少个实例变量及其布局等信息,而元类中保存了类对象本身所具备的元数据,“类方法”就定义在这里,因为这些方法可以理解成类对象的实例方法。objc_class
结构体中的isa
和superclass
变量构建起了类的继承体系,接下来我们看看剩下的部分。
cache_t
objc_class
结构体中的cache
字段主要用于缓存调用过的方法。当对象接收到消息时,runtime 根据isa
去对象所属类的方法列表中查找,如果每次都经过这样的流程,方法调用的效率会比较差,因此在第一次调用过一个方法后,这个方法就会被缓存在cache
中,下次接收到消息时会先在这里查找,没有才去方法列表中查找。
1 | struct cache_t { |
class_data_bits_t
objc_class
结构体中的bits
字段的注释为:”class_rw_t * plus custom rr/alloc flags”.
1 |
|
可以看出这个结构体中只有一个 64 位的值bits
,该数据中保存了一个指向class_rw_t
结构体的指针和是否为 swift 类、是否有默认的retain/release
等方法及是否要求 raw isa 三个标志位。结构体中还提供了使用掩码来访问这些数据的方法(甚至还提供一些对class_rw_t
结构体中flags
字段的访问函数)。引用深入解析 ObjC 中方法的结构文章中的配图可以看得更明白一些:
class_rw_t
class_data_bits_t
结构体中的data()
方法返回了指向class_rw_t
结构体的指针,这个结构体中保存了类的方法、属性和协议等信息。先看一下它的定义:
1 | struct class_rw_t { |
class_rw_t
中还包含一个与它类似的结构体class_ro_t
的常量指针。从命名我们可以猜出class_ro_t
中存储的是只读的信息,而class_rw_t
中存储了可读写的信息。实际上也是如此,在class_ro_t
中存储了当前类在编译期就已经确定的属性、方法以及遵循的协议等信息,class_rw_t
存储的内容是可以动态修改的,所以运行时对类的扩展大都存储在这里。先看下class_ro_t
的定义:
class_ro_t
1 | struct class_ro_t { |
在上面的“创建新的类与元类”部分,我们看到在初始化类与元类的函数objc_initializeClassPair_internal
中,开辟了class_rw_t
和class_ro_t
的存储空间,并将返回的指针保存在相应的数据中。接着对class_rw_t
中的flags
,version
,ro
字段进行了初始化,在addSubclass(superclass, cls)
方法中又设置了自身的nextSiblingClass
以及父类的firstSubclass
。同时初始化了class_ro_t
中的flags
,instanceStart
,instanceSize
,ivarLayout
,weakIvarLayout
,name
等字段(注意当存在父类时,本类的instanceStart
与instanceSize
都使用父类的instanceSize
进行了初始化)。
我们上面不是说过class_ro_t
结构体中的值是只读的嘛?为什么在这里对其中的变量进行了设置?而且如果你阅读过Objective-C Runtime函数列表,就会发现class_addIvar
这个函数也是在运行时通过修改类的class_ro_t
中的字段,向类中添加了实例变量。
这是因为这些操作都是发生在动态构建类的过程中!class_addIvar
的文档中有这么一段话:
This function may only be called after objc_allocateClassPair and before objc_registerClassPair. Adding an instance variable to an existing class is not supported.
也就是说一旦完成类的构建过程,就不能再修改class_ro_t
中的内容了,当然也就不能向其中添加实例变量了。objc_registerClassPair
中做的事情就是将类的class_rw_t
中的flags
标记的RW_CONSTRUCTING | RW_REALIZING
标志位清除,同时设置RW_CONSTRUCTED
这个表示类已经构建完成的标志位。
类的加载过程
在上面一部分,我们通过阅读创建类与元类的函数objc_allocateClassPair
对动态创建类的过程有了一定的理解,并且认识了表示类的结构体及其初始化的过程。但是对于我们在源代码中编写的类,它的加载过程是怎样的呢?
我们编写的源代码在编译结束后链接的过程中就静态链接进程序的二进制文件中了,而程序中引入的系统库如”Foundation.framework”和包含 Runtime 的”libobjc.A.dylib”等则是通过苹果的动态链接器-dyld(the dynamic link editor)在程序启动时动态加载的。我们看一下 Runtime 的初始化入口方法_objc_init
:
1 | void _objc_init(void) |
在这个函数的最后注册了 dyld 相关事件的回调函数,当 ImageLoader 读取了 images(可执行文件或动态库等)并将其加载进内存后,dyld 会调用回调函数map_images
对其进行解析和处理。接下来当需要对该 image 进行初始化时,dyld 就会调用回调函数load_images
对其初始化。
map_images
map_images
中提取了 images 中 ObjC 相关的元数据(Class、Selector、Protocol等符号)进行了初始化,主要过程发生在_read_images
函数中:
针对包含有 swift 旧版本代码和对 sdk 版本在 OS X 10.11之前的进行
disableTaggedPointers
操作。初始化一个全局的映射表
gdb_objc_realized_classes
用来存储没有在 dyld 共享缓存中优化过的 Classes,注意这个名字中的 realized 属于误用,其中存放的类也可以是未实现的状态。readClass
: 将类与元类照编译器编译好的方式从二进制中读出来,然后将不重复的非元类插入到gdb_objc_realized_classes
中,该函数的返回值有3中情况:- cls:即类的指针,注意如果该类之前被 alloc 为未来实现的类则需要拷贝一个新类并将
rw
的类型class_rw_t
强制转换为class_ro_t
为rw->ro
赋值,设置新的rw
并将其添加进remappedClasses
,注意这里的newCls
就已经是realized
的状态了。代码如下:
1
2
3
4
5
6
7
8
9
10
11
12class_rw_t *rw = newCls->data();
const class_ro_t *old_ro = rw->ro;
memcpy(newCls, cls, sizeof(objc_class));
rw->ro = (class_ro_t *)newCls->data();
newCls->setData(rw);
freeIfMutable((char *)old_ro->name);
free((void *)old_ro);
addRemappedClass(cls, newCls);
replacing = cls;
cls = newCls;- nil:当类没有父类或者当前父类是弱连接的,将这个类添加进需要重新映射的表后返回 nil.
- something else: 指向保留的用于为未来实现的类开辟的空间.
- cls:即类的指针,注意如果该类之前被 alloc 为未来实现的类则需要拷贝一个新类并将
修正上一步中添加进
remappedClasses
的类,用已经实现的新类替换旧类的引用。修正 selector 的引用。
读取 protocals 以及修正其引用。
对于实现了
+load
方法或者有静态实例的 non-lazy classes 执行realizeClass
来实现它(注意对于 lazy classes 则是在其首次接收到消息时才实现它),返回类真实的数据结构使其处于可用的状态,关键代码如下所示: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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61//将 ro 替换为 rw,标记类为 已实现|实现中
ro = (const class_ro_t *)cls->data();
if (ro->flags & RO_FUTURE) {
// This was a future class. rw data is already allocated.
rw = cls->data();
ro = cls->data()->ro;
cls->changeInfo(RW_REALIZED|RW_REALIZING, RW_FUTURE);
} else {
// Normal class. Allocate writeable class data.
rw = (class_rw_t *)calloc(sizeof(class_rw_t), 1);
rw->ro = ro;
rw->flags = RW_REALIZED|RW_REALIZING;
cls->setData(rw);
}
//查询是否为元类
isMeta = ro->flags & RO_META;
//设置version
rw->version = isMeta ? 7 : 0; // old runtime went up to 6
// Choose an index for this class.
// Sets cls->instancesRequireRawIsa if indexes no more indexes are available
cls->chooseClassArrayIndex(); //仅在watchABI下有效
// Realize superclass and metaclass, if they aren't already.
// This needs to be done after RW_REALIZED is set above, for root classes.
// This needs to be done after class index is chosen, for root metaclasses.
supercls = realizeClass(remapClass(cls->superclass));
metacls = realizeClass(remapClass(cls->ISA()));
//...省略SUPPORT_NONPOINTER_ISA时的代码
// Update superclass and metaclass in case of remapping
cls->superclass = supercls;
cls->initClassIsa(metacls);
// Reconcile instance variable offsets / layout.
// This may reallocate class_ro_t, updating our ro variable.
if (supercls && !isMeta) reconcileInstanceVariables(cls, supercls, ro);
// Set fastInstanceSize if it wasn't set already.
cls->setInstanceSize(ro->instanceSize);
// Copy some flags from ro to rw
if (ro->flags & RO_HAS_CXX_STRUCTORS) {
cls->setHasCxxDtor();
if (! (ro->flags & RO_HAS_CXX_DTOR_ONLY)) {
cls->setHasCxxCtor();
}
}
// Connect this class to its superclass's subclass lists
if (supercls) {
addSubclass(supercls, cls);
} else {
addRootClass(cls);
}
// Attach categories
methodizeClass(cls);
return cls;在上面的
reconcileInstanceVariables
过程中,在ro->instanceStart < super_ro->instanceSize
情况下即父类扩展了它的实例变量使得实例变量大小增大时,校准了ro->instanceSize
,ro->instanceStart
和ivars
容器中每个ivar->offset
指向的值以实现non-fragile instance variables
的功能!在
methodizeClass
过程中将ro->baseMethods()
,ro->baseProperties
,ro->baseProtocols
分别添加进rw->methods
,rw->properties
和rw->protocols
容器列表中,然后将通过unattachedCategoriesForClass
获取的未附加的分类附加在类上。读取 Category ,然后通过
addUnattachedCategoryForClass
将分类注册到它的所属类上,之后调用remethodizeClass
将实例方法、协议和属性添加到类上,将类方法添加到元类上。需要注意的是:1,Category 的方法没有“完全替换掉”原来类已经有的方法,也就是说如果 Category 和原来类都有methodA,那么 Category 附加完成之后,类的方法列表里会有两个methodA;2,Category的方法被放到了新方法列表的前面,而原来类的方法被放到了新方法列表的后面,这也就是我们平常所说的category的方法会“覆盖”掉原来类的同名方法,这是因为运行时在查找方法的时候是顺着方法列表的顺序查找的,它只要一找到对应名字的方法,就会返回。
load_images
load_images
方法就是调用+load
方法,看一下prepare_load_methods
方法:
1 | classref_t *classlist = |
这个函数基本就是准备好实现了+load
方法的类和分类,将其分别添加到 loadable_list 中。需要留意的是在schedule_class_load
方法中会递归调用schedule_class_load(cls->superclass)
,来保证先将父类添加进 loadable_list 中。
准备好后就调用了call_load_methods
,关键代码如下:
1 | do { |
需要注意的是:
- 类的
+load
方法是先于分类调用的!但是在类的+load
方法中可以调用该类的分类中声明的方法,因为在此之前分类就已经 attach 到这个类上面了。 call_class_loads
从 loadable_list 中依次取出 class 然后调用+load
,这里保证了父类优先调用的顺序!+load
方法是直接使用函数内存地址的方式调用的:(*load_method)(cls, SEL_load)
,它对于实现了+load
方法的添加进 loadable_list 中的每个类与分类都会调用。因此这里成了极佳的Method Swizzling的时机。但是分类之间的+load
执行顺序是按照编译顺序决定的,因此不同的编译顺序会导致分类间的+load
顺序不固定。类的载入顺序不同也导致类之间+load
方法执行顺序不固定。- 如果代码还依赖了其他程序库,那么其他程序库里面的相关类的
+load
方法会先执行。 - 留意与
+load
类似的+initialize
方法,可以执行类的初始化操作,不同的是调用这个方法时,运行时系统已经处于正常状态了,在这里可以调用任意类的任意方法。+initialize
方法是惰性调用的,在程序首次使用类的时候才会调用一次它,这个方法是线程安全的,执行时会堵塞其他线程。还有与+load
不同,+initialize
遵循普通的继承与覆写规则,当类没有实现这个方法时,会调用其父类的实现。
对象的创建
接下来简单看下对象的创建过程,我们通过调用[[XXXClass alloc] init]
来生成一个实例对象。
alloc
在代码[[NSObject alloc] init]
中添加一个断点,查看一下调用栈:
初始化对象的关键代码都在_class_createInstanceFromZone
方法中,抽取出一个主要流程的代码如下:
1 | //获取实例对象内存对齐后的大小 |
init
1 | - (id)init { |
可以看到init
操作只是简单的返回了对象背身。
最后
看完了这些我们也能回答引言中提出的问题了:Objective-C对象不能简单对应于一个C struct,访问成员变量不等于访问C struct成员,当生成对象时,开辟出来对象的instanceSize
大小的内存区域并返回指向该空间的指针,在这个内存空间中不仅包含了objc_object
中定义的isa
还有其他所有的实例变量(包括继承自父类的及其自己的),我们通常所说的类的实例对象也就指的是这块内存空间。