前言

系列第二篇:深入分析OC中的属性与方法的本质。通过上一篇Objective-C runtime - 类和对象,我们了解了objc_class中的 isa 成员变量;在这篇文章中,则会详细介绍objc_class的另一成员变量 bits

bits

先回忆一下objc_class的结构

objc_class

bits 用来存储类的属性,方法,协议等信息。它是一个class_data_bits_t类型

class_data_bits_t

1
2
3
4
struct class_data_bits_t {
uintptr_t bits;
// method here
}

这个结构体只有一个64bit的成员变量bits,先来看看这64bit分别存放的什么信息:

class_data_bits_t

is_swift : 第一个bit,判断类是否是Swift类

has_default_rr :第二个bit,判断当前类或者父类含有默认的retain/release/autorelease/retainCount/_tryRetain/_isDeallocating/retainWeakReference/allowsWeakReference 方法

require_raw_isa :第三个bit, 判断当前类的实例是否需要raw_isa (不是很懂)

data :第4-48位,存放一个指向class_rw_t结构体的指针,该结构体包含了该类的属性,方法,协议等信息。至于为何只用44bit来存放地址,可以参考Objective-C runtime - 类和对象

class_rw_t 和 class_ro_t

先来看看两个结构体的内部成员变量

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 class_rw_t {
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

Class firstSubclass;
Class nextSiblingClass;
};

struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
uint32_t reserved;

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};

可以看出,class_rw_t结构体内有一个指向class_ro_t结构体的指针。

每个类都对应有一个class_ro_t结构体和一个class_rw_t结构体。在编译期间,class_ro_t结构体就已经确定,objc_class中的bits的data部分存放着该结构体的地址。在runtime运行之后,具体说来是在运行runtime的realizeClass 方法时,会生成class_rw_t结构体,该结构体包含了class_ro_t,并且更新data部分,换成class_rw_t结构体的地址。

用两张图来说明这个过程:

类的realizeClass运行之前:

before runtime

类的realizeClass运行之后:

before runtime

细看两个结构体的成员变量会发现很多相同的地方,他们都存放着当前类的属性、实例变量、方法、协议等等。区别在于:class_ro_t存放的是编译期间就确定的;而class_rw_t是在runtime时才确定,它会先将class_ro_t的内容拷贝过去,然后再将当前类的分类的这些属性、方法等拷贝到其中。所以可以说class_rw_t是class_ro_t的超集,当然实际访问类的方法、属性等也都是访问的class_rw_t中的内容

属性和实例变量

现在知道了属性(property)存放在class_rw_t中,实例变量(ivar)存放在class_ro_t中。下面来看看他们的真实面目。

先看一段代码:

1
2
3
4
@interface TestOBJ : NSObject {
NSInteger ivarInt;
}
@property (nonatomic, assign) NSInteger propertyInt;

这个TestOBJ类有一个实例变量ivarInt 和 一个属性propertyInt 。使用lldb打印出class_ro_t的ivars:

ivars list

再使用lldb打印出class_ro_t的baseProperties:

properties list

顺便也把class_ro_t的baseMethodList打印出来吧:

methods list

从上面三张lldb的结果可以看出:TestOBJ类含有2个实例变量,1个属性和2个方法。但是代码里只有一个实例变量一个属性和0个方法。原因就是声明一个属性的同时,会同时生成一个实例变量和该实例变量的存取方法。

propertyInt为例:生成了 _propertyInt 实例变量和 - (NSInteger)propertyInt; 以及 - (void)setPropertyInt:(NSInteger)propertyInt 两个存取方法

property_t

class_ro_t的baseProperties是一个存放property_t结构体的list。property_t结构体用来存放属性的信息

1
2
3
4
struct property_t {
const char *name;
const char *attributes;
};

该结构体保存了属性的名字和type encoding。

ivar_t

其实到这里你就会发现property_t结构体中存放的信息没啥用,真正有用的还是存放实例变量的结构体ivar_t

1
2
3
4
5
6
7
8
9
10
11
12
13
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
// alignment is sometimes -1; use alignment() instead
uint32_t alignment_raw;
uint32_t size;

uint32_t alignment() const {
if (alignment_raw == ~(uint32_t)0) return 1U << WORD_SHIFT;
return 1 << alignment_raw;
}
};

TestOBJ中的 ivarInt 实例变量为例来说明,先使用lldb打印该实例变量的详细信息:

ivar_t

