【iOS】类与对象底层

iSA与类关联的原理

探索对象本质

假设我们有一个CGObject类,我们在main函数中创建实例,并通过终端使用clang编译命令将main.m文件编译程main.cpp文件

objc 复制代码
//1、将 main.m 编译成 main.cpp
clang -rewrite-objc main.m -o main.cpp

//2、将 ViewController.m 编译成  ViewController.cpp
clang -rewrite-objc -fobjc-arc -fobjc-runtime=ios-13.0.0 -isysroot / /Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator13.7.sdk ViewController.m

//以下两种方式是通过指定架构模式的命令行,使用xcode工具 xcrun
//3、模拟器文件编译
- xcrun -sdk iphonesimulator clang -arch arm64 -rewrite-objc main.m -o main-arm64.cpp 

//4、真机文件编译
- xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc main.m -o main- arm64.cpp 

编译之后,可以看到类的定义如下:

objc 复制代码
//NSObject的定义
@interface NSObject <NSObject> {
    Class isa  OBJC_ISA_AVAILABILITY;//一个指向类对象的指针
}

//NSObject 的底层编译
struct NSObject_IMPL {
	Class isa;
};

struct CGObject_IMPL {
	struct NSObject_IMPL NSObject_IVARS; // //CGObject_IMPL中的第一个属性其实就是isa,继承自NSObject,是伪继承,直接将NSobject结构体定义为CGObject的第一个属性,意味着CGObject拥有NSObject中的所有成员变量
	NSString *_name;
};

static NSString * _I_CGObject_name(GGObject * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_CGObject$_name)); } //get方法,基地址+偏移量 -》 取值
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);

/*
* static:表示这是内部链接,外部不可见。
* _I_...:命名约定,I 代表 Instance method(实例方法)。
* CGObject * self:隐式参数,指向当前对象实例。
* SEL _cmd:隐式参数,代表当前调用的选择器(即方法名 @selector(name))。
*/

static void _I_CGObject_setName_(CGObject * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct CGObject, _name), (id)name, 0, 1); } //set方法
/*
*	 __OFFSETOFIVAR__(struct CGObject, _name):告诉runtime,_name的偏移量
*/

我们可以发现底层编译成struct结构体。

C语言本身是没有继承概念的,OC是通过结构体嵌套实现的。子类结构体的第一个成员必须是父类的完整结构体,内部的NSObject_IVARS的地址、CGObject的地址、NSObject_IVARS的地址完全相同.

因为isa永远在内存偏移量为0的位置,所以无论对象实际是哪一个子类,运行时只要将对象指针强转换为NSObject*并读取第一个字节,就能正确获取isa指针,识别类型。

为什么isa的类型是Class呢?通过学习alloc的实现我们可以知道,isa是通过isa_t类型初始化

在NSObject定义中isa类型是Class,其根本原因是由于isa对外反馈的是类信息,为了让开发人更加清晰明确需要isa返回时做了一个类型强制转换

主要是为了支持Tagged Pointer (把对象数据直接存进指针里,不在分配堆内存)和Non-Pointer ISA

早期的OC中,NSObject的定义确实是:

objc 复制代码
@interface NSObject {
  Class isa;
}

这是一个纯粹的指针,指向类对象。功能单一,并且在处理小对象的时候对空间利用效率低。

于是现代的OC引入了isa_t结构体,即Non-Pointer ISA,这是一个联合体或者结构体。将原本的64位地址空间分开来,高几位存储Class指针,低位存引用计数、标记为等。

++通过学习我们可以知道,OC对象的本质就是结构体。CGObject中的isa就是继承自NSObject的isa++

objc_setProperty源码

我们看看上面的一个set代码:

objc 复制代码
static void _I_GGObject_setName_(GGObject * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct GGObject, _name), (id)name, 0, 1); } //set方法

其中调用了一个objc_setPropery

objc 复制代码
/*
id self: 当前对象实例
SEL _cmd: 当前调用的选择器(即 setter 方法名,如 setName:)。
ptrdiff_t offset: 实例变量(Ivar)在对象内存布局中的偏移量。是为了直接通过指针运算找到内存地址,而不是通过字符串名字查找,效率更高。
id newValue: 要设置的新值。
BOOL atomic: 表示该属性是否声明为 atomic
signed char shouldCopy: 标志位,指示是否需要复制值。
通常值为 0 (NO), 1 (YES/COPY), 或 2 (MUTABLE_COPY)。
*/
void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{
    bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);//对应属性标识符copy
    bool mutableCopy = (shouldCopy == MUTABLE_COPY);
    reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy); //一个内联优化策略,将具体逻辑提取到reallySetProperty中,并根据上一步计算的bool标志位传递参数,这样避免了在核心逻辑中反复判断枚举值
}


