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提供了两个成员变量
bits和cls,代表两种初始化方法。二者共用一块内存,是互斥使用的。
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方法中,cls与isa关联的原理就是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_class和objc_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_class与objc_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列表是两个独立但有关联的数组。
这里有两个链路:
- 获取实例变量列表:
bits -> data() -> ro() -> ivars,这条链路的目标是获取所有属于该对象的成员变量,无论他们是直接定义的还是由属性生成的- 获取属性列表:
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对象,节省内存,运行时采用了延时加载和共享制度数据的策略