分析 runtime 对象和结构
基于 objc4-950 版本
NSObject
NSObject 的结构在 NSObject.h 中,非常简单明了,就是一个Class类型的isa变量,其他信息都被隐藏.
@interface NSObject <NSObject> {
Class isa OBJC_ISA_AVAILABILITY;
}
这是公开头文件里的旧 ABI 兼容声明,用来让调试器/旧代码看到一个 isa;在现代运行时它被标记为 deprecated 且真实实现并不依赖这个公开 ivar,在现实应用中也不能直接操作,而是object_getClass 等方法获取信息.
具体结构继续分析
对象结构
objc_object
在对外公开里,参照上文的解释,使用 Class 表示以保证 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、位域标志、引用计数等执行操作。
通过这个,我们可以了解到,在 OC 中,除了少数特殊情况(比如说 Tagged Pointer),每个对象都是一个结构体,结构体中都包含一个 isa 的成员变量,而为了为支持非指针 isa、内联引用计数和 ptrauth,runtime 又把 isa 封装为 isa_t.
在这里我们可以下一个简单的结论:
公开头将其声明为 Class 以维持 ABI 稳定;内部实现用封装过的 isa_t 以支持非指针 isa、内联 RC 与 ptrauth。Tagged Pointer 是对象模型的一个特例,不是常规“结构体对象”。
isa_t
isa_t 是一个 union 结构对象,包含整字 bits、私有 cls 和平台特定的位域 struct;位域在 64 位平台占 8 字节,总 64 位,字段顺序由 ISA_BITFIELD 宏定义。
这里的 bits 可以操作整个内存区,而位域只能操作对应的位;直接使用 cls 读/写被禁止(私有),必须通过 setClass/getClass,原因是 ptrauth 需要根据对象地址进行签名/认证;而在非指针 isa 情况下,位域里的 shiftcls/indexcls 保存类信息,extra_rc 管理内联 RC,has_sidetable_rc 表示是否溢出到 SideTable.
私有定义了 cls,防止绕过 ptrauth,要求通过 setClass/getClass 方法来使用.
下面是 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,说明“类对象也是对象”,拥有 isa_t 头与相同的对象语义。
里面包含了 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)以防篡改。在运行时,才会通过 realizeClass 函数将 bits 指向 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 方法来初始化 Class 然后将它们串联起来.这部分在新版本中进行了大幅度优化:
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,会产生两次查找Cache、两次查找方法列表的开销.新版本则直接优化了这些东西. - 而在
App冷启动过程中,大量的类是首次创建和被使用,如果走objc_msgSend,会触发大量的方法缓存填充和方法列表搜索;而新版本则会走_objc_rootAllocWithZone只需要做一次realizeIfNeeded(如果还没realize),之后就是纯内存分配,不会污染Cache,也不会触发动态查找。 fastpath(!cls->ISA()->hasCustomAWZ())利用了更强大的CPU的分支预测。绝大多数类( 99% 以上)都不会重写alloc或allocWithZone,因此CPU可以非常高效地预测并执行这一分支。
总之,新版本把一个原本复杂的 动态消息分发 过程,在绝大多数情况下优化成了一个简单的 函数调用 。对于 alloc 这种在 App 生命周期中调用次数数以万计的操作,这种 O(1) 级别的路径优化带来的累积性能提升是非常显著的。
疑问
为什么只优化 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 的任务很单一:99% 的情况下就是“开辟一块内存,把 isa 指针指过去”。这非常适合由系统统一接管。
而 init 的任务千变万化:在开发时几乎都会重写 init 来做各种初始化逻辑。系统无法预知 init 要做什么,所以必须通过动态消息机制让对象自己去处理。