static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
    if (offset == 0) {//在OC对象内存布局中,偏移量为0的位置对应的是对象的ISA指针,指向类的原数据。如果偏移量为0说明不是在设置普通实例变量,而是在动态修改对象的类(修改类不涉及常规的retain/release逻辑,类对象本身就是单例且常驻内存)
        object_setClass(self, newValue);//修改ISA指针
        return;
    }

    id oldValue;
    id *slot = (id*) ((char*)self + offset);//计算对象内部存储该属性的具体内存地址即实例变量的地址

    if (copy) {//副本的拷贝
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue); //如果发现当前内存槽位的值和新值是同一个对象指针,那么直接返回。这里的判断非常重要,如果没有,后续release的旧值再retain新值会导致对象被提前释放
    }

  //判断执行原子操作还是非原子操做
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];//获取一个基于内存地址slot的自旋锁,不是锁住对象,而是锁住这个内存槽位。(只保证对单个变量的读写是原子的,并不保证业务逻辑的原子性)
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }

    objc_release(oldValue); //release 旧值(将耗时的release移除临界区,减少锁竞争时间)
}

objc_setProperty方法的目的是用于关联上层set方法以及底层的set方法,本质就是一个接口。这么设计的原因是因为,上层有许多的set方法,如果把底层的reallySetProperty()方法结合到每一个set方法中,带来的开销太大了。其实是涉及了一个适配器设计模式,对外提供一个接口共上层的set方法使用,对内调用底层的set方法。

cls与类的关联原理

联合体

将不同数据合成一个整体,占用一段内存

缺点是包容性弱。但是成员共用一段内存,使得内存使用更为精细,同时节省内存空间。修改一个成员影响其他所有成员

结构体成员之间互相不影响。结构体内存>=所有成员的内存总和。共用体占用的内存等于最大成员占用内存

isa_t类型

OC对象的基石。

objc 复制代码
union isa_t {
    isa_t() { } // 默认的一个构造函数
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits; // 将isa视为一个原始的无符号整数,用于原子操作,快速判断对象是否为空或者特殊状态

private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls; // 这里把cls指针私有化,禁止直接访问cls,必须通过getClass()和setClass()进行解密验证(使用非纯净指针,类的地址可能会位移,getClass可以还原)

public:
#if defined(ISA_BITFIELD)//位域结构体
    struct {
        ISA_BITFIELD;  // defined in isa.h 这里是一个位域设置一个结构体
    };

    bool isDeallocating() {//判断对象是否正在被销毁释放
        return extra_rc == 0 && has_sidetable_rc == 0; //判断位域对应的一个标志位
      // has_sidetable_rc:辅助引用计数表,当引用计数很大时,放不进isa就会放在这个引用计数表中
    }
    void setDeallocating() {//标记对象进入销毁状态,这是一个过程
        extra_rc = 0; 
        has_sidetable_rc = 0;
    }
#endif

    void setClass(Class cls, objc_object *obj); //安全设置对象的isa指针。在现代的runtime中,isa已经不是简单的指针,而是位域结构。通过set存取方法保证位域正确编码,同时不破坏其他标志位
    Class getClass(bool authenticated); //表示是否需要进行指针验证,因为类指针在现代是加密的。先从isa位域中取出class bits,如果auth~位true进行解密计算,返回真实Class指针
    Class getDecodedClass(bool authenticated); //获取解码后的类
}; //这里是因为C++支持里一个联合体方法,c++11以上支持联合体里面放方法,这里的方法并会被存储到我们的一个代码段,不会影响联合体的一个内存

我们可以看出,ias_t提供了两个成员变量bitscls,代表两种初始化方法。二者共用一块内存,是互斥使用的。

