基于 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_tclass_ro_t,标志部分包含 FAST_*(如 FAST_HAS_DEFAULT_RRFAST_IS_SWIFT_STABLE/LEGACY)。

class_rw_t 和 class_ro_t

class_data_bits_t 相关的有两个很重要结构体,class_rw_tclass_ro_t,其中定义着method listprotocol listproperty 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 反混淆名与版本等附加数据.

这里具体举几个例子:

  1. 添加 Category :当包含类别的镜像被 dyld 映射并交给 ObjC 运行时,运行时会“附加类别”的方法/属性/协议到目标类上,需聚合这些增量数据到类的 rw 扩展
  2. 在代码里动态新增或替换方法:调用 class_addMethod/class_replaceMethod/class_addMethodsBulk/class_replaceMethodsBulk,会生成新的方法列表并附加到类,触发 rw 扩展分配
  3. 动态添加属性:调用 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] 实际上是:

  1. objc_msgSend(MyClass, @selector(alloc))
  2. 查找缓存 :在元类(MetaClass)的 Cache 中查找 alloc
  3. 缓存未命中 :如果首次调用,缓存大概率为空,进入 lookUpImpOrForward
  4. 方法列表搜索 :遍历方法列表。
  5. 动态解析/转发 :如果还没找到,可能触发 resolveClassMethod 等。
  6. 最终调用 :找到 +allocIMP(通常是 _objc_rootAlloc ),然后执行。
  7. 再次消息发送 : _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 运行速度很大的优化:

  1. 正常来说,按照之前的流程,我们需要执行 alloc -> allocWithZone:,这里会产生两次 objc_msgSend,会产生两次查找 Cache、两次查找方法列表的开销.新版本则直接优化了这些东西.
  2. 而在 App 冷启动过程中,大量的类是首次创建和被使用,如果走 objc_msgSend ,会触发大量的方法缓存填充和方法列表搜索;而新版本则会走 _objc_rootAllocWithZone 只需要做一次 realizeIfNeeded (如果还没 realize),之后就是纯内存分配,不会污染 Cache,也不会触发动态查找。
  3. fastpath(!cls->ISA()->hasCustomAWZ()) 利用了更强大的 CPU 的分支预测。绝大多数类( 99% 以上)都不会重写 allocallocWithZone ,因此 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 要做什么,所以必须通过动态消息机制让对象自己去处理。