当前位置: 首页 > news >正文

iOS 类与对象底层原理

iOS 类与对象底层原理

文章目录

  • iOS 类与对象底层原理
    • 探索对象本质
    • objc_setProperty 源码
    • cls与类的关联原理
      • 联合体
      • isa的类型isa_t
    • 原理探索
        • initIsa方法
        • 通过setClass方法中的shiftcls来验证绑定的一个流程
        • 通过 isa & ISA_MSAK
        • 通过object_getClass
        • 通过位运算
    • 类&类的结构
      • 从终端调试开始认识调用逻辑
        • 什么是元类
          • 小结
        • NSObject到底有几个?
          • 类存在几份
      • isa走位继承图
      • object_class & objc_object
        • 这里的objc_class 与objc_object有什么关系?
        • objc_object与对象的关系
        • 总结
    • 类结构分析
      • 类信息中有哪些内容
        • 计算cache类的内存大小
        • bits
        • 探索property_list(属性列表)
        • 探索methods_list
        • 从编译期开始介绍class_ro_t,class_rw_t,class_rw_ext_t
        • class_ro_t
        • class_rw_t
          • 区别
        • class_rw_ext_t
    • 总结

探索对象本质

  • 在main中自定义一个类GCObject,有一个属性name
@interface GGObject : NSObject
@property (nonatomic, copy) NSString* name;
@end@implementation GGObject@end

通过终端将main.m编译成main.cpp:

//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 

在编译之后,我们可以开始看这里的类的一个定义:

//NSObject的定义
@interface NSObject <NSObject> {Class isa  OBJC_ISA_AVAILABILITY;
}//NSObject 的底层编译
struct NSObject_IMPL {Class isa;
};struct GGObject_IMPL {struct NSObject_IMPL NSObject_IVARS; // 这里NSString *_name;
};static NSString * _I_GGObject_name(GGObject * self, SEL _cmd) { return (*(NSString **)((char *)self + OBJC_IVAR_$_GGObject$_name)); } //get方法
extern "C" __declspec(dllimport) void objc_setProperty (id, SEL, long, id, bool, bool);static void _I_GGObject_setName_(GGObject * self, SEL _cmd, NSString *name) { objc_setProperty (self, _cmd, __OFFSETOFIVAR__(struct GGObject, _name), (id)name, 0, 1); } //set方法

这里我们可以看到NSObject的定义会产生一个问题,isa的类型居然是Class,这里我们定义alloc放的核心之一的initInstance方法,通过拆看这个方法的源码实现,我们发现在初始化isa指针的时候,是通过isa_t类型初始化的。

这里为了让开发人员更加清晰明确,需要在isa返回的时候做了一个类型强制转换.

这里其实我们可以看出:

  • OC对象的本质就是结构体
  • CGObject中的isa就是继承自NSObject中的isa

objc_setProperty 源码

在这之前我们可以看到这里的上面的一个set方法:

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_setProperty


void objc_setProperty(id self, SEL _cmd, ptrdiff_t offset, id newValue, BOOL atomic, signed char shouldCopy) 
{bool copy = (shouldCopy && shouldCopy != MUTABLE_COPY);bool mutableCopy = (shouldCopy == MUTABLE_COPY);reallySetProperty(self, _cmd, newValue, offset, atomic, copy, mutableCopy); //跳转到真正的一个调用方法
}
static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{if (offset == 0) {object_setClass(self, newValue);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); // 新增retain}if (!atomic) {oldValue = *slot;*slot = newValue;} else {spinlock_t& slotlock = PropertyLocks[slot];slotlock.lock();oldValue = *slot;*slot = newValue;        slotlock.unlock();}objc_release(oldValue); //release 旧值
}

小结

  • objc_setProperty 方法的目的适用于关联上层的set方法以及底层的set方法,其本质就是一接口
  • 这么设计的原因是,上层的set方法很多,如果直接调用底层set方法中,会有很多临时变量。当你吵着一个set的时候非常麻烦
  • 这里其实采用了一种设计模式:适配器设计模式(即将底层接口适配位客户段需要的接口),对外提供一个接口共上层的set方法使用,对内调用底层的set方法。让他们两者互不影响)