objc 复制代码
#   define ISA_BITFIELD         //   __x86_64__                                            \
      uintptr_t nonpointer        : 1; \ //是否开启isa指针开启指针优化                      
      uintptr_t has_assoc         : 1; \//是否有关联对象                       
      uintptr_t has_cxx_dtor      : 1; \ //   是否有C++相关实现                                
      uintptr_t shiftcls          : 44; \/*MACH_VM_MAX_ADDRESS 0x7fffffe00000存储类信息*/ 
      uintptr_t magic             : 6;  \//判断对象是真对象还是未初始化对象
      uintptr_t weakly_referenced : 1;  \//对象是否被指向一个或者曾经指向ARC的一个弱变量
      uintptr_t unused            : 1;  \ //未被使用
      uintptr_t has_sidetable_rc  : 1;  \ //是否外挂一个sidetbale
      uintptr_t extra_rc          : 8 //额外的引用计数
        
define ISA_BITFIELD                //__arm64__                                         \
        uintptr_t nonpointer        : 1;                                       \
        uintptr_t has_assoc         : 1;                                       \
        uintptr_t has_cxx_dtor      : 1;                                       \
        uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ \
        uintptr_t magic             : 6;                                       \
        uintptr_t weakly_referenced : 1;                                       \
        uintptr_t unused            : 1;                                       \
        uintptr_t has_sidetable_rc  : 1;                                       \
        uintptr_t extra_rc          : 19

原理探索

我们先看看下面这段代码:

objc 复制代码
inline void
objc_object::initInstanceIsa(Class lcs, bool hasCxxDtor) {//采用优化版initIsa的方法
	ASSERT(!cls->instanceRequireRawIsa());//断言,限制必须使用优化isa。不能是特殊类
  ASSERT(hasCxxDtor == cls->hasCxxDtor());//检验析构函数标志
  initIsa(cls, true, hasCxxDtor);//真正实现初始化isa的部分
}

我们进入initIsa方法学习下:(也是有中间层调用这部分)

objc 复制代码
inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ //这里
    ASSERT(!isTaggedPointer()); //确保当前对象不是taggedPointer
    
    isa_t newisa(0);//创建一个新的isa结构

    if (!nonpointer) {
        newisa.setClass(cls, this); // 初始化cls指针,用之前的那个set方法
    } else {
        ASSERT(!DisableNonpointerIsa);//确保支持优化isa指针
        ASSERT(!cls->instancesRequireRawIsa());


#if SUPPORT_INDEXED_ISA //即isa由cls定义,用索引表示class
        ASSERT(cls->classArrayIndex() > 0);//每个class在runtime中有唯一索引
        newisa.bits = ISA_INDEX_MAGIC_VALUE;//大概就是设置一个基础模版,索引模式
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
        newisa.has_cxx_dtor = hasCxxDtor;
        newisa.indexcls = (uintptr_t)cls->classArrayIndex();//通过index在index table中查找cls。现在基本不用
#else //bits指向的流程。用压缩指针表示class
        newisa.bits = ISA_MAGIC_VALUE; // 对bits进行一个赋值,压缩指针模式
        // isa.magic is part of ISA_MAGIC_VALUE
        // isa.nonpointer is part of ISA_MAGIC_VALUE
#   if ISA_HAS_CXX_DTOR_BIT
        newisa.has_cxx_dtor = hasCxxDtor;//如果每次释放都去查太慢了,这样直接判断isa的这个成员就行
#   endif
        newisa.setClass(cls, this);
#endif
        newisa.extra_rc = 1;
    }

    // This write must be performed in a single store in some cases
    // (for example when realizing a class because other threads
    // may simultaneously try to use the class).
    // fixme use atomics here to guarantee single-store and to
    // guarantee memory order w.r.t. the class index table
    // ...but not too atomic because we don't want to hurt instantiation
    isa = newisa;
}

这个方法的逻辑主要分为两部分:

  • 通过cls初始化isa
  • 通过bits初始化isa

我们这里是通过initIsa来绑定isa指针的,objc838将具体存储类信息的内容放在setClass方法中,clsisa关联的原理就是isa指针中的shiftcls位域中存储类信息,其中initInstanceIsa过程就是将calloc指针和当前的类cls关联起来

我们看一下setClass方法中的shiftcls来绑定的一个流程

objc 复制代码
//安全的将一个新的类对象newCls设置到某个Objective-C对象的isa指针中
isa_t::setClass(Class newCls, UNUSED_WITHOUT_PTRAUTH objc_object *obj)
{
    // Match the conditional in isa.h.
#if __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
  //编译器支持PAC(指针认证或模拟器)
#   if ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_NONE//不签名
    // No signing, just use the raw pointer.
    uintptr_t signedCls = (uintptr_t)newCls;//直接用原始指针

#   elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ONLY_SWIFT//只给稳定的swift类签名
    // We're only signing Swift classes. Non-Swift classes just use
    // the raw pointer
    uintptr_t signedCls = (uintptr_t)newCls;
    if (newCls->isSwiftStable())
        signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));

