前面我们分析了对象的创建与本质,对象的创建依赖于类,接下来我们继续探索类的本质。
类的创建
我们知道,对象是在运行时创建的,对象的创建依赖于类,那么类是在什么时候创建的呢?
我们可以有两种方法类验证:
lldb 打印类和元类的指针
我们通过在 main 函数处断点,此时 main 函数还未执行,通过 lldb 命令,可以在控制台输出类和元类的地址。

查看 Mach-O 文件
我们将 cmd + b 编译后的 Mach-O 文件在 MachOView 中打开,可以看到:

这说明类和元类在程序编译期就已经创建。
指针与内存偏移
在 OC 的世界里,数据在内存中的存储是以指针的形式存在的。这些指针大致可以分为:
- 普通指针
- 对象指针
- 数组指针
普通指针
OC 中普通指针是相对于值类型来说的:
int a = 10; |
值类型在内存中是值拷贝类型,所以 a和b 在内存中是两个不同的存在。
对象指针
对象指针很好理解,就是针对对象来说的:
SMPerson *p1 = [SMPerson alloc]; |
数组指针
在编写程序时,数组是用的相当多的:
int d[4] = {1, 2, 3, 4}; |
在内存中示意图大致如下:

内存偏移
上面的几种指针,我们都可以通过访问内存地址取得数据:对象在内存中分配地址,我们可以通过首地址,并结合各类型所占字节长度偏移取得的数据。
上面的数组的例子最能体现我们要表达的内存偏移的概念:根据数组的数组的首地址,在结合 int 类型 4 字节长度的特性,我们可以每个元素存储的位置。
类的本质
类的本质
要解读类的本质,我们从 NSObject 开始:
@interface NSObject <NSObject> { |
NSObject有一个Class类型的isa成员变量。
接下来用 clang 编译 main.m,输出 main.cpp 文件,查看 NSObject 的底层定义:
clang -rewrite-objc main.m -o main.cpp |
打开 main.cpp, 找到 NSObject:
|
NSObject 是一个 objc_object 结构体,同时定义了一个 NSObject_IMPL 结构体,里面有 isa 成员变量,对应上面类 NSObject 的 isa。
对于继承自 NSObject 的类:
@interface SMBook : NSObject { |
我们也同样可以在 main.cpp 中看到:
|
SMBook 同样是一个 objc_object 的结构体,因为继承自 NSObject,SMBook_IMPL 结构体中除其自身的属性外,还多了 NSObject_IVARS – 即继承 NSObject 的类都相当于有一个 isa。
NSObject 及其子类本质上都是 objc_object 结构体类型,所以类本质上也是一个对象,即万物皆对象。
类的结构
类在底层实现是一个结构体指针:
typedef struct objc_class *Class; |
所以 Class 是一个8字节的指针类型。
继续看 objc_class 的结构:
struct objc_class : objc_object { |
objc_class 继承自 objc_object,这说明类也是一个对象。
注意这里的 Class ISA,这个 ISA 是针对优化后的:
inline Class |
现在看 objc_class 的结构:
ISA表示元类superclass表示父类cache_t,方法缓存重要结构体bits,存储数据的结构体
类的存储
OC 中类一般都会有属性以及成员变量,他们在类中是如何存储的呢?
类的内存分布
首先需要我们对类的结构体的内存结构有一个清晰的认识:
| 结构体成员 | 内存大小 |
|---|---|
| ISA | 8 |
| superclass | 8 |
| cache | 16 |
ISA 和 superclass 很好理解,都是 Class 的指针类型,在64位结构下各占8个字节,这里我们着重看下 cache:
struct cache_t { |
从上面的代码可以看出,cache 是 cache_t 的结构类型,其内部有3个成员变量,在 64 为架构模式下,结合内存对齐等,cache_t 占 8 + 4 + 4 = 16 个字节。
我们要读取类中成员变量和属性、方法等信息,需要读取 bits 中的值,结合上面讲的内存偏移,我们需要在类的首地址上偏移 32 个字节,用16进制表示为:0x20。
获取类的 bits
我们通过 LLDB 命令来探索类结构的第四个属性 bits。
@interface SMPerson : NSObject { |
我们先拿到 pClass, 然后在控制台使用 LLDB 命令:
x/4xg pClass |

我们需要得到 bits 指针的地址,需要进行指针偏移,即:
0x100001238 + 0x20 = 0x100001258 |
我们继续在控制填输入:
(lldb) po 0x100001258 |
会有如下输出:
objc[6727]: Attempt to use unknown class 0x10190d4a0. |
显然,bits 不是一个对象而是一个结构体,这里我们需要强转一下并得到如下输出:
(lldb) p (class_data_bits_t *)0x100001258 |
解析 class_rw_t
OC 中类的属性、成员变量和方法等都存储在 class_rw_t 中,结合上面 objc_class 的结构:
class_rw_t *data() { |
struct class_data_bits_t 中:
struct class_data_bits_t { |
class_data_bits_t 占8个字节,即64位,其中从第 4~47 共 44 位表示 class_rw_t。
我们调用 $2->data()获得 class_rw_t:
(lldb) p $2->data() |
然后我们根据 libObjc 的源码中关于 class_rw_t 相关的定义:
struct class_rw_t { |
我们在打印证实下是否这种结构:

这里我们需要留意几个关键成员变量:
const class_ro_t *ro是一个不可变的属性methodspropertiesprotocols
这里还有两点是需要注意的:
class_rw_t中没有发现成员变量的列表ro存在的意义:class_rw_t是可以在运行时拓展一些属性、方法和协议等内容class_ro_t是在编译时就已经确定了的,存储类的成员变量、属性、方法和协议
现在我们已经获取到 class_rw_t 的值,下面我们就预测一下我们的属性、方法等存储在结构体的哪些变量上
预测属性应该定义在
properties中:接着我们查看
properties中的内容:
预测方法应该定义在
methods中:
在
method_list_t中我们可以看到此时我们的方法有 3 个:struct method_list_t : entsize_list_tt<method_t, method_list_t, 0x3> { }
method_list_t继承自entsize_list_tt,entsize_list_tt实现了first和迭代器方法,我们可以通过get方法读取到数组中的元素:(lldb) p $10.first
(method_t) $16 = {
name = "sayHello"
types = 0x0000000100000f85 "v16@0:8"
imp = 0x0000000100000dc0 (objc-debug`-[SMPerson sayHello] at SMPerson.m:12)
}
(lldb) p $10.get(1)
(method_t) $17 = {
name = "nickName"
types = 0x0000000100000f8d "@16@0:8"
imp = 0x0000000100000e20 (objc-debug`-[SMPerson nickName] at SMPerson.h:16)
}
(lldb) p $10.get(2)
(method_t) $18 = {
name = "setNickName:"
types = 0x0000000100000f95 "v24@0:8@16"
imp = 0x0000000100000e50 (objc-debug`-[SMPerson setNickName:] at SMPerson.h:16)
}
预测协议应该定义在
protocols中:
因为这里并没有实现任何协议,所以数组为空。
进行到这里,现在有个疑问是,我们的成员变量去哪里了呢?这就需要我们的 class_ro_t 出场了。上面已经说过,class_ro_t 在编译时就已经确定了成员变量、属性、方法和协议的布局,不考虑运行时动态添加方法等操作,我们应该在 class_ro_t 读取类的数据。这里的 ro 就是 read only 的意思了。
解析 class_ro_t
源码中 class_ro_t 的结构为:
struct class_ro_t { |
首先第一步是获取 class_ro_t:
(lldb) p $4.ro |

属性存储在
baseProperties中:
成员变量存储在
ivars中:
成员变量为
_hobby和_nickName,为什么会有_nickName呢,他不是属性吗?这就是编译器会帮助我们给属性生成一个带下划线的成员变量了。方法存储在
baseMethodList中:
这里除了我们写的方法
sayHello之外,还有setNickName和nickName方法。这是编译器帮助我们给属性生成的setter和getter方法。
类方法的存储
在上面的 baseMethodList 中,并没有发现我们的类方法 sayHappy,这说明类方法并不存储在此,那么类方法放在哪里呢?
我们知道在 OC 的世界中,万物皆对象,类也是对象,且类是元类的对象,那么我们是不是可以大胆猜测,类方法是存储在元类的 ro 中呢?下面我们就此来验证:
首先获得元类,类的 isa 指向元类,从之前的 isa 相关的知识:

我们在元类的 ro 中找到我们的类方法。
class_rw_t 与 class_ro_t 的联系与区别
根据上面的分析 class_rw_t 和 class_ro_t 中都存储了类的属性、方法等。为什么 class_rw_t 也能拿到这些信息呢?是因为执行了方法 realizeClassWithoutSwift:
static Class realizeClassWithoutSwift(Class cls) { |
然后在调用 methodizeClass:
static void methodizeClass(Class cls) |
在methodizeClass中,将 ro 中的方法、属性,遵循的协议、category 的方法都添加都 rw 中(注意这里只是将指针指向 ro 中对应的列表地址)。这样在运行期我们就可以在 rw 中拿到相应的信息了。
前面已经说过 ro 是在编译期就已经确定了的,而 rw 可以在运行期拓展方法等,现在我们就开看一个例子:
void run() { |
现在我们开看 rw 与 ro 中的方法列表:

ro 中没有我们动态添加的方法,符合我们的预期,但是很奇怪的是,rw 里面的值变的很奇怪,留个坑 o(╥﹏╥)o
这里仍然有需要注意的点:
- 在没有动态添加方法时,
ro的baseMethodList与rw的methods的list指向的地址是相同的,不只是方法列表,属性列表指向的地址也是相同的,这说明运行时若没有动态添加属性或方法时,他们指向相同的地址 - 运行时动态添加方法等之后,
rw发生了变化
类的内存分布图

总结
- 类和元类创建于编译期
- 万物皆对象,类的元类的对象
class_ro_t存储类的成员变量、属性、方法、协议等,是只读的class_rw_t可以在运行期进行拓展- 实例方法存储在类中
- 类方法存储在元类中