【iOS】源码学习-类的结构分析

【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昂贵得多。原因在于:

  1. 因为它需要更多的内存信息,并且只要进程在运行时,就必须保留它。
  2. 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{};
    }
相关推荐
ii_best1 小时前
ios/安卓脚本工具开发按键精灵脚本常见运行时错误与解决方法
android·ios·自动化
MonkeyKing71551 小时前
iOS 开发 内存泄漏常见场景及检测方案
ios·面试
小新同学^O^1 小时前
简单学习--> 神经网络
人工智能·python·神经网络·学习
沉浸式学习ing1 小时前
音视频内容怎么快速消化?视频转思维导图+精华速览的方法
人工智能·学习·ai·音视频·知识图谱·xmind
UnicornDev2 小时前
从零开始学iOS开发(第四十五篇):SwiftUI 数据可视化进阶 —— 构建交互式图表与仪表盘
ios
楼田莉子3 小时前
仿Muduo的高并发服务器:Http协议模块
linux·服务器·c++·后端·学习
AI机器学习算法10 小时前
《动手学深度学习PyTorch版》笔记
人工智能·学习·机器学习
贺一航【Niki】10 小时前
【学习笔记】杂乱知识
笔记·学习
白雪茫茫11 小时前
监督学习、半监督学习、无监督学习算法详解
python·学习·算法·ai