#   elif ISA_SIGNING_SIGN_MODE == ISA_SIGNING_SIGN_ALL//全量签名
    // We're signing everything
    uintptr_t signedCls = (uintptr_t)ptrauth_sign_unauthenticated((void *)newCls, ISA_SIGNING_KEY, ptrauth_blend_discriminator(obj, ISA_SIGNING_DISCRIMINATOR));

#   else
#       error Unknown isa signing mode.
#   endif

    shiftcls_and_sig = signedCls >> 3;

#elif SUPPORT_INDEXED_ISA
    // Indexed isa only uses this method to set a raw pointer class.
    // Setting an indexed class is handled separately.
    cls = newCls;

#else // Nonpointer isa, no ptrauth
    shiftcls = (uintptr_t)newCls >> 3; // 这里会有shiftcls赋值的逻辑是将LGPerson进行编码后,右移3位
#endif
}

shiftcls赋值的逻辑是将类进行编码后,右移动三位,由于指针是8位对齐的,所以后三位一定是000,右移后腾出三位用来存储类信息。

运行initIsa之后,可以清楚的看到cls完美指向了我们的类。

shiftcls出现强制转换的原因是因为,内存无法存储字符串,机械码只能识别01这种数字才强制类型转化成一个uintptr_t数据类型。64位机器上,int_ptr和uint_ptr分别是long int、unsigned long int的别名。32位上是int、unsigned int的别名。

通过isa & ISA_MASK

将isa指针的地址&ISA_MASK,即po 0x0000000100008358 & 0x00007ffffffffff8,输出CGObject

通过object_getClass

我们通过使用runtime的api,即object_getClass函数获取类信息

objc 复制代码
Class cls = object_getClass(obj);

本质就是执行了

objc 复制代码
return obj->getIsa();

在这个getIsa()内部主要是判断non-pointer isa。内部自动mask+shift还原

通过位运算

之前提到过在macOS系统中,shiftcls只有44位大小,存储着类信息,需要经过位运算。

手动通过位运算找到shiftcls对应地址,输出cls地址对比(先右移动3位再左移动20位)

类&类的结构

我们先展示一张调试过程图:

图中p/x 0x0100000100008171 & 0x00007ffffffffff8ULL p/x 0x0000000100008148 & 0x00007ffffffffff8ULL的类信息打印出来都是LZclass。前者是打印obj对象的一个isa指针地址,通过&运算后的结果是创建obj的类LZClass。

后者是isa中类信息所指的类的isa的指针地址,即LZclass类的isa地址,即其元类。

什么是元类

  • 对象的isa指向类,类也是一个对象即类对象,类对象的位域指向元类

  • 元类是系统给的,定义和创建都由系统完成,这个过程中,类的归属来自于元类

  • 元类是类对象的类,每个类都有一个独一无二的元类来存储类方法等相关信息

  • 元类本身是没有名称的,由于其与类相关联,所以使用了同类名一样的名称

从图中我们可以看出来:

  • 对象的isa指向类对象
  • 类对象的isa指针指向元类
  • 元类的isa指向根元类,即NSObject
  • 根元类的isa指向他自己

内存中之存在一份根元类NSObject,根元类的元类指向自己

由于类的信息在内存中只存在一份,所以类对象只有一份

isa继承关系图

isa走位

  • 实例对象的isa指向类(class)
  • 类对象的isa指向元类(Meta class)
  • 元类的isa指向根元类(root metal class)
  • 根元类的isa指向自己本身,形成闭环。这里根元类就是NSObject

superclass走位

  • 类之间的继承关系
    • 类继承自父类
    • 父类继承自根类,此时根类是指NSObject
    • 根类继承自nil,所以根类的NSObject差不多就是万物起源
  • 元类也存在继承,关系如下
    • 子类的元类继承自父亲的元类
    • 父亲的元类继承自根元类
    • 根元类继承于根类,此时的根类是指NSObject

objc_class & objc_object

为什么对象和类都有isa属性呢,我们先看看objc_classobjc_object

  • 前面我们说过,NSObject底层编译后是一个NSObject_IMPL结构体。其中含有一个Class类型的isa指针,是由objc_class定义的类型。而objc_class是一个结构体,所有的Class都是以objc_class位模板创建的