image-20250416202436992

cls与类的关联原理

联合体

联合体是指把不同的数据合成一个整体,但是所有变量是互斥的,所有的成员共占一段内存

  • 缺点:包容性弱
  • 优点:成员共用一段内存,是内存的使用更为精细灵活,同时节省了一个内存空间

共用体的所有成员占用同一段内存,修改一个成员影响其他所有成员变量。

结构体各个成员会占用不同的内存,互相之间没有影响。

结构体内存>=所有成员占用的内存总和。

共用体占用的内存等于最大成员占用的内存

isa的类型isa_t

以下isa指针的类型isa_t的定义,从定义中可以看到是通过联合体来定义的。

union isa_t {isa_t() { } // 默认的一个构造函数isa_t(uintptr_t value) : bits(value) { }uintptr_t bits; // 这个和cls是一个互斥类型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()方法操作public:
#if defined(ISA_BITFIELD)struct {ISA_BITFIELD;  // defined in isa.h 这里是一个位域设置一个结构体};bool isDeallocating() {return extra_rc == 0 && has_sidetable_rc == 0; //判断位域对应的一个标志位}void setDeallocating() {extra_rc = 0; has_sidetable_rc = 0;}
#endifvoid setClass(Class cls, objc_object *obj); //提供set方法获取cls指针更加安全Class getClass(bool authenticated); //提供get方法获取cls指针Class getDecodedClass(bool authenticated); 。。
}; //这里是因为C++支持里一个联合体方法,c++11以上支持联合体里面放方法,这里的方法并会被存储到我们的一个代码段,不会影响联合体的一个内存

我们现在来简单分析一个这个isa_t的一个定义可以看出:

  • 提供了两个成员变量 clsbits由联合体的一个定义所知,这里两个成员是互斥的,所以这里有俩种初始化方式:
    • 通过cls初始化
    • 通过bits初始化
  • 这里提供了一个位域的内容,用于存储类信息以及其他信息,结构体的成员ISA_BITFIEld这里我们就看一下他是怎么定义位域的:

有两个版本 __arm64__(对应ios 移动端) 和 __x86_64__(对应macOS)

#   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;                                         \ //是否外挂一个sidetbaleuintptr_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
  1. nonpointer有两个值,表示自定义的类等占1位
    • 0:纯isa指针
    • 1:不只是类对象地址,isa中包含了类对象,对象的引用计数等
  2. has_assoc表示关联对象标志位
    • 0:没有关联对象
    • 1: 存在关联对象
  3. has_cxx_dtor表示是否有C++/OC析构器:类似于dealloc,占一位
    • 如果有则需要做析构逻辑
    • 没有就直接可以释放对象
  4. shifcls白哦是存储类的指针的值
    • arm64占33位
    • x86_64中占44位
  5. magic用于调试器判断当前对象是真的对象还是没有初始化的空间,6位
  6. weakly_refrenced是指对象是否被指向或者曾经指向一个ARC的弱变量
  7. has_sidetable_rc表示 当对象引用计数大于10时,则需要借用该变量存储进位
  8. extra_rc(额外的引用计数) ,表示该对象的引用计数值,实际上是引用计数值减1

isa存储情况

原理探索

我们在之前讲过alloc的一个调用流程,在里面有一个initInstanceIsa,现在我们来看这部分内容:

inline voi
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{ASSERT(!cls->instancesRequireRawIsa());ASSERT(hasCxxDtor == cls->hasCxxDtor());initIsa(cls, true, hasCxxDtor);
}
initIsa方法

这里面也和之前,有一个中间层调用这部分内容,也就是真正开始绑定的一部分内容

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ //这里ASSERT(!isTaggedPointer()); isa_t newisa(0);if (!nonpointer) {newisa.setClass(cls, this); // 初始化cls指针,用之前的那个set方法} else {ASSERT(!DisableNonpointerIsa);ASSERT(!cls->instancesRequireRawIsa());#if SUPPORT_INDEXED_ISA //即isa由cls定义ASSERT(cls->classArrayIndex() > 0);newisa.bits = ISA_INDEX_MAGIC_VALUE;// isa.magic is part of ISA_MAGIC_VALUE// isa.nonpointer is part of ISA_MAGIC_VALUEnewisa.has_cxx_dtor = hasCxxDtor;newisa.indexcls = (uintptr_t)cls->classArrayIndex();
#else //bits指向的流程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_BITnewisa.has_cxx_dtor = hasCxxDtor;
#   endifnewisa.setClass(cls, this);
#endifnewisa.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 instantiationisa = newisa;
}

