Objective-C Runtime 完整机制:objc_class /cache/bits 源码解析

7 阅读13分钟

Objective-C(以下简称 OC)的灵活性、动态性,核心源于其底层的 Runtime 机制。而 Runtime 所有动态行为(消息发送

objc_class 的核心字段中,superclass(父类指针)、cache(方法缓存)、bits(类数据指针+标志位)三者缺一不可。其中,cache 决定了方法调用的效率,bits 存储了类的核心数据(方法、属性、协议等),二者是理解 Runtime 动态机制的关键。

很多开发者使用 OC 多年,却只停留在“会用”层面,对objc_class 的底层结构、cache 的缓存机制、bits 的数据存储逻辑一知半解。本文将基于 Apple 开源的 objc4 源码(最新稳定版),逐行解析 objc_classcachebits 的底层实现,结合 Runtime 核心流程,让你彻底吃透 OC 类的底层逻辑。

一、前置基础:Runtime 与 objc_class 的核心关联

在解析具体源码前,先明确两个核心前提,避免陷入细节误区:

  1. OC 是“动态语言”,其类和对象的行为并非编译期确定,而是由 Runtime 动态解析——比如方法调用、属性访问,最终都会被 Runtime 转化为底层函数调用(如 objc_msgSend)。

先看最基础的 objc_object 结构体(所有对象的祖宗),它是理解 objc_class 的前提:

// 所有OC对象的底层结构体(精简版,保留核心字段)
struct objc_object {
    isa_t isa; // 64位联合体,存储类指针、引用计数、标志位等信息
};

// isa_t 的核心结构(ARM64架构,iOS真机环境)
union isa_t {
    uintptr_t bits; // 原始64位数值,承载所有信息
    // 位域展开(64位按位分配)
    struct {
        uintptr_t nonpointer : 1;        // bit 0:是否是优化后的isa(0=纯指针,1=包含额外信息)
        uintptr_t has_assoc : 1;         // bit 1:是否有关联对象
        uintptr_t has_cxx_dtor : 1;      // bit 2:是否有C++析构函数
        uintptr_t shiftcls : 33;         // bit 3-35:类指针(右移3位存储,节省空间)
        uintptr_t magic : 6;             // bit 36-41:固定值0x1a,用于调试校验
        uintptr_t weakly_referenced : 1; // bit 42:是否被弱引用
        uintptr_t unused : 1;            // bit 43:未使用
        uintptr_t has_sidetable_rc : 1;  // bit 44:引用计数是否溢出到SideTable
        uintptr_t extra_rc : 19;         // bit 45-63:引用计数-1(存储额外引用计数)
    };
};

简单来说,isa 的核心作用是“标识对象的类型”——通过shiftcls 字段,对象能找到自己对应的类(objc_class),而类的 isa 则指向元类(Meta Class),这是 OC 实现方法调用的基础。

二、核心解析:objc_class 结构体源码拆解

OC 中的“类”(如 NSObject、自定义类),底层本质是 objc_class 结构体的实例。以下是从 objc4 源码中提取的精简版 objc_class 结构体(保留核心字段,省略辅助方法),也是本文的核心分析对象:

// 类的底层结构体(继承自objc_object,因此包含isa字段)
struct objc_class : objc_object {
    // 1. 父类指针:指向当前类的父类(如NSObject的父类是nil)
    Class superclass;
    // 2. 方法缓存:哈希表结构,缓存最近调用的方法,提升调用效率
    cache_t cache;
    // 3. 类数据指针+标志位:存储类的核心数据(方法、属性、协议等)
    class_data_bits_t bits;
    
    // 核心方法:从bits中取出类的可读写数据(class_rw_t)
    class_rw_t *data() const {
        return bits.data();
    }
};

从源码可以看出,objc_class 继承自 objc_object,因此它本身也有 isa 字段(继承而来),同时新增了三个核心字段:superclasscachebits

三者的核心关系的是:superclass 负责继承链的构建,cache 负责方法调用的缓存优化,bits 负责存储类的核心业务数据,三者协同支撑起 OC 类的所有动态行为。

补充:Class 类型的本质

我们日常使用的 Class 类型,本质是 objc_class 的指针别名,源码定义如下:

typedef struct objc_class *Class;

这就是为什么我们可以用 Class cls = [NSObject class]; 获取类对象——本质是获取 objc_class 结构体的指针。

三、深度解析:cache_t(方法缓存)的底层实现

在 OC 中,方法调用是高频操作(如 [self method]),如果每次调用都遍历类的方法列表查找,会严重影响性能。cache_t 的核心作用就是“缓存最近调用的方法”,下次调用时直接从缓存中取出,无需重复查找,这是 Runtime 优化方法调用效率的关键。

1. cache_t 结构体源码(精简版)

// 方法缓存结构体(哈希表实现)
struct cache_t {
    // 缓存存储的数组(数组元素是cache_entry_t类型,存储方法名和函数指针)
    bucket_t *_buckets;
    // 缓存的容量(总是2的幂,如4、8、16,方便哈希计算)
    mask_t _mask;
    // 已缓存的方法数量(当count > mask * 3/4时,会触发缓存扩容)
    mask_t _occupied;
    
    // 核心方法:插入方法缓存
    void insert(SEL sel, IMP imp, id receiver);
    // 核心方法:查找方法缓存
    IMP lookup(SEL sel);
};

其中,bucket_t 是缓存的“桶”,存储单个方法的缓存信息,源码如下:

// 单个缓存项(存储一个方法的信息)
struct bucket_t {
    SEL _sel; // 方法名(选择子,本质是const char*,如@selector(method))
    IMP _imp; // 函数指针(指向方法的具体实现代码地址)
    
    // 辅助方法:获取方法名和函数指针
    SEL sel() const { return _sel; }
    IMP imp() const { return (IMP)((uintptr_t)_imp ^ (uintptr_t)this); }
};

2. cache_t 的核心特性与工作流程

理解 cache_t,关键要掌握“哈希表存储”“缓存插入”“缓存查找”“缓存扩容”四个核心流程,结合源码逻辑逐一拆解:

(1)哈希表存储逻辑

cache_t 采用“开放寻址法”实现哈希表:

  • 用方法名 SEL 的哈希值,对_mask(缓存容量-1)取模,得到当前方法在 _buckets 数组中的索引;
  • 如果该索引对应的桶为空,直接存入当前方法的 SELIMP
  • 如果该索引已被占用(哈希冲突),则顺次查找下一个空桶,直到找到空桶存入。

这里 _mask = 容量 - 1(如容量为8,_mask=7),取模操作可简化为 hash & _mask,效率远高于传统取模运算,这也是缓存容量必须是2的幂的原因。

(2)缓存插入流程(insert 方法核心逻辑)

当我们第一次调用某个方法时,Runtime 会先查找方法列表,找到后将其插入 cache_t,核心步骤如下(结合源码逻辑简化):

  1. 计算方法 SEL 的哈希值 hash = sel_hash(sel)

注意:IMP 存储时会进行“异或加密”(_imp = (IMP)((uintptr_t)imp ^ (uintptr_t)this)),读取时再解密,这是苹果的安全优化,防止恶意篡改方法实现。

(3)缓存查找流程(lookup 方法核心逻辑)

当我们再次调用该方法时,Runtime 会先从 cache_t 中查找,核心步骤如下:

  1. 计算 SEL 的哈希值,得到索引 index = hash & _mask

(4)缓存扩容机制

_occupied(已缓存数量)超过 _mask * 3/4(缓存容量的75%)时,会触发缓存扩容,核心逻辑:

  • 新容量 = 旧容量 * 2(始终保持2的幂);
  • 创建新的 _buckets 数组(容量为新容量);
  • 将旧缓存中的所有方法,重新哈希后插入新数组;
  • 更新 _mask(新容量-1)和 _occupied(重置为旧的数量),释放旧数组内存。

3. cache_t 的实战意义

理解 cache_t 的缓存机制,能帮我们解释很多实际开发中的现象:

  • 为什么“首次调用方法比后续调用慢”?—— 首次调用需要查找方法列表,后续调用直接从缓存中获取,效率更高;
  • 为什么分类(Category)的方法会覆盖原类方法?—— 分类方法会在 Runtime 加载时,插入到类的方法列表头部,首次调用时会优先被缓存,后续调用会直接使用分类的方法;
  • 为什么频繁调用不同方法,会导致缓存命中率下降?—— 缓存容量有限,频繁切换方法会导致缓存被覆盖,需要重新查找方法列表。

四、深度解析:class_data_bits_t(bits)的底层实现

如果说 cache_t 是“方法调用的加速器”,那么 bits 就是“类的核心数据仓库”——它存储了类的所有核心信息,包括方法列表、属性列表、协议列表、成员变量列表等,是 Runtime 实现动态特性的核心载体。

bits 的类型是 class_data_bits_t,它本身是一个“64位整数”,低位存储标志位,高位存储指向 class_rw_t 的指针(类的可读写数据),这种设计既能节省内存,又能高效访问数据。

1. class_data_bits_t 结构体源码(精简版)

// bits的类型:存储类数据指针+标志位
struct class_data_bits_t {
private:
    uintptr_t bits; // 64位整数,核心存储载体
    
public:
    // 核心方法:从bits中取出class_rw_t指针(核心数据)
    class_rw_t *data() const {
        // FAST_DATA_MASK:掩码,用于过滤标志位,取出高位的指针地址
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
    
    // 标志位操作方法(示例)
    bool isSwiftLegacy() const { return getBit(FAST_IS_SWIFT_LEGACY); }
    bool isSwiftStable() const { return getBit(FAST_IS_SWIFT_STABLE); }
    
private:
    // 读取指定位置的标志位
    bool getBit(uintptr_t bit) const {
        return (bits & bit) != 0;
    }
};

其中,FAST_DATA_MASK 是关键掩码(ARM64架构下),源码定义如下:

#define FAST_DATA_MASK 0x00007ffffffffff8UL

该掩码的作用是“过滤低位的标志位,保留高位的指针地址”——ARM64架构下,bits 的 bit 346 存储 class_rw_t 指针,bit 02 存储标志位,通过 bits & FAST_DATA_MASK 可快速取出指针。

2. 核心标志位解析(bit 0~2)

bits 的低位(bit 0~2)存储了3个核心标志位,用于标识类的类型和特性,源码定义如下:

  • FAST_IS_SWIFT_LEGACY = 1 << 0(bit 0):是否是旧版 Swift 类(OC 类该标志位为0);
  • FAST_IS_SWIFT_STABLE = 1 << 1(bit 1):是否是新版 Swift 类(OC 类该标志位为0);
  • FAST_HAS_DEFAULT_RR = 1 << 2(bit 2):是否有默认的 retain/release 方法(ARC 环境下,OC 类默认有)。

这些标志位的作用是“快速区分类的类型”,Runtime 在处理方法调用、内存管理时,会根据这些标志位执行不同的逻辑。

3. class_rw_t:bits 指向的核心数据

bits.data() 会返回 class_rw_t 指针,class_rw_t 是“类的可读写数据”结构体,存储了类的方法、属性、协议等核心信息,源码精简如下:

// 类的可读写数据(runtime运行时可修改)
struct class_rw_t {
    // 版本号(用于兼容不同的Runtime版本)
    uint32_t version;
    // 类的flags(标志位,如是否是元类、是否有分类等)
    uint32_t flags;
    
    // 方法列表(存储类的所有方法,包括实例方法和类方法)
    method_array_t methods;
    // 属性列表(存储类的所有属性)
    property_array_t properties;
    // 协议列表(存储类遵循的所有协议)
    protocol_array_t protocols;
    
    // 成员变量列表(存储类的所有成员变量)
    ivar_array_t ivars;
};

其中,method_array_tproperty_array_t 等都是“动态数组”(本质是指针数组),支持 Runtime 运行时动态添加(比如分类添加方法、属性),这也是 OC 支持“动态扩展”的核心原因。

4. bits 的核心工作流程

bits 的工作流程非常简单,核心是“通过掩码取出数据指针,访问类的核心信息”,结合 Runtime 方法查找流程,可总结为:

  1. 当 Runtime 需要查找类的方法时,先通过 objc_class->bits.data() 取出 class_rw_t 指针;

五、三者协同:objc_class / cache / bits 完整工作流程

结合前面的解析,我们用一个“方法调用”的完整流程,串联起 objc_classcachebits 的协同工作,让你彻底理解三者的关联:

  1. 调用 [obj method],OC 编译器将其转化为 Runtime 函数调用 objc_msgSend(obj, @selector(method))

从这个流程可以看出:cache 负责“加速查找”,bits 负责“存储数据”,objc_class 负责“组织关联”,三者协同,构成了 OC 方法调用的底层逻辑,也是 Runtime 动态机制的核心。

六、实战延伸:源码解析的实际应用

很多开发者会问:“搞懂这些源码,对实际开发有什么用?” 其实,Runtime 源码解析的价值,在于“解决底层问题、实现高级特性”,以下是3个常见的实战场景:

1. 解决“方法未实现”崩溃问题

当调用未实现的方法时,会触发 unrecognized selector sent to instance 崩溃。通过理解 cachebits 的查找流程,我们可以通过 Runtime 钩子(如 resolveInstanceMethod),动态添加方法实现,避免崩溃:

// 动态添加未实现的方法
+ (BOOL)resolveInstanceMethod:(SEL)sel {
    if (sel == @selector(unimplementedMethod)) {
        // 动态添加方法实现
        class_addMethod([self class], sel, (IMP)dynamicMethod, "v@:");
        return YES;
    }
    return [super resolveInstanceMethod:sel];
}

// 动态方法实现
void dynamicMethod(id self, SEL _cmd) {
    NSLog(@"动态添加的方法实现");
};

2. 实现“方法交换”(Method Swizzling)

方法交换是 OC 开发中常用的高级技巧,其底层依赖 bits 中的方法列表。通过修改 class_rw_t->methods 中方法的 IMP,可以实现方法交换:

// 方法交换
+ (void)swizzleMethod {
    Class cls = [self class];
    // 获取两个方法的SEL
    SEL originalSel = @selector(originalMethod);
    SEL swizzledSel = @selector(swizzledMethod);
    
    // 获取方法实例
    Method originalMethod = class_getInstanceMethod(cls, originalSel);
    Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);
    
    // 交换方法实现
    method_exchangeImplementations(originalMethod, swizzledMethod);
}

3. 动态添加属性(关联对象)

OC 中不能直接给分类添加属性,但可以通过 Runtime 的关联对象机制实现,其底层依赖 objc_objecthas_assoc 标志位(存储在 isa 中)和 bits 中的相关逻辑:

// 给分类添加关联属性
@interface NSObject (Associated)
@property (nonatomic, copy) NSString *associatedStr;
@end

@implementation NSObject (Associated)
- (NSString *)associatedStr {
    return objc_getAssociatedObject(self, _cmd);
}

- (void)setAssociatedStr:(NSString *)associatedStr {
    objc_setAssociatedObject(self, @selector(associatedStr), associatedStr, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
@end

七、总结:Runtime 核心机制的本质

通过对 objc_classcachebits 的源码解析,我们可以发现:OC Runtime 的核心本质,是“用结构体存储类和对象的信息,用哈希表优化查找效率,用动态数组支持扩展”。

总结三个核心要点,帮你快速掌握本文重点:

  1. objc_class 是类的底层载体,继承自 objc_object,包含 superclasscachebits 三个核心字段,负责组织类的继承关系和核心数据;

理解这些底层源码,不仅能帮你解决实际开发中的底层问题,更能让你从根源上理解 OC 的动态性,为后续学习更高级的 Runtime 特性(如元类、消息转发、分类加载)打下基础。毕竟,只有看透底层,才能真正掌控 OC 开发。