objc 复制代码
struct NSObject_IMPL {
	Class* cls;
};
typedef struct objc_class* Class;

我们简单看一下这两个结构体:

objc 复制代码
struct objc_object {
  Class _Nonnull isa OBJC_ISA_AVAILABILITY;//永远位于对象内存地址的起始位置
};//OC中所有实例对象的通用结构体。
objc 复制代码
//描述了类对象的内部结构,当访问一个类的静态信息时,实际上就是在访问这个结构体中的数据
struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY; // 1. 指向元类 (Meta Class)
    
#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE; // 2. 父类 (旧版 runtime)
    const char * _Nonnull name                               OBJC2_UNAVAILABLE; // 3. 类名
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

    // --- 现代 Runtime (Objective-C 2.0, 64-bit) ---
    Class _Nonnull super_class                              OBJC2_AVAILABILITY; // 2. 父类
    const char * _Nonnull name                              OBJC2_AVAILABILITY; // 3. 类名 (注意:这里存的是字符串指针,不是名字本身)
    long version                                            OBJC2_AVAILABILITY;
    long info                                               OBJC2_AVAILABILITY;
    long instance_size                                      OBJC2_AVAILABILITY;
    struct objc_ivar_list * _Nullable ivars                 OBJC2_AVAILABILITY; // 4. 实例变量列表
    struct objc_method_list * _Nullable * _Nullable methodLists                   OBJC2_AVAILABILITY; // 5. 方法列表 (包含 IMP)
    struct objc_cache * _Nonnull cache                      OBJC2_AVAILABILITY; // 6. 方法缓存 (加速查找)
    struct objc_protocol_list * _Nullable protocols         OBJC2_AVAILABILITY; // 7. 遵循的协议列表
    
    // ... 其他字段如数据段 (data()) 等,具体实现可能在 data() 方法中封装
};

objc_class与objc_object有什么关系?

  • objc_class继承自objc_object类型(不是语法意义上的继承,是因为objc_class的第一个成员是一个objc_object类型的变量,结构体指针完全相同)
  • NSObject中的isa底层是由Class定义的,其中class的底层编码来自于objc_class类型
  • 所有对象都有一个objc_object特性,即isa指针

objc_oebjct与对象的关系

  • 所有对象的内存头部都有一个objc_object类型的结构体指针变量
  • 所有对象都是来源于NSObject,底层就是一个objc_object结构体类型

可以理解为上层OC于底层对接

  • 下层就是通过结构体定义的模板,例如objc_classobjc_object
  • 上层就是通过底层模版创建的一些类型

类结构分析

类信息中有哪些内容

在最新版的objc_class的定义中,有以下几个属性:

objc 复制代码
struct objc_class : objc_object {
    // Class ISA; //8字节
    Class superclass; //Class 类型 8字节
    cache_t cache;             // formerly cache pointer and vtable
    class_data_bits_t bits;    // class_rw_t * plus custom rr/alloc flags
    
    //....方法部分省略,未贴出
}
  • isa属性继承自objc_object的isa,占8字节
  • superclass属性:是由objc_class定义的
  • cache方法缓存,提高方法性能
  • bits属性:首地址经过上面三个属性的内存大小总和的平移才能获取到bits

计算cache内存大小

cache字段:极速查找、线程安全、内存紧凑、欲优化支持(支持dyld共享缓存中的欲优化,即启动前就将常用方法的cache填好,直接映射到进程内存,无需初始化)

进入cache类cache_t的定义(只贴出来非static修饰的属性)