我们这里是通过InitIsa来绑定isa指针的,但objc838在这里把绑定具体存储类信息的内容放在我们的setClass这个函数中.下面我们来看这部分代吗内容.

clsisa 关联原理就是isa指针中的shiftcls位域中存储了类信息,其中initInstanceIsa的过程是将 calloc 指针 和当前的 类cls 关联起来

通过setClass方法中的shiftcls来验证绑定的一个流程

这里我们接着看setClass这个方法:

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
#   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// We're only signing Swift classes. Non-Swift classes just use// the raw pointeruintptr_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 everythinguintptr_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.
#   endifshiftcls_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 ptrauthshiftcls = (uintptr_t)newCls >> 3; // 这里会有shiftcls赋值的逻辑是将LGPerson进行编码后,右移3位
#endif
}

image-20250419164656131

从上面这张图可以看出我们这里是在shiftcls中存储一个类信息的

运行至newisa.shiftcls = (uintptr_t)cls >> 3 其中shiftcls存储当前类信息部分

  • shiftcls赋值的逻辑是将LGPerson进行编码后,右移3位,这里为什么可以保证后三位没有信息的原因是内存对齐后从二进制层面来说他一定是后三位为000的,(因为结构体内存对齐的一个机制,导致了我们的结构体的二进制后三位一定为0);

这个时候我们再运行会我们的一个initIsa这里的一个创建isa的内容:isa() = newisa;然后观察这里的一个内存的情况:

image-20250419165633798

这里我们可以清楚的看到一个内容这里一个cls完美的指向了我们的一个类.

这里为什么shiftcls为什么会出现一个强制类型转化的原因是,内存不可以存储字符串,机械码只可以识别01这种数字所以才强制类型转化成一个uintptr_t数据类型

在64位的机器上,intptr_t和uintptr_t分别是long int、unsigned long int的别名;在32位的机器上,intptr_t和uintptr_t分别是int、unsigned int的别名。

这里为什么需要向右移动三位,是因为他的前面还有三个位域,需要右移动将其抹零.

主要是由于shiftcls处于isa指针地址的中间部分,前面还有3个位域,为了不影响前面的3个位域的数据,需要右移将其抹零

通过 isa & ISA_MSAK
  • 在方式一后,继续执行,回到_class_createInstanceFromZone方法,此时cls 与 isa已经关联完成,执行po objc

isa指针地址 & ISA_MASK (处于macOS,使用x86_64中的定义),即 po 0x0000000100008358 & 0x00007ffffffffff8 & 0x00007ffffffffff8 ,得出CGObject

在这里插入图片描述

通过object_getClass

查看object_getClass的源码实现,同样可以验证isa与类关联的原理,这里的原理和上面的大致一样,所以笔者就不多赘述了.

通过位运算

我们之前提到过shiftcls只有44位的大小.这44位其实存储了一个类信息,需要经过位运算,将右边三位,和去除44位的位置都抹零

位运算计算过程

所以我们只有这样处理之后也可以得到shiftcls里面保存的是一个类,说明isa已经关联成功了.

类&类的结构

这里主要分析的isa走向和继承关系

在分析之前,我们先定义两个类:

@interface CJLPerson : NSObject
{NSString *hobby;
}
@property (nonatomic, copy) NSString *cjl_name;
- (void)sayHello;
+ (void)sayBye;
@end@implementation CJLPerson
- (void)sayHello
{}
+ (void)sayBye
{}
@end
@interface CJLTeacher : CJLPerson
@end@implementation CJLTeacher
@end

从终端调试开始认识调用逻辑

image-20250419202458850

