从 objc4-950 看 Objective-C 对象与类的核心结构
基于 objc4-950 版本。
这篇主要想回答 4 个问题:
- 一个 Objective-C 对象最底层到底长什么样?
isa为什么从公开头文件里的Class,变成了 runtime 内部的isa_t?- 类对象本身的结构由哪些部分组成?
class_ro_t、class_rw_t和class_data_bits_t分别负责什么?
NSObject
NSObject 在公开头文件里的结构非常简单,看起来几乎只有一个 Class 类型的 isa 成员,其他信息都被隐藏起来了。
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
这其实更像是公开头文件里的 ABI 兼容声明,用来让调试器和旧代码还能看到一个 isa。在现代 runtime 里,它已经被标记为 deprecated,真实内部实现也不是直接围绕这个公开 ivar 展开的;在实际开发中,也不应该直接去操作它,而是通过 object_getClass 之类的方法来获取信息。
接着往下看内部的真实结构。
对象结构
objc_object
在对外公开的定义里,还是使用 Class 来表示 isa,主要还是为了 ABI 兼容。
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};
进入 objc-private.h 之后,再看内部实现:
struct objc_object {
private:
char isa_storage[sizeof(isa_t)];
isa_t &isa() { return *reinterpret_cast<isa_t *>(isa_storage); }
const isa_t &isa() const { return *reinterpret_cast<const isa_t *>(isa_storage); }
...省略...
id retain();
void release();
id autorelease();
我这里省略了一部分代码,但如果继续往下看,就会发现 id 这个类型的定义其实是:
typedef struct objc_object *id;
实际上,id 就是指向 objc_object 的指针。
这里可以顺手解释一下 id 和其他常见类型的不同。
- 与
NSObject(或具体类指针): NSObject 是“静态类型为某个具体类”的对象指针;id 是“动态类型的任意对象指针”。二者在运行时都指向以 objc_object 开始的对象头,但编译期类型约束不同(id 更宽泛,可接收任何对象)。 - 与
Class: Class 是“指向 objc_class 的指针”,而 objc_class 继承自 objc_object,说明“类对象也是对象”;id 则用于表示任意实例对象(也可指向类对象,但习惯上 Class 更明确)。 - 与
void: void 只是原始地址,没有对象语义;id 有强约定的对象头结构,运行时 API 基于它的 isa、位域标志、引用计数等执行操作。
通过这些定义,我们大概可以先得到一个直观认识:从对象模型的角度来说,除了少数特殊情况(比如 Tagged Pointer),大多数 Objective-C 对象最终都可以看成是“以 objc_object 为对象头”的一块内存结构。而为了支持非指针 isa、内联引用计数和 ptrauth,runtime 又把这部分封装成了 isa_t。
这里可以先下一个简单结论:
公开头将其声明为 Class 以维持 ABI 稳定;内部实现用封装过的 isa_t 以支持非指针 isa、内联 RC 与 ptrauth。Tagged Pointer 是对象模型的一个特例,不是常规“结构体对象”。
isa_t
isa_t 是一个 union 结构,里面同时提供了整字 bits、私有的 cls,以及平台相关的位域定义。放在 64 位平台上看,它本质上就是 8 字节、64 位的一段数据,只不过 runtime 根据不同场景给了它不同的访问方式。
这里的 bits 可以用来整体读写整段内存,而位域只能操作各自对应的那几位。cls 被放在私有区域里,不能随便直接读写,必须通过 setClass/getClass 之类的方法来处理;这里面一个很重要的原因就是 ptrauth 需要结合对象地址来做签名和认证。
在非指针 isa 的情况下,位域里的 shiftcls/indexcls 用来保存类信息,extra_rc 用来管理内联引用计数,has_sidetable_rc 则表示这个对象是否还有一部分引用计数放在 SideTable 里。
把 cls 放成私有,本质上也是为了防止绕过 ptrauth 的这套流程,强制你通过受控接口来访问。
下面是 isa_t 的部分源码,其他架构和模拟器相关的定义我去掉了。
#include "isa.h"
union isa_t {
isa_t() { }
isa_t(uintptr_t value) : bits(value) { }
uintptr_t bits;
private:
Class cls;
public:
#if defined(ISA_BITFIELD)
struct {
ISA_BITFIELD; // defined in isa.h
};
...省略
# if __arm64__
# define ISA_MASK 0x0000000ffffffff8ULL
# define ISA_MAGIC_MASK 0x000003f000000001ULL
# define ISA_MAGIC_VALUE 0x000001a000000001ULL
# define ISA_HAS_CXX_DTOR_BIT 1
# define ISA_BITFIELD \
uintptr_t nonpointer : 1; /* 非指针 isa 标志:1=编码(packed/indexed),0=原始指针 */ \
uintptr_t has_assoc : 1; /* 对象存在或曾存在关联对象 */ \
uintptr_t has_cxx_dtor : 1; /* 类具有 C++/ObjC 析构 */ \
uintptr_t shiftcls : 33; /* 类指针>>3(MACH_VM_MAX_ADDRESS 0x1000000000) */ \
uintptr_t magic : 6; /* 与 ISA_MAGIC_MASK/ISA_MAGIC_VALUE 比较,用于识别/校验 packed-isa;非“初始化完成”标志 */ \
uintptr_t weakly_referenced : 1; /* 对象存在或曾存在弱引用 */ \
uintptr_t unused : 1; /* 保留位 */ \
uintptr_t has_sidetable_rc : 1; /* 引用计数在侧表中扩展存储 */ \
uintptr_t extra_rc : 19 /* 内联引用计数增量;见 RC_ONE/RC_HALF */
# define ISA_HAS_INLINE_RC 1
# define RC_HAS_SIDETABLE_BIT 44
# define RC_ONE_BIT (RC_HAS_SIDETABLE_BIT+1)
# define RC_ONE (1ULL<<RC_ONE_BIT)
# define RC_HALF (1ULL<<18)
# endif
class 结构
前面讲的是“对象头”,这里开始看“类对象本身”的结构。
objc_class 继承自 objc_object,这也说明一句我们经常会说的话在 runtime 结构上是成立的:类对象本身也是对象。
里面最重要的 3 个成员变量是:
- superclass: 指向父类的 Class 指针,用于 isKindOfClass/isSubclassOfClass 等向上链,与父类相关的操作使用它;
- cache: 方法缓存,用于加速消息派发,与缓存相关的方法通过 cache_t 操作,消息发送先查缓存再查方法列表与父类链。
- bits: 封装了指针与快速标志的 class_data_bits_t,使用位打包保存“数据指针与快速标志”。这是 objc_class 的核心指针/标志存储。与 ro/rw 视图、快速标志相关的方法才主要通过 bits.
struct objc_class : objc_object {
// 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
Class getSuperclass() const {
...省略
}
class_rw_t *data() const {
// 取运行时可变视图(class_rw_t*),含 ptrauth 验证/剥离,并用 FAST_DATA_MASK 掩码指针
return bits.data();
}
void setData(class_rw_t *newData) {
// 设置运行时可变视图指针(class_rw_t*)
bits.setData(newData);
}
const class_ro_t *safe_ro() const {
// 并发安全地获取只读视图(class_ro_t*),当 bits 指向 rw 时自动跳转 rw->ro
return bits.safe_ro();
}
}
关于 bits 可以单独细说一下:
bits 是一个封装类型 class_data_bits_t(不是裸的 uintptr_t),用位打包的方式存储“数据指针 + 快速标志”。其中指针部分可以指向 class_rw_t 或 class_ro_t,标志部分则会包含一系列 FAST_* 位,比如 FAST_HAS_DEFAULT_RR、FAST_IS_SWIFT_STABLE/LEGACY 等。
class_rw_t 和 class_ro_t
和 class_data_bits_t 紧密相关的还有两个很重要的结构:class_rw_t 和 class_ro_t。很多我们熟悉的 method list、protocol list、property list,最终都和它们有关。
先看 class_ro_t
struct class_ro_t {
// 编译期只读标志位(如 RO_META、RO_HAS_SWIFT_INITIALIZER 等)
uint32_t flags;
// 本类实例变量区起始偏移(对象头之后,用于布局/拷贝扫描)
uint32_t instanceStart;
// 实例总大小(包含父类与本类的 ivar)
uint32_t instanceSize;
// 非元类:ivarLayout 为强引用布局位图;元类:nonMetaclass 指向对应普通类
union {
const uint8_t * ivarLayout;
Class nonMetaclass;
};
// 类名指针(显式原子),保证并发读取的一致性
explicit_atomic<const char *> name;
// 基础方法列表(PointerUnion:单列表或相对列表数组;含 ptrauth)
objc::PointerUnion<method_list_t, relative_list_list_t<method_list_t>, method_list_t::Ptrauth, method_list_t::Ptrauth> baseMethods;
// 基础协议列表(PointerUnion:单列表或相对列表数组;含 ptrauth)
objc::PointerUnion<protocol_list_t, relative_list_list_t<protocol_list_t>, PtrauthRaw, PtrauthRaw> baseProtocols;
// 本类声明的 ivar 列表(不包含父类)
const ivar_list_t * ivars;
// 弱引用 ivar 布局位图(用于拷贝时弱引用修复)
const uint8_t * weakIvarLayout;
// 基础属性列表(PointerUnion:单列表或相对列表数组;含 ptrauth)
objc::PointerUnion<property_list_t, relative_list_list_t<property_list_t>, PtrauthRaw, PtrauthRaw> baseProperties;
...省略
在程序刚编译出来的时候,class_data_bits_t 这一侧主要对应的是 class_ro_t 这类只读视图。它本身是不可变的,通常放在 Mach-O 的常量区里,也会结合 ptrauth 做保护。等进入运行时阶段之后,runtime 才会在 realize 过程中逐步建立 class_rw_t 这套可写视图。
那接着看 class_rw_t
struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
// 指向只读 ro 的已签名指针(ptrauth),描述编译期常量视图
class_ro_t_authed_ptr<const class_ro_t> ro;
// 运行时聚合后的方法数组(包含类别/动态添加)
method_array_t methods;
// 运行时聚合后的属性数组
property_array_t properties;
// 运行时聚合后的协议数组
protocol_array_t protocols;
// Swift 类名的反混淆缓存(demangle 后的可读名)
const char *demangledName;
// 类版本号(class_getVersion/class_setVersion)
uint32_t version;
};
struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
// 运行时可变标志位(如构造/已实现/扩展等状态)
uint32_t flags;
// 已知类快速路径见证值:缓存数据段范围索引,用于 isKnownClass 快速判断
uint16_t witness;
#if SUPPORT_INDEXED_ISA
// 用于 indexed ISA 的类索引(平台支持时存在)
uint16_t index;
#endif
// 指向 ro 或 rw 扩展的原子指针(PointerUnion 封装,含签名与内存序)
explicit_atomic<uintptr_t> ro_or_rw_ext;
// 父类的第一直接子类指针(维护子类链表头)
Class firstSubclass;
// 同一父类下的下一个兄弟类(维护兄弟链表)
Class nextSiblingClass;
...省略
}
这里的 class_rw_t 可以理解成类在运行时的“可写头部与管理者”,负责状态位、层级链表,以及 ro/rw 扩展指针的管理;而 class_rw_ext_t 则更像一个“按需分配的扩展载体”,在需要时承载聚合后的方法/属性/协议数组,以及 Swift 反混淆名、版本号之类的附加数据。
这里可以举几个比较典型的例子:
- 添加
Category:当包含类别的镜像被dyld映射并交给ObjC运行时,运行时会“附加类别”的方法/属性/协议到目标类上,需聚合这些增量数据到类的rw扩展 - 在代码里动态新增或替换方法:调用
class_addMethod/class_replaceMethod/class_addMethodsBulk/class_replaceMethodsBulk,会生成新的方法列表并附加到类,触发rw扩展分配 - 动态添加属性:调用
class_addProperty/class_replaceProperty,会构建属性列表并附加,触发rw扩展分配
如果读过老版本代码,应该会对 realizeClass 这条链比较熟悉。新版本在这部分做了比较明显的拆分和优化:
realize 时线程安全的读取 ro,常规类分配 rw 并将 ro 关联到 rw,然后把类的数据指针从 ro 切换到 rw 并标记 RW_REALIZED;随后完成缓存、ISA 索引、父类/元类实现、ivar 对齐与方法/类别安装;Swift 类会在释放锁后由 Swift 运行时协同完成 realize。
void objc_class::realizeIfNeeded() {
if (fastpath(isRealized()))
return;
mutex_locker_t lock(runtimeLock);
realizeIfNeeded_nolock();
}
void objc_class::realizeIfNeeded_nolock() {
lockdebug::assert_locked(&runtimeLock.get());
realizeAndInitializeIfNeeded_locked(nil, (Class)this, false);
}
之后,将“实现 + 可选初始化”合并为一个锁内流程
static Class
realizeAndInitializeIfNeeded_locked(id inst, Class cls, bool initialize)
{
lockdebug::assert_locked(&runtimeLock.get());
if (slowpath(!cls->isRealized())) {
cls = realizeClassMaybeSwiftAndLeaveLocked(cls, runtimeLock);
}
if (!cls || !cls->ISA())
return nil;
if (slowpath(initialize && !cls->isInitialized())) {
cls = initializeAndLeaveLocked(cls, inst, runtimeLock);
}
return cls;
}
另外,如果遇到的是 Swift 类,还需要单独处理:
static Class
realizeClassMaybeSwiftMaybeRelock(Class cls, mutex_t& lock, bool leaveLocked)
{
lockdebug::assert_locked(&lock);
if (!cls->isSwiftStable_ButAllowLegacyForNow()) {
// Non-Swift class. Realize it now with the lock still held.
// fixme wrong in the future for objc subclasses of swift classes
cls = realizeClassWithoutSwift(cls, nil);
if (!leaveLocked) lock.unlock();
} else {
// Swift class. We need to drop locks and call the Swift
// runtime to initialize it.
lock.unlock();
cls = realizeSwiftClass(cls);
ASSERT(cls->isRealized()); // callback must have provoked realization
if (leaveLocked) lock.lock();
}
return cls;
}
总结
最后再试着从整体上总结一下:objc4-950 相比老版本,在这部分到底做了什么优化。
在老版本里,调用 [MyClass alloc],实际流程大概是:
objc_msgSend(MyClass, @selector(alloc))- 查找缓存 :在元类(MetaClass)的
Cache中查找alloc。 - 缓存未命中 :如果首次调用,缓存大概率为空,进入
lookUpImpOrForward。 - 方法列表搜索 :遍历方法列表。
- 动态解析/转发 :如果还没找到,可能触发
resolveClassMethod等。 - 最终调用 :找到
+alloc的IMP(通常是_objc_rootAlloc),然后执行。 - 再次消息发送 :
_objc_rootAlloc内部通常还会调用[self allocWithZone:nil],这又是一次完整的objc_msgSend流程。
这个过程会涉及不少内存访问、条件判断和跳转。对于 alloc 这种极高频操作来说,这些开销累积起来其实很可观。
到了 objc4-950,编译器和运行时会配合把 [MyClass alloc] 转成 objc_alloc(MyClass)。内部直接进入 callAlloc;如果类没有自定义 alloc/allocWithZone,那就直接调用 _objc_rootAllocWithZone。
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
if (slowpath(checkNil && !cls)) return nil;
// 核心优化判断:如果类没有自定义 alloc/allocWithZone 实现
if (fastpath(!cls->ISA()->hasCustomAWZ())) {
// 直接调用底层 C 函数,完全跳过 objc_msgSend
return _objc_rootAllocWithZone(cls, nil);
}
// 慢路径:如果有自定义实现,只能老老实实走消息发送
if (allocWithZone) {
return ((id(*)(id, SEL, struct _NSZone *))objc_msgSend)(cls, @selector(allocWithZone:), nil);
}
return ((id(*)(id, SEL))objc_msgSend)(cls, @selector(alloc));
}
------------
id _objc_rootAllocWithZone(Class cls, objc_zone_t)
{
// allocWithZone under __OBJC2__ ignores the zone parameter
// 1. 确保类已实现(如果没实现,这里会触发 realize)
cls->realizeIfNeeded();
// 2. 直接分配内存,创建实例
return _class_createInstance_realized(cls, 0, OBJECT_CONSTRUCT_CALL_BADALLOC);
}
这些结构和流程上的优化,最终会直接体现在 App 的运行速度上:
- 按照旧流程,
alloc -> allocWithZone:至少会带来两次objc_msgSend,也就意味着两次缓存查询、两次方法查找的成本;新版本把这部分尽量压缩掉了。 - 在
App冷启动过程中,大量类都是第一次被创建和使用。如果都走objc_msgSend,就会触发很多方法缓存填充和方法列表查找;而新版本更多会直接落到_objc_rootAllocWithZone,先做一次realizeIfNeeded(如果还没 realize),后面基本就是纯内存分配。 fastpath(!cls->ISA()->hasCustomAWZ())也利用了 CPU 的分支预测。绝大多数类都不会重写alloc或allocWithZone,所以这条分支天然就是一个非常适合优化的热点路径。
总之,新版本把原本复杂的一段动态消息分发流程,在绝大多数情况下优化成了一次更直接的 函数调用。对于 alloc 这种在 App 生命周期里会被调用成千上万次的操作,这种路径上的优化累积起来是非常可观的。
疑问
为什么只优化 alloc 而没有优化 init ?
答案也不复杂,我们先看一个最常见的例子:
id obj = [[MyClass alloc] init];
在这里:
[MyClass alloc]:可以走快路径,不一定需要填充消息缓存。[obj init]:这是一个普通的实例方法调用,仍然必须经过objc_msgSend。第一次调用时,也会触发查找并把init填进缓存。
id objc_alloc_init(Class cls)
{
// callAlloc: 尝试走 fast path 分配内存(优化部分)
// [ ... init]: 对结果发送 init 消息(未优化部分,走标准 objc_msgSend)
return [callAlloc(cls, true/*checkNil*/, false/*allocWithZone*/) init];
}
我们可以这么理解:
alloc 的任务很单一:大多数情况下就是“开一块内存,把对象头初始化好”。这件事非常适合由 runtime 做统一优化。
而 init 的任务则完全不同。开发里几乎人人都会重写 init 去做自己的初始化逻辑,系统无法提前知道它会干什么,所以这里依然必须保留动态消息分发机制。