name : 实例变量的名字 -> ivarInt

type : 实例变量的类型,type encoding -> q 表示int类型

size : 实例变量的内存大小 -> 8 因为NSInteger类型占8个字节

offset : 实例变量距离对象的首地址的偏移量 -> offset是一个指向int的指针,取出offset指向的int,发现偏移量为8

上图还打印了class_ro_t,里面有2个变量需要注意一下:

instanceStart : 对象开始存放实例变量的地址的偏移量 -> 8 因为对象的前8字节存放的是isa_t结构体

instanceSize : 对象的大小 -> 24 = isa_t + ivarInt + _propertyInt

所以:获取TestOBJ实例a的ivarInt的过程:取出 对象首地址加ivarInt的偏移量 的内存空间的int

get ivar

类继承关系中的对象的内存空间结构

先看看没有继承关系的类 TestOBJ:

testObj

再看看继承TestOBJ的Son类:

1
2
3
@interface Son : TestOBJ
@property (nonatomic, strong) NSString *sonStr
@end

它的对象既存储了TestOBJ的实例变量,也存放了Son自己的实例变量

son

使用lldb打印son的class_ro_t:

super

可以发现Son类的 instanceStart 为24,因为Son的实例的前8字节存放isa,后面16字节存放父类的ivarInt和_propertyInt两个NSInteger型实例变量。Son类的 instanceSize为32字节,因为他还有自己的一个指向NSString结构体的指针 _sonStr ,这个指针也占8字节。

另外_sonStr 这一个ivar_t的offset是24,也符合分析

注意Son类的class_ro_t在realizeClass方法完成后就确定下来了

方法

编译结束之后,类的方法就已经存储在class_ro_t里面的baseMethodList变量中了;当运行完runtime的realizeClass方法之后,会将baseMethodList拷贝到class_rw_t的methods变量中,并将各种分类的方法拷贝到methods中。这样类的所有方法就都聚集在class_rw_t的methods变量中了。

使用lldb打印出class_rw_t中的methods:

methods

可以看到这里的methods和上面打印出来的class_ro_t中的baseMethodList一模一样。这是因为这个类没有分类,只是简单的将baseMethodList的内容拷贝过去。

method_t

runtime使用method_t结构体来存放方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct method_t {
SEL name;
const char *types;
IMP imp;

struct SortBySELAddress :
public std::binary_function<const method_t&,
const method_t&, bool>
{
bool operator() (const method_t& lhs,
const method_t& rhs)
{ return lhs.name < rhs.name; }
};
};

name : 方法名,类型为SEL选择子,可以简单地理解成方法的id,字符串类型

types : 方法的type encoding

imp : 方法的实现,类型为IMP。IMP是函数指针,指向方法的实现,也就是指向实现该方法的代码区

底层存储方式

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 class_rw_t {
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

Class firstSubclass;
Class nextSiblingClass;
};

struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
uint32_t reserved;

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};

class_ro_t里面的method_list_tivar_list_t 都是 entsize_list_tt 的结构体;class_rw_t里面的 method_array_tproperty_array_t 都是 list_array_tt 的结构体。来看看这个底层存储方式

entsize_list_tt

1
2
3
4
5
6
7
template <typename Element, typename List, uint32_t FlagMask>
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
Element first;
// other code
}

一个存储Element的序列,可以使用get(index)方法获取特定index的Element;它也提供了iterator遍历器,具体实现细节可以查看代码。objc-runtime-new.h

list_array_tt

1
2
3
4
5
6
7
8
template <typename Element, typename List>
class list_array_tt {
private:
union {
List* list;
uintptr_t arrayAndFlag;
};
}

简单来说是一个存放entsize_list_tt的数组。也提供了iterator遍历器。也定义在objc-runtime-new.h

总结

其实这篇文章就是👇两篇参考文章的一个合体。总结来说:objc_class结构体中的bits成员变量存放着类的属性、方法、协议等信息。这一些信息,有的是在编译期间就确定的;另外一些则是在运行runtime的realizeClass的方法时添加上去的。

那么当runtime将类初始化之后,所有的类所需的信息都存放在了class_rw_t结构体中

fixme

在这里做个更正:realizeClass方法中,并不是将class_ro_t中的属性、方法的list拷贝到class_rw_t中;而只是通过class_rw_t来操作class_ro_t中的各种list。class_rw_t中的list_array_tt 有指向 class_ro_t中的 entsize_list_tt 的指针。

参考