【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对象,节省内存,运行时采用了延时加载和共享制度数据的策略

相关推荐
sp42a2 小时前
NativeScript iOS 平台开发技巧
ios·nativescript·app 开发
2501_915921432 小时前
常用iOS性能测试工具大全及使用指南
android·测试工具·ios·小程序·uni-app·cocoa·iphone
for_ever_love__2 小时前
Objecgtive-C学习实例对象,类对象, 元类对象与 isa指针
c语言·学习·ios
一招定胜负3 小时前
视频转写+LLM分析:课堂录音自动化处理实现
macos·ios·xcode
坚果派·白晓明3 小时前
在 macOS 中搭建鸿蒙 PC 三方库交叉编译开发环境
macos·华为·harmonyos
2501_915918413 小时前
有没有Xcode 替代方案?在快蝎 IDE 中完成 iOS 开发的过程
ide·vscode·ios·个人开发·xcode·swift·敏捷流程
blackorbird4 小时前
通过攻陷合法网站传播的新型iOS漏洞利用工具包DarkSword
macos·ios·objective-c·cocoa
for_ever_love__6 小时前
Objective-C学习 NSSet 和 NSMutableSet 功能详解
开发语言·学习·ios·objective-c
ricky_fan18 小时前
(OpenAI)Codex 安装、部署使用方式
python·macos·conda·vim