objc 复制代码
struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {//两个成员在互斥条件下使用
        struct {
            explicit_atomic<mask_t>    _maybeMask;//存储当前哈希表掩码
#if __LP64__
            uint16_t                   _flags;//存储缓存的状态标志。
#endif
            uint16_t                   _occupied;//记录当前缓存表中已占用槽位数量
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;//是否启动欲优化缓存模式
    };

上面的代码我们可以知道,通过联合体实现了两种缓存模式的切换,节省内存并提高了灵活性。

这个类在LP64位的环境下应该是16个字节,上面包含一个一个结构体指针类型和一个结构体类型,这个类的结构可以大概分为下面这种样子:

bits

封装了类的其他信息,例如成员变量、方法列表、协议、属性等

bits的数据结构如下:

objc 复制代码
struct class_data_bits_t {
    friend objc_class;//声明 objc_class 是 class_data_bits_t 的友元(只有objc_class类内部的方法可以直接访问bits成员或者其他私有成员,外部代码必须通过公开方法data()或safe_ro())访问数据
    // Values are the FAST_ flags above.
    uintptr_t bits;//低位存类当前状态,高位存实际数据结构地址
    public:
    class_rw_t* data() const {//提取并返回class_rw_t的指针(是类在运行时的数据结构,包含可修改信息,当类被实现之后(realized,bits指向这个数据结构)
        return (class_rw_t *)(bits & FAST_DATA_MASK);//位运算清除低位的标志位
    }
    // Get the class's ro data, even in the presence of concurrent realization.
    // fixme this isn't really safe without a compiler barrier at least
    // and probably a memory barrier when realizeClass changes the data field
    const class_ro_t *safe_ro() const {//安全获取只读数据
        class_rw_t *maybe_rw = data();
        if (maybe_rw->flags & RW_REALIZED) {
            // maybe_rw is rw
            return maybe_rw->ro();//内部持有calss_ro_t指针,获取只读数据
        } else {
            // maybe_rw is actually ro
            return (class_ro_t *)maybe_rw;
        }
    }
}

//这里又有一个线程安全的问题,记忆

在OC的运行时中,一个类从磁盘加载到内存完全可用,经历两个主要阶段:

  • 未实现(unrealized):此时只有静态编译产生的只读数据(class_ro_t),为了省内存,运行时直接讲bits指向class_ro_t的地址(此时低位标志位表明其还没实现)
  • 已实现(realized):当第一次使用该类时,运行时会讲class_ro_t中的数据复制/迁移到class_rw_t结构中,以便于后续动态修改。此时bits指向calss_rw_t。并设置标志位以实现

在64位系统下,bits的内存分布如下:

位范围 (Bit Index) 用途 说明
0 - 2 (低3位) FAST Flags 存储状态标志,如 FAST_REALIZED, FAST_HAS_DEFAULT_RR 等。
3 - 63 (高61位) Data Pointer 存储 class_rw_t (或 class_ro_t) 的内存地址。

Clean Memory:加载后不会发生更改的内存块, class_ro_t属于Clean Memory

Dirty Memory:运行时会发生更改的内存块,类结构一旦被加载,就会变成Dirty Memory,因为运行时会向他写入新的数据。

Dirty Memory 比 Clean Memory要昂贵的多,因为它需要更多的内存信息,并且只要进程正在运行,就必须保留它,Clean Mrmory可以移除,需要的时候再加载就行了

class_ro_t

objc 复制代码
struct class_ro_t {
    uint32_t flags;               // 1. 标志位(存储类的静态属性)
    uint32_t instanceStart;       // 2. 实例变量起始偏移量
    uint32_t instanceSize;        // 3. 实例对象的大小(不含 isa 指针)
    
    // 4. 保留字段/对齐填充 (在 ARM64 等架构上可能用于其他用途或仅为对齐)
    #ifdef __LP64__
    uint32_t reserved;            
    #endif

    // 5. 指向只读数据的指针数组 (关键部分)
    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) - 注意:旧版本可能没有这个,或者位置不同
    
    // 注意:在现代源码中,为了兼容性,这些指针通常被封装在 union 或通过 offset 访问
    // 但逻辑上它们指向的是具体的 list_t 结构体。
};

有些类可能永远不需要动态扩展,如果没有class_ro_t,每个类一加载就必须分配可写的class_rw_t浪费内存,有了class_ro_t之后,未使用的类只占用只读段的静态空间,只有真正需要动态能力时才分配堆内存
这里还有一个ivars实例变量列表,我们区分一下实例变量(instance variable)和属性(properties)。

  • ivar是对象内存的一部分,实际存储数据的槽位
  • property是访问这些数据的接口

ivar列表和property列表是两个独立但有关联的数组。

这里有两个链路:

  1. 获取实例变量列表:bits -> data() -> ro() -> ivars,这条链路的目标是获取所有属于该对象的成员变量,无论他们是直接定义的还是由属性生成的
  2. 获取属性列表:bits -> data() -> properties() -> list,这条链路的目标是获取仅有@property声明的属性元数据

class_rw_t

