【iOS】源码学习-类的结构分析
前言
我们看下类的源码:
objc
struct objc_class : objc_object {
objc_class(const objc_class&) = delete;
objc_class(objc_class&&) = delete;
void operator=(const objc_class&) = delete;
void operator=(objc_class&&) = delete;
// 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
//...
结构图如下:

类本质上也是一个对象,接下来详细分析。
cache
cache_t中存储的是方法的缓存列表,之所以设计方法缓存列表存储在类的结构中,是为了更快地响应消息发送。
objc
struct cache_t {
private:
// explicit_atomic:是为了线程安全,将普通指针转换为原子指针的操作
// 主要存buckets指针,也可能顺序编码一部分mask
// bucket:缓存数组里的一个格子,每个bucket通常存一组方法名(sel)+方法实现地址(lmp)
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
// mask是缓存数组的掩码,本质上代表缓存容量范围,用来计算bucket下标
explicit_atomic<mask_t> _maybeMask;
#if __LP64__
// 是缓存相关的标记位,只在64位环境下存在,用来记录一下缓存状态
uint16_t _flags;
#endif
// 表示当前cache里已经占用了多少个bucket(方法
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
这里只贴出了结构体中非static修饰的属性,主要是因为static类型的属性不存在在结构体的内存中。
这里简单区分几个概念:
- bucket:缓存数组里的一个格子
- capacity;缓存数组格子的总数
- mask:用来计算下标的掩码,通常等于capacity - 1
这个类在64位环境下应该是16个字节,包含一个结构体指针类型和一个联合体。这个类的结构可以大致理解为:

bits
bits封装了类的其他信息,如成员变量列表(ivars)、方法列表(methods)、协议列表(protocols)、属性列表(properties)等。
通过看class_data_bits_t源码我们发现bits不是直接存放所有方法、属性、协议和成员变量的,而是一个入口,它内部通过一个整数值,同时保存了两类信息:
- 真正的数据地址
- 低位flags标志
在64位系统下,bits的内存分布如下:
- Bit Index为0-2(低三位):存储状态标志。
- Bit Index为3-63(高61位):要用来保存数据指针。类未 realized时,这个数据指针指向class_ro_t;类realized后,这个数据指针指向class_rw_t。class_rw_t中包含flags、witness、firstSubclass、nextSiblingClass,以及一个ro_or_rw_ext字段。ro_or_rw_ext 可能直接关联 class_ro_t,也可能在需要动态扩展时关联 class_rw_ext_t。
objc
static bool has_rw_pointer(uintptr_t bits) {
#if FAST_IS_RW_POINTER
return (bool)(bits & FAST_IS_RW_POINTER);
#else
return bits != 0 && (flags(bits) & RW_REALIZED);
#endif
}
objc
class_rw_t* data() const {
// 调用前提是当前bits已经存的是class_rw_t *,即已完成realized
ASSERT(has_rw_pointer());
uintptr_t localBits = bits.load(std::memory_order_relaxed);
#if __BUILDING_OBJCDT__
return (class_rw_t *)((uintptr_t)ptrauth_strip((class_rw_t *)localBits,
CLASS_DATA_BITS_RW_SIGNING_KEY) & FAST_DATA_MASK);
#else
return (class_rw_t *)((uintptr_t)ptrauth_auth_data((class_rw_t *)localBits,
CLASS_DATA_BITS_RW_SIGNING_KEY,
ptrauth_blend_discriminator(&bits,
CLASS_DATA_BITS_RW_DISCRIMINATOR)) & FAST_DATA_MASK);
#endif
// 从bits这个原子变量里读取当前保存的值
// bits中还保存两种东西:1. class_ro_t(类还没有realized)2. class_rw_t(类已经realized)
uintptr_t bitsValue = bits.load(std::memory_order_relaxed);
// 如果是bits是class_rw_t,从class_rw_t里拿到原始的class_ro_t
if (has_rw_pointer(bitsValue)) {
return data()->ro();
}
uintptr_t authedBits;
// 如果bits是class_ro_t,说明类还没realized,那就对bits做指针认证或剥离签名,再用FAST_DATA_MASK取出真正的class_ro_t地址
if (authentication == Authentication::Strip || objc::disableEnforceClassRXPtrAuth) {
authedBits = (uintptr_t)ptrauth_strip((const void *)bitsValue,
CLASS_DATA_BITS_RO_SIGNING_KEY);
} else {
authedBits = (uintptr_t)ptrauth_auth_data((const void *)bitsValue,
CLASS_DATA_BITS_RO_SIGNING_KEY,
ptrauth_blend_discriminator(&bits,
CLASS_DATA_BITS_RO_DISCRIMINATOR));
}
return (const class_ro_t *)(authedBits & FAST_DATA_MASK);
类最开始只是保存在Mach-O文件中的静态结构体数据,APP启动时由dyld映射到内存中,然后还需经过Runtime的realized/initialize等处理,最终可变成一个可以创建对象、发送信息、动态修改的运行时类。这个过程叫类变得"完全可用"。
在OC的运行时,一个类从磁盘加载到内存完全可用,主要经历两个阶段(bits指向什么,完全取决于类当前是否已经被Runtime realized):
- 未实现(unrealized):此时只有静态编译产生的原始只读数据(class_ro_t),为了省内存,运行时直接将bits指向class_ro_t的地址。
- 已实现(realized):当第一次使用该类时,运行时会将class_ro_t中的数据复制或者迁移到class_rw_t结构中,以便于后续动态修改,此时bits指向class_rw_t,class_rw_t关联class_ro_t。
这里区分对比一下realized和initialized:
- realized:关注类的底层结构有没有被Runtime准备好。当类第一次被Runtime使用时,Runtime会对它执行realize,即创建rw关联ro。
- Initialized:关注类的
+initialize方法有没有调用过,即初始化逻辑有没有执行过。
我们着重探索属性列表和方法列表。在此之前,先来区分一下几个概念。
ro + rw + rw_ext
class_ro_t
编译期确定的原始类信息,只读,省内存。
这里的代码是逻辑上的简化结构,真实objc4源码会因版本和架构不同有所变化。
objc
uint32_t flags; // 标志位,存储类的静态属性
uint32_t instanceStart; // 实例变量起始偏移量
uint32_t instanceSize; // 实例对象所需内存大小,Runtime会基于它计算allo
// 保留字段,对齐填充
#ifdef __LP64__
uint32_t reserved;
#endif
// 指向只读数据的指针数组 (关键部分)
const uint8_t * ivars; // 实例变量列表(ivar_list_t)
const uint8_t * baseMethods; // 原始方法列表(method_list_t,编译期确定的方法)
const uint8_t * baseProtocols; // 原始协议列表(protocol_list_t)
const char * name; // 类名字符串
const uint8_t * baseProperties; // 原始属性列表(property_list_t)
};
base的意义是:表示类本身在编译期就有的原始内容,不包含分类后续添加的协议、属性、协议。
class_rw_t
运行时创建的可写类信息,负责运行时状态。
objc
struct class_rw_t {
// 基础信息
uint32_t flags; // 运行时标志
uint32_t version; // 版本号
const class_ro_t *ro; // 指向只读数据class_ro_t的指针
// 核心数据数组,支持动态扩容
method_array_t methods; // 方法列表.包含原始方法和分类添加的方法
property_array_t properties; // 属性列表
protocol_array_t protocols; // 协议列表
// 关联对象和扩展数据
Class firstSubclass; // 第一个子类,用于遍历继承树
Class nextSiblingClass; // 下一个兄弟类。用于遍历同一父类的其他子类
// 扩展结构体指针 (懒加载)
class_rw_ext_t *ext; // 指向扩展数据的指针 (如果数据太多或需要额外字段)
};
class_rw_ext_t
在实际生活中,class_rw_t会占用比class_ro_t更多的内存,因此结构体class_rw_ext_t派上用场了。
运行时按需创建的扩展信息,用于存放不常用或可选的数据(方法、属性、协议等可变列表),以实现内存节省。
objc
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr<const class_ro_t> ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
const char *demangledName;
uint32_t version;
};
该结构体存储了class_ro_t和methods(方法列表)、properties(属性列表)、protocols(协议列表)等信息。而class_ro_t中也存储了baseMethodList(方法列表)、baseProperties(属性列表)、baseProtocols(协议列表)以及实例变量、类的名称、大小等信息。
这里我们认识两个概念:
- Clean Memory:类被Runtime加载之后不会发生更改的内存块。
- Dirty Memory:运行时会发生更改的内存块。类一旦被加载,就会变成Dirty Memory,因为运行时会向它写入新的数据。例如我们可以通过Runtime给类动态的添加方法。
Dirty Memory会比Clean Memory昂贵得多。原因在于:
- 因为它需要更多的内存信息,并且只要进程在运行时,就必须保留它。
- Clean Memory可以进行移除,从而节省更多的内存空间,需要Clean Memory时,系统可以从磁盘中重新加载。
class_ro_t属于Clean Memory,因为它是只读的。class_rw_t和class_rw_ext_t属于Dirty Memory,它们在运行时可能被修改。
这里我们值得注意一下class_rw_ext_t的创建前提条件:
- 使用分类的类
- 使用Runtime API动态修改类结构的时候
在遇到以上两种情况的时候,类的结构(属性、协议、方法)发送改变,原有的ro已经不能继续记录类的属性、协议、方法信息了,于是系统重新生成可读可写的内存结构rw_ext来存放新的类结构。
总结
Runtime采用ro+rw+rw_ext的拆分结构。ro保存不会变的原始数据,rw保存运行时状态,rw_ext按需保存动态扩展数据。
看一下整个编译期的过程:
当类被编译时,二进制类在磁盘中表示为:

当类首次被使用时,Runtime会为它分配额外的存储容量,用于读取、写入:

大约10%左右的类会存在动态更改行为,这样clss_rw_t的大小减少了一半,变成如下的结构:

探索属性列表
现在看源码就理解了
对比class_ro_t和class_rw_t源码:
objc
uint32_t flags; // 标志位,存储类的静态属性
uint32_t instanceStart; // 实例变量起始偏移量
uint32_t instanceSize; // 实例对象所需内存大小,Runtime会基于它计算allo
// 保留字段,对齐填充
#ifdef __LP64__
uint32_t reserved;
#endif
// 指向只读数据的指针数组 (关键部分)
const uint8_t * ivars; // 实例变量列表(ivar_list_t)
const uint8_t * baseMethods; // 原始方法列表(method_list_t,编译期确定的方法)
const uint8_t * baseProtocols; // 原始协议列表(protocol_list_t)
const char * name; // 类名字符串
const uint8_t * baseProperties; // 原始属性列表(property_list_t)
};
objc
struct class_rw_t {
// 基础信息
uint32_t flags; // 运行时标志
uint32_t version; // 版本号
const class_ro_t *ro; // 指向只读数据class_ro_t的指针
// 核心数据数组,支持动态扩容
method_array_t methods; // 方法列表.包含原始方法和分类添加的方法
property_array_t properties; // 属性列表
protocol_array_t protocols; // 协议列表
// 关联对象和扩展数据
Class firstSubclass; // 第一个子类,用于遍历继承树
Class nextSiblingClass; // 下一个兄弟类。用于遍历同一父类的其他子类
// 扩展结构体指针 (懒加载)
class_rw_ext_t *ext; // 指向扩展数据的指针 (如果数据太多或需要额外字段)
};
我们发现:
- class_rw_t这个结构体存储的是属性,class_ro_t中存储的是成员变量。
- 成员变量列表(ivars)不仅包含在{}中定义的成员变量,还包括通过属性定义的成员变量。通过
bits->data()->ro()->ivars流程来获取成员变量表。 - 通过@property定义的属性,也会存储在bits属性中。通过
bits->data()->properties()->list流程来获取属性列表,其中只包含属性。
探索方法列表
这里的methods_list其实不是存储类方法的,它只存储一个实例方法。
- 类的实例方法存储在类的bits属性中,通过
bits->methods()->list获取实例方法列表。类中的方法列表除了包括实例方法,还包括属性的setter方法和getter方法。 - 类的类方法存储在元类的bits属性中,通过
元类bits->methods()->list获取类方法列表。
objc
MethodListAlternates methodAlternates() const {
MethodListAlternates result = {};
auto v = get_ro_or_rwe();
// 判断有没有创建rw_ext_t
if (v.is<class_rw_ext_t *>()) {
// 有就在这里创建
result.array = &v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
} else {
// 没有就在ro里读取
auto &baseMethods = v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods;
result.list = baseMethods.dyn_cast<method_list_t *>();
result.relativeList = baseMethods.dyn_cast<relative_list_list_t<method_list_t> *>();
}
return result;
}
const method_array_t methods() const {
auto alternates = methodAlternates();
if (auto *array = alternates.array)
return *array;
if (auto *list = alternates.list)
return method_array_t{list};
if (auto *relativeList = alternates.relativeList)
return method_array_t{relativeList};
return method_array_t{};
}