根图中的p/x 0x0000000100008320 & 0x00007ffffffffff8ULLp/x 0x00000001000082f8 & 0x00007ffffffffff8ULL 中的类信息打印出来都是CJLPerson

前者打印的是person对象的一个isa指针地址,他&后的结果是创建person的类CJLPerson

后者打印的是isa中类信息所指的类的isa的指针地址,即通过CJLPerson类的类的isa地址,在苹果中,我们把一个类的类叫做元类

什么是元类
  • 对象的isa是指向类,类其实也是一个对象,可以叫做类对象.类对象的位域3指向平果定义的元类
  • 元类是系统给的,他的定义和创建都是由编译器玩测的,这个过程中,类的归属来自于元类
  • 元类是类对象的类,每一个类都有独一无二的元类来存储类方法的相国信息
  • 元类本身是没有名字的,只是因为类与之关联,所以直接采用了类的一个名字

这里我们继续用调试来观察

image-20250419203657748

我们可以发现NSObject的元类指向了自己,CJLPerson元类的元类指向的是NSObject,从上面的调试信息我们其实可以得到下面的图片:

image-20250419203823782

小结
  • 对象的isa指向类
  • 类的isa指向元类
  • 元类的isa指向根元类
  • 根元类的isa指向自己
NSObject到底有几个?

其实NSObject只有一个,我们可以打印出NSObject的元类的地址和前面的CJLPerson元类的地址,发现他其实都是同一个,所以可以得出一个结论,内存中只存在一份根元类NSObject,根元类是指向他自己

类存在几份

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

isa走位继承图

关系图示

isa走位

isa的走向有以下几点说明:

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

superclass走位

类之间的继承关系:

  • 类继承自父类superClass
  • 父类继承自根类rootClass
  • 根力类继承自nil,即根类可以理解为万物起源,无中生有

元类也存在继承,元类之间的继承关系如下:

  • 自子类的元类元类继承自父类的元类
  • 父类的元类继承自根元类
  • 根元类继承自根类,这里的根类是指NSObject

实例对象之间没有继承关系,类之间由继承关系

object_class & objc_object

isa走位我们理清楚了,又来了一个新的问题:为什么 对象都有isa属性呢?这里就不得不提到两个结构体类型:objc_class & objc_object

这里我们重新回顾我们的一个NSObject定义的内容:

struct NSObject_IMPL {Class isa;
};
typedef struct objc_class *Class;

其中Class是用isa指针类型,是由objc_class定义的一个类型

objc_class是一个结构体,iOS中兵所有的Class都是以objc_class为模板创建的.

这里我们先认识一下这两个结构体:

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

struct objc_object {Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
};
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 vtableclass_data_bits_t bits;//这里代码太长了就不展示了
这里的objc_class 与objc_object有什么关系?
  • 结构体类型objc_class继承自objec_class类型.其中objc_obejct是一个结构体,他又一个isa属性,所以objc_class也有了isa属性
  • NSObject中的isa在底层是由Class 定义的,其中class的底层编码来自 objc_class类型,所以NSObject也拥有了isa属性
  • NSObject是一个类,用它初始化一个实例对象objc,objc满足objc_object的一个特性,主要还是因为isa是由objc_class继承过来的,而objce_class继承自objc_object,因为objc_object由isa属性,所以每一个对象都有一个isa,isa表示一个指向,来自当前的objc_object.
  • objc_object是当前的根对象,缩由对象都有这样一个特性,即拥有isa属性
objc_object与对象的关系
  • 所有的对象都是以objc_object继承过来的
  • 所有的对象都是来源于NSObject,但是到真正的底层就是一个objc_object的一个结构体类型了

所以可以说对象是继承于objc_object的

总结

所有的对象都是由objc_object继承过来的

所有的对象,类,元类都有isa属性.

简单概括就是万物都是对象,所有都来源于objc_object,有一下两点结论:

  • 以objc_object为模板创建的对象,都有isa
  • 以objc_class为模板创建的类,都有isa

整体关系图示

在结构层面可以通俗的理解为上层OC与底层对接:

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

类结构分析

类信息中有哪些内容

探索类信息中有什么的时.事先我们并不清楚类的结构是什么样,但是我们可以通过类得到一个首地址,然后通过地址平移去获取里面所有的值.