objc 复制代码
struct class_rw_t {
    // 1. 基础信息 (固定存在)
    uint32_t flags;                   // 运行时标志 (如 RW_REALIZED, RW_CONSTRUCTING)
    uint32_t version;                 // 版本号
    const class_ro_t *ro;             // 指向只读数据 (class_ro_t) 的指针

    // 2. 核心数据数组 (通过 method_array_t 等封装,支持动态扩容)
    method_array_t methods;           // 方法列表 (包含原始方法和分类添加的方法)
    property_array_t properties;      // 属性列表
    protocol_array_t protocols;       // 协议列表

    // 3. 关联对象和扩展数据 (关键点!)
    Class firstSubclass;              // 第一个子类 (用于遍历继承树)
    Class nextSiblingClass;           // 下一个兄弟类 (用于遍历同一父类的其他子类)
    
    // 注意:在较新的源码中,为了节省内存,上述 subclass/sibling 以及更多扩展数据
    // 可能被移到了 class_rw_ext_t 中,通过指针间接访问。
    // 但逻辑上它们属于 rw 的可写部分。
    
    // 4. 扩展结构体指针 (懒加载)
    class_rw_ext_t *ext;              // 指向扩展数据的指针 (如果数据太多或需要额外字段)
};

class_rw_ext_t

一个扩展结构体,用于存放不常用或可选的数据,以实现内存节省(class_rw_t中有一个指针指向它)

objc 复制代码
struct class_rw_ext_t {
    const class_ro_t *ro;     // 有时 ro 也会放在这里 (取决于版本)
    Class firstSubclass;      // 第一个子类
    Class nextSiblingClass;   // 下一个兄弟类
    const char *demangledName;// 类的修饰名 (用于 Swift/C++ 交互)
    // 其他动态添加的元数据...
};

属性列表

通过前面对class_rw_t的学习,我们可以发现其内部存在的放的属性列表和方法列表等:

objc 复制代码
const method_array_t methods() const {
    // 1. 获取当前的底层数据存储对象
    // get_ro_or_rwe() 返回一个 variant 类型的对象 v,它内部封装了 ro 或 rwe 指针
    auto v = get_ro_or_rwe();

    // 2. 判断当前处于哪种状态
    // v.is<class_rw_ext_t *>() 检查是否已经扩展为读写模式
    if (v.is<class_rw_ext_t *>()) {
        // --- 情况 A: 已扩展 (Read-Write) ---
        // 如果已经扩展,直接从 class_rw_ext_t 结构中获取完整的方法列表。
        // 这个列表通常包含了原始的只读方法 + 动态添加的方法。
        return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods;
    } else {
        // --- 情况 B: 未扩展 (Read-Only) ---
        // 如果还未扩展,说明类处于初始状态。
        // 直接从只读的 class_ro_t 结构中获取基础方法列表 (baseMethods)。
        // 这里构造了一个临时的 method_array_t 包装器来返回结果。
        return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods};
    }
}

    const property_array_t properties() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->properties;
        } else {
            return property_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProperties};
        }
    }

    const protocol_array_t protocols() const {
        auto v = get_ro_or_rwe();
        if (v.is<class_rw_ext_t *>()) {
            return v.get<class_rw_ext_t *>(&ro_or_rw_ext)->protocols;
        } else {
            return protocol_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseProtocols};
        }
    }

在现代的运行时系统中,class_t结构体不再直接包含所有数据,而是指向一个class_rw_t对象,节省内存,运行时采用了延时加载和共享制度数据的策略

相关推荐
T1an-12 小时前
最右IOS岗一面
ios
踏着七彩祥云的小丑3 小时前
Mac——已安装工具查找
macos
小红的布丁3 小时前
公网 IP、私网 IP、路由表、转发表与 MAC 地址的关系
tcp/ip·macos·智能路由器
Lecxcy_Kastreain4 小时前
如何自适应 MacOS
macos
坏小虎5 小时前
Expo 快速创建 Android/iOS 应用开发指南
android·ios·rn·expo
光影少年6 小时前
Android和iOS原生开发的基础知识对RN开发的重要性,RN打包发布时原生端需要做哪些配置?
android·前端·react native·react.js·ios
北京自在科技6 小时前
Find My 修复定位 BUG,AirTag 安全再升级
ios·findmy·airtag
简单点了6 小时前
mac安装node环境
macos
简单点了6 小时前
mac安装vm装win11虚拟机
macos
Digitally7 小时前
如何不用 USB 线将 iPhone 照片传到电脑?
ios·电脑·iphone