开始认识我们的一个结构体:

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 vtableclass_data_bits_t bits;
  • isa属性:继承自objec_object的isa,占8个字节
  • superclass属性:Class类型,Class实有objc_object定义的,是一个指针
  • cache方法缓存,提高方法的性能
  • bits属性:只有首地址经过上面三尔个属性的内存大小总和的平移,才能获得到bits
计算cache类的内存大小

进入cache类cache_t的定义(只贴出了结构体中非static修饰的属性,主要是因为static类型的属性 不存在结构体的内存中)

struct cache_t {
private:explicit_atomic<uintptr_t> _bucketsAndMaybeMask; //是一个结构体指针类型,占8个字节 explicit_atomic 是将普通指针转换为原子指针的操作,为了线程安全. _bucketsAndMaybeMask 字段通过预处理宏和 buckets() 方法间接实现了 _buckets 的功能 这种设计是为了 优化内存(共享 _bucketsAndMaybeMask 字段)并支持不同架构的掩码存储模式。union {struct {explicit_atomic<mask_t>    _maybeMask; 
#if __LP64__uint16_t                   _flags;
#endifuint16_t                   _occupied;};explicit_atomic<preopt_cache_t *> _originalPreoptCache;};

这个类在LP64位的一个情况应该是只有16个字节,上面包含一个一个结构体指针类型,和一个结构体类型.这个类的结构可以大致的简单理解为下面这个样式:

这里展示一下,这里和objc838有所出入,在objc838版本中把bucket和mask合并了,然后通过掩码的一个方式去访问这部分内存.

image-20250423164953976

bits

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

这里我们可以看到这里的一个中的bits的内容,这里我们先来认识bits中非常重要的一个类别class_rw_t这是一个结构体类型,这里我们呢还没有看到属性列表以及方法列表:

image-20250422191812131

image-20250422195849256

所以我们还需要进一步探索,所以我们找一下这里最重要的内容bits中最重要的两个方法:safe_ro data,后者返回的是class_rw_t前者返回的是class_ro_t

在64位架构CPU下,bits 的第3到第46字节存储 class_rw_t class_rw_t 中存储 flags 、witness、firstSubclass、nextSiblingClass 以及 class_rw_ext_t

在本文的最后几章内容中会讲几个的一个区别

探索property_list(属性列表)

通过查看class_rw_t这个类.我们可以发现这里的一个属性列表和方法列表等多个列表

image-20250422192320343

这里注意一个内容,这里的存储在class_rw_t这个类是存储一个属性的,并不存储一个成员变量成员变量是存储在另一个类中:class_ro_t的这里我们对比看一下这两个类的区别:

image-20250422193310722

这个类也有方法,属性,协议和成员变量.但方法,属性,协议的开头是用base开头的

这里有一个成员变量列表也就是我们这里的ivars,这个列表包含的内容不仅仅包含一个成员变量列表,除了包括在{}中定义的一个成员变量,还包括通过属性定义的成员变量.bits --> data() -->ro() --> ivars通过这个流程来获取成员变量表.

通过@property定义的属性,也会存储在bits属性中,通过bits --> data() --> properties() --> list获取属性列表,其中只包含属性

探索methods_list

这里的methods_list是不存储类方法的,他只存储一个实例方法.那我们的类方法存储在哪里呢?

类方法其实存储在我们的一个元类的方法列表里面:

  • 类的实例方法存储在类的bits属性中,通过bits --> methods() --> list获取实例方法列表,例如CJLPersong类的实例方法sayHello 就存储在 CJLPerson类的bits属性中,类中的方法列表除了包括实例方法,还包括属性的set方法 和 `get方法

  • 类的类方法存储在元类的bits属性中,通过元类bits --> methods() --> list获取类方法列表,例如CJLPerson中的类方法sayBye 就存储在CJLPerson类的元类(名称也是CJLPerson)的bits属性中

从编译期开始介绍class_ro_t,class_rw_t,class_rw_ext_t

首先我们从编译期程序做了什么事情开始介绍:

当类被编译的时候,二进制类在磁盘中的表示如下:

image-20250422222317161

首先是类对象本身,包含最常访问的信息:指向元类(isa),超类(superclass)和方法缓存(cache)的指针,它还具有指向包含更多数据的结构体 class_ro_t 的指针,包含了类的名称,方法,协议,实例变量等等 编译期确定 的信息。其中 ro 表示 read only 的意思。

当类第一次从磁盘加载到内存时,它们总是以这样的形式开始布局的,但是一旦使用它们,就会发生改变:

当类被 Runtime 加载之后,类的结构会发生一些变化,在了解这些变化之前,我们需要知道2个概念:

Clean Memory:加载后不会发生更改的内存块,class_ro_t 属于 Clean Memory,因为它是只读的。
Dirty Memory:运行时会发生更改的内存块,类结构一旦被加载,就会变成 Dirty Memory,因为运行时会向它写入新的数据。例如,我们可以通过 Runtime 给类动态的添加方法。

这里要明确,Dirty MemoryClean Memory 要昂贵得多。因为它需要更多的内存信息,并且只要进程正在运行,就必须保留它。另一方面, Clean Memory 可以进行移除,从而节省更多的内存空间,因为如果你需要 Clean Memory ,系统可以从磁盘中重新加载。

Dirty Memory 是这个类数据 被分成两部分的原因。

对于我们来说,越多的 Clean Memory 显然是更好的,因为它可以 节约更多的内存。我们可以通过分离出永不更改的数据部分,将大多数类数据保留为 Clean Memory,应该怎么做呢?

在介绍优化方法之前,我们先来看一下,在类加载之后,类的结构会变成如何呢?

类分析

image-20250422223702206

在类首次被使用的时候,runtime会为它分配额外的存储容量,用于 读取/写入 数据的一个结构体 class_rw_t

这个结构体用来存储只有在运行时才会生成的新信息比方说所有类都会连接成一个树状结构,这里是通过firstSubclass和newxtSon;欧美化Class指针实现的,这样runtime可以遍历当前使用的所有的类.

这里之所以还会有该类的方法列表和属性列表的信息是因为它们在运行时是可以更改的.当category被加载的时候,它可以想类中添加新的方法.而且程序员也可以通过runtimeAPI动态的添加.

class_ro_t 是只读的,存放的是 编译期间就确定 的字段信息;而class_rw_t 是在 runtime 时才创建的,它会先将 class_ro_t 的内容拷贝一份,再将类的分类的属性、方法、协议等信息添加进去,之所以要这么设计是因为 Objective-C 是动态语言,你可以在运行时更改它们方法,属性等,并且分类可以在不改变类设计的前提下,将新方法添加到类中。

在实际生活中,我们会发现class_rw_t会占用比class_ro_t更多的内存所以诞生了一个新的结构体class_rw_ext_t

大约只有10%左右的类实际会存在动态的一个更改行为,这样减少了一个class_rw_t的大小减小了一半,所以会变成下面这个结构

image-20250423164835020

class_ro_t

这里先展示这个类的样式

struct class_ro_t {uint32_t flags;uint32_t instanceStart;uint32_t instanceSize;
#ifdef __LP64__uint32_t reserved;
#endifunion {const uint8_t * ivarLayout;Class nonMetaclass;};explicit_atomic<const char *> name;WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;protocol_list_t * baseProtocols;const ivar_list_t * ivars;const uint8_t * weakIvarLayout;property_list_t *baseProperties;

这个类我们可以很清楚的看到他有属性列表,方法列表.

这个类其实是在编译期的时候就产生了,他会将本来编译好的内容,放到我们的ro中.

class_rw_t

先展示一下这个结构体的一个样式:

struct class_rw_t {// Be warned that Symbolication knows the layout of this structure.uint32_t flags;uint16_t witness;
#if SUPPORT_INDEXED_ISAuint16_t index;
#endifexplicit_atomic<uintptr_t> ro_or_rw_ext; //按需存储静态或动态元数据,兼顾性能和内存。//作用原子化指针,指向类的 只读元数据(class_ro_t)或 动态扩展数据(class_rw_ext_t)。Class firstSubclass; //指向当前类的  第一个直接子类Class nextSiblingClass; //指向当前类的 下一个兄弟类,与 firstSubclass 共同维护类的 链表结构。

从他两者的一个成员变量其实就可以看出两者的一个区别,这里里面之所以没有属性的一个内容,是因为在iOS14之后属性的部分被放到了我们之前提到的class_rw_ext_t中.

区别

从生成时机的角度来说, ro编译阶段生成,rw运行的时候生成。从存储的内容角度来讲,ro中有方法、属性、协议和成员变量,而rw中并没有成员变量。rw中的方法属性协议的取值方法中,也是通过取ro或者rwe中的值来获得。ro中的方法、属性、协议都是base,也就是只有本类中的方法属性和协议。

class_rw_ext_t

这里我们来看一下这个结构体的一个样式:

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;char *demangledName;uint32_t version;
};

这个结构体存储了class_ro_t methods (方法列表)、properties (属性列表)、protocols (协议列表)等信息。

class_ro_t 中也存储了 baseMethodList (方法列表)、baseProperties (属性列表)、baseProtocols (协议列表) 以及 实例变量、类的名称、大小 等等信息。

但是我们这里要注意一下他的一个创建的前提条件:

  • 使用分类的类
  • 使用runtime API动态修改类的结构的时候

在遇到以上2种情况的时候,类的结构(属性、协议、方法)发生改变,原有的ro(Claer Memory,便宜)已经不能继续记录类的属性、协议、方法信息了,于是系统重新生成可读可写的内存结构rw_ext(Dirty Memory, 比较贵),来存放新的类结构。

这时候我们就很好理解下面这个方法列表了:

image-20250423185611660

const method_array_t methods() const {auto v = get_ro_or_rwe();if (v.is<class_rw_ext_t *>()) { // 判断有没有创建一个re_extreturn v.get<class_rw_ext_t *>(&ro_or_rw_ext)->methods; //如果有就在这里创建} else {return method_array_t{v.get<const class_ro_t *>(&ro_or_rw_ext)->baseMethods}; //如果没有就在ro中读取}}

总结

这是笔者对于iOS类与对象底层原理的一个学习,这里可能会有什么纰漏或者错误,还请不吝指出.

参考博客:

iOS-底层原理 08:类 & 类结构分析

iOS 类的结构分析

类结构中的class_rw_t与class_ro_t

【iOS】类与对象底层探索

相关文章:

  • 深度学习常见框架:TensorFlow 与 PyTorch 简介与对比
  • Postman接口测试: postman设置接口关联,实现参数化
  • 超级创新思路:基于CBAM-Transformer的强化学习时间序列预测模型(Python\matlab实现)
  • 【仿Mudou库one thread per loop式并发服务器实现】服务器边缘测试+性能测试
  • 小结: DHCP
  • Haply MinVerse触觉3D 鼠标—沉浸式数字操作,助力 3D 设计与仿真
  • 【QT】QT多线程
  • MySQL----查询
  • 计算机组成原理系列3--存储系统
  • 【C语言操作符详解(一)】--进制转换,原反补码,移位操作符,位操作符,逗号表达式,下标访问及函数调用操作符
  • 《代码整洁之道》全书归纳
  • SpringMVC 通过ajax 前后端数据交互
  • Scala集合操作与WordCount案例实战总结
  • Linux命令-iostat
  • w~嵌入式C语言~合集6
  • Spring中生成Bean的方式总结-笔记
  • 颠覆传统微商!开源AI智能名片链动2+1模式S2B2C商城小程序:重构社交电商的“降维打击”革命
  • 基于Playwright的浏览器自动化MCP服务
  • Go 语言 核心知识点
  • golang goroutine(协程)和 channel(管道) 案例解析
  • “自己生病却让别人吃药”——抹黑中国经济解决不了美国自身问题
  • 外交部:印度香客赴中国西藏神山圣湖朝圣将于今年夏季恢复
  • 国家发改委:建立实施育儿补贴制度
  • 多地征集农村假冒伪劣食品违法线索,全链条整治“三无”产品
  • 迟来的忍者与武士:从《刺客信条:影》论多元话语的争议
  • 加拿大财长:加拿大需要抗击美国关税