【iOS】源码阅读(四)——isa与类关联的原理

发布于:2025-05-16 ⋅ 阅读:(15) ⋅ 点赞:(0)

前言

  本篇文章主要是笔者在学习和理解类与isa的关联关系时所写的笔记。

OC对象本质探索

  在学习和理解类与isa的关联关系之前,我们先来探寻一下OC对象的本质。探寻其本质,我们首先要了解一个编译器:clang。

clang

  Clang 是一个开源的 C/C++/Objective-C/Objective-C++ 编译器,属于 LLVM(Low Level Virtual Machine) 项目的一部分。它被设计为 GCC(GNU Compiler Collection) 的替代品,提供更快的编译速度、更低的内存占用、更清晰的错误提示以及更好的兼容性。
  这个编译器是xcode自带的,主要是用于底层编译,将一些文件``输出成c++文件,例如main.m 输出成main.cpp,其目的是为了更好的观察底层的一些结构 及 实现的逻辑,方便理解底层原理。

探索对象本质

  在main文件中自定义一个GGObject类,并声明一个name属性。
在这里插入图片描述
  通过终端,利用clang将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 

在这里插入图片描述
  然后我们打开编译好的main.cpp文件,找到关于GGObject的定义,我们能发现GGObject在底层会被编译成struct结构体。

GGObject的底层编译:
在这里插入图片描述

struct GGObject_IMPL {
    struct NSObject_IMPL NSObject_IVARS;
    //定义一个自定义类 GGObject 的底层实现(通常对应 .m 文件中的类扩展);_IMPL 后缀表明这是类的私有实现结构体,用于组织实例变量
    //嵌入 NSObject 的实例变量结构体(通常是私有定义,如 isa 指针);NSObject_IVARS 表示这是 NSObject 的实例变量集合。

    NSString *_name;
};

  从上述代码中我们能看出来,GGObject_IMPL中的第一个属性是NSObject_IMPL结构体,从名称上来看,这貌似是继承自NSObject的。我们找到关于NSObject的定义及底层编译来进一步探索。

NSObject的定义:
在这里插入图片描述

@interface NSObject <NSObject> {  // 声明 NSObject 的类接口
#pragma clang diagnostic push    // 保存当前编译器警告状态
#pragma clang diagnostic ignored "-Wobjc-interface-ivars"  // 禁用 "接口中声明实例变量" 警告
    Class isa  OBJC_ISA_AVAILABILITY;  // 声明一个名为 isa 的实例变量
#pragma clang diagnostic pop     // 恢复之前的警告状态
}

NSObject的底层编译:
在这里插入图片描述
  由上述,我们可以知道,GGObject_IMPL中的第一个属性其实就是 isa,是继承自NSObject,属于伪继承,伪继承的方式是直接将NSObject结构体定义为LGPerson中的第一个属性,意味着GGObject 拥有 NSObject中的所有成员变量。GGObject中的第一个属性 NSObject_IVARS 等效于 NSObject中的 isa。

GGObject类的完整定义如下:
在这里插入图片描述
  通过上述探索,我们了解了OC对象的本质,同时我们产生了一个疑问:为什么NSObject_IMPL中的isa的类型为class?
  在之前探索alloc实现过程时,提及过alloc方法的核心之一的initInstanceIsa方法,通过查看这个方法的源码实现,我们发现,在初始化isa指针时,是通过isa_t类型初始化的,而在NSObject定义中isa的类型是Class,其根本原因是由于isa 对外反馈的是类信息,为了让开发人员更加清晰明确,需要在isa返回时做了一个类型强制转换。这里在源码中代码如下:
在这里插入图片描述

  另外,关于class类型的本质我们可以在objc.h文件中找到对应的定义:

typedef struct objc_class *Class;

这里我们可以知道,它是指向objc_class结构体的指针,存储类的元数据(即方法列表、属性列表、协议等)。

而当isa是class类型时,它直接指向对象的类:

Class cls = object_getClass(object);

总结
由以上我们可以得出:

  1. OC对象的本质就是结构体。
  2. GGObject中的isa是继承于NSObject中的isa。

objc_setProperty源码探索

  在前面探索GGObject源码的时候,我们还发现了我们声明的name属性对应的set和get方法在底层的代码实现,其中set方法的实现依赖于runtime中的objc_setProperty:
在这里插入图片描述
我们通过xcode自带的搜索功能可以进一步对objc_setProperty源码进行探索:
在这里插入图片描述
然后我们通过command+点击,可以进入reallySetProperty的实现代码:

static inline void reallySetProperty(
    id self, //对象实例
    SEL _cmd, //setter方法名(未直接使用)
    id newValue, //新属性值
    ptrdiff_t offset, //属性在对象内存中的偏移量
    bool atomic, //是否为原子操作(决定是否加锁)
    bool copy, //是否用copy修饰
    bool mutableCopy //是否用mutableCopy修饰
    //copy和mutableCopy决定是否需要对newValue执行拷贝
)
{
    if (offset == 0) { //offset == 0 表示操作的是对象的 isa 指针(isa 是对象的第一个成员,偏移量为 0)
        object_setClass(self, newValue); //调用 object_setClass 动态修改对象的类(返回值为对象原来的类)
        //obj:要修改类的对象(实例)
        //cls:目标类(必须是有效的 objc_class)
        //这里将对象 obj 的 isa 指针从原来的类改为 cls;不修改对象的内存布局,仅改变类信息,不影响实例变量(ivar)的存储
        return;
    }

    id oldValue;
    //计算属性内存地址
    id *slot = (id*) ((char*)self + offset);
    //slot:指向属性在对象内存中的地址
    //(char*)self:将对象指针转为字节指针(便于偏移计算)
    //+ offset:跳转到属性的实际存储位置
    
    //处理copy/mutableCopy修饰
    if (copy) {
        newValue = [newValue copyWithZone:nil];
    } else if (mutableCopy) {
        newValue = [newValue mutableCopyWithZone:nil];
    } else {
        if (*slot == newValue) return;
        newValue = objc_retain(newValue);
    }

    //非原子性处理(不加锁,直接赋值,这样会更高效)
    if (!atomic) {
        oldValue = *slot; //保存旧值
        *slot = newValue; //直接赋值
    } else {
        //原子性处理(通过自旋锁,即spinlock_t保证线程安全)
        spinlock_t& slotlock = PropertyLocks[slot]; //PropertyLocks 是一个全局锁表,每个属性地址对应一个锁
        slotlock.lock(); //加锁
        oldValue = *slot;
        *slot = newValue; //赋值
        slotlock.unlock(); //解锁
    }
    //释放酒值(调用 objc_release 减少旧值的引用计数)
    objc_release(oldValue);
}

所以通过上述代码,我们可以总结出其调用链:

  1. 编译器生成 setName: 方法,调用 objc_setProperty。
  2. objc_setProperty 内部调用 reallySetProperty。
  3. reallySetProperty 根据修饰符(copy/atomic)处理赋值。

总结

  • objc_setProperty方法是 Objective-C 运行时提供的 统一属性设置入口,本质上是一个 适配层(Adapter),负责桥接编译器生成的属性设值方法(Setter)与底层内存管理操作。(即适用于 关联上层的set方法 以及 底层的set方法,其本质就是一个接口)
  • 这么设计的原因是,为了进行接口统一化和隔离变化。
  • 基于上述原因,苹果采用了适配器设计模式(即将底层接口适配为客户端需要的接口),对外提供一个接口,供上层的set方法使用,对内调用底层的set方法,使其相互不受影响,即无论上层怎么变,下层都是不变的,或者下层的变化也无法影响上层,主要是达到上下层接口隔离的目的

(1) 接口统一化
问题:编译器为每个 @property 生成独立的 Setter 方法,若直接调用底层内存操作(如 reallySetProperty),会导致:
二进制体积膨胀(每个 Setter 都包含重复的内存管理代码)。
难以集中优化(如原子性、拷贝逻辑的处理)。
解决方案:通过 objc_setProperty 收敛所有属性设值逻辑,成为唯一入口。
(2) 隔离变化
上层(编译器生成的 Setter):只需关注如何传递参数(如属性偏移量、修饰符标志)。
下层(内存管理):只需处理标准化后的参数(如 atomic/copy 标志),无需关心调用来源。

请添加图片描述
上述代码中,我们看到关于分别用copy和mutableCopy修饰的底层逻辑,笔者在这里再根据之前所学进行一下回顾:

copy 修饰符

当属性声明为 copy 时,reallySetProperty 会调用 [newValue copyWithZone:nil]。生成的是 不可变副本(即使 newValue 是可变对象)

eg:

@property (nonatomic, copy) NSString *name;

如果传入 NSMutableString,会被转为 NSString:

NSMutableString *mutableName = [NSMutableString stringWithString:@"Alice"];
obj.name = mutableName; // 实际存储的是不可变的 @"Alice" 副本
[mutableName appendString:@"Bob"]; // 不影响 obj.name(仍是 @"Alice")

当我们需要确保属性值不被外部修改时(如 NSString、NSArray、NSDictionary 等),可以使用copy来防止传入可变对象后,外部修改导致属性值意外变化。

mutableCopy 修饰符
当属性声明为 mutableCopy 时,reallySetProperty 会调用 [newValue mutableCopyWithZone:nil]。生成的是可变副本(即使newValue是不可变对象)

eg:

@property (nonatomic, mutableCopy) NSMutableArray *items;

如果传入 NSArray,会被转为 NSMutableArray:

NSArray *immutableItems = @[@"A", @"B"];
obj.items = immutableItems; // 存储的是可变的副本
[obj.items addObject:@"C"]; // 允许修改

当我们需要属性值可被内部修改时(较少使用,通常直接声明为 NSMutableArray 类型)可以使用mutableCopy。

默认行为(无 copy/mutableCopy)

若未指定 copy 或 mutableCopy,则直接 保留(retain) 新值:

newValue = objc_retain(newValue);

新旧值相同时优化:如果 *slot == newValue,直接返回,避免无谓操作。

请添加图片描述
在刚刚 reallySetProperty 的源码中:

if (copy) {
    newValue = [newValue copyWithZone:nil]; // 生成不可变副本
} else if (mutableCopy) {
    newValue = [newValue mutableCopyWithZone:nil]; // 生成可变副本
} else {
    newValue = objc_retain(newValue); // 默认 retain
}

其中的copyWithZone: 和 mutableCopyWithZone:是 NSCopying 和 NSMutableCopying 协议的方法,由对象类实现。
eg:NSString的copy返回自身(不可变),mutableCopy返回NSMutableString。

copy和mutableCopy的内存管理规则

请添加图片描述

实际应用

  1. 优先使用copy修饰不可变对象:
@property (nonatomic, copy) NSString *text;
@property (nonatomic, copy) NSArray *items;

避免传入 NSMutableString/NSMutableArray 后,原属性值被外部修改。

  1. 谨慎使用mutableCopy修饰不可变对象:
    除非明确需要可变副本,否则直接用 NSMutableArray 等类型。
  2. 自定义类支持copy:
    实现NSCopying协议:
@interface Person : NSObject <NSCopying>
@end

@implementation Person
- (id)copyWithZone:(NSZone *)zone {
    Person *copy = [[Person alloc] init];
    copy.name = self.name; // 深拷贝或浅拷贝按需实现
    return copy;
}
@end

了解到这里经常会涉及到一个经典问题:为什么NSString要用copy修饰?
这是为了防止可变字符串篡改:

NSMutableString *mutableName = [NSMutableString stringWithString:@"Alice"];
self.name = mutableName; // 若用 strong,mutableName 修改会影响 self.name
[mutableName appendString:@"Bob"];
// 若 name 是 copy,self.name 仍是 @"Alice";若 strong,会变成 @"AliceBob"

cls与类的关联原理

在这里我们有两个疑问:为什么isa的类型是isa_t ?initInstanceIsa是如何将cls与isa关联的?
首先,我们可以在objc-private.h文件中找到关于isa的类型isa_t的定义源码:

#include "isa.h"

union isa_t { //联合体
    //构造函数
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;//存储完整的 isa 值(64 位整数)

//私有成员:类指针(通过方法访问)
private:
    // Accessing the class requires custom ptrauth operations, so
    // force clients to go through setClass/getClass by making this
    // private.
    Class cls;//将 cls 设为私有进行直接访问限制,强制外部代码通过 setClass/getClass 方法访问。
    
//这里的bits与cls是互斥关系

public:
#if defined(ISA_BITFIELD)
    //条件编译:位域结构(由isa.h定义)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

//內联引用计数相关方法(仅当ISA_HAS_INLINE_RC启用时)
#if ISA_HAS_INLINE_RC
    bool isDeallocating() const {
        return extra_rc == 0 && has_sidetable_rc == 0;
    }
    void setDeallocating() {
        extra_rc = 0;
        has_sidetable_rc = 0;
    }
#endif // ISA_HAS_INLINE_RC

#endif

    //公共方法:安全访问类指针
    void setClass(Class cls, objc_object *obj);
    Class getClass(bool authenticated) const;
    Class getDecodedClass(bool authenti

为什么说bits与cls为互斥关系

这里就涉及到Union的内存共享特性:
​​内存复用​​:union 的所有成员共享同一块内存,大小由最大的成员决定。
​​互斥性​​:同一时间只能使用一个成员。
eg:我们现在对Union的内存复用和互斥性进行代码实感

union Example {
    int a;
    float b;
};
Example u;
u.a = 42;  // 此时 u.b 的值是未定义的
u.b = 3.14; // 此时 u.a 的值被覆盖
NSLog(@"u.a = %d\nu.b = %f", u.a, u.b);

这里的输出如下:

在这里插入图片描述
为什么会出现这种情况呢?我们来逐行解析一下以上代码:

  1. 首先我们给u.a赋值为42,这是内存中存储的是整数值42的二进制表示。
  2. 然后给u.b赋值为3.14,这时同一块内存被浮点数3.14的二进制表示覆盖。

u.a显示为1078523331,这是因为当我们给u.b赋值为3.14时,这块内存被重新解释为整数,其整数值就是1078523331,而为什么这里3.14对应的整数值是1078523331,这里需要大概了解一下IEEE 754浮点数表示法:
我们就以3.14为例:
符号位:0(正数)
指数:128(二进制为10000000,实际指数为128-127=1)
尾数:100100011110101110000101(3.14的二进制,去掉前导1之后)

将这些二进制位组合起来得到的32位表示是:
0 10000000 100100011110101110000101
将其解释为无符号整数,得到的就是1078523331。

小结
联合体中所有成员共享相同的内存地址,所以当你通过一个成员修改内存内容时,其他所有成员的值都会被改变,因为它们查看的是同一块内存。

isa的类型isa_t

  通过上面isa的类型isa_t的定义源码,我们可以得知,其是通过联合体定义的。为什么要通过联合体而非结构体进行定义呢?
这主要是为了内存优化​​,使用联合体定义来共享内存空间​​。

​​联合体特性​​:所有成员共享同一块内存,大小由最大成员决定。
​​isa_t 的需求​​:需要同时存储 ​​类指针​​(Class)和 ​​位域​​(如引用计数、标记位等),但无需同时使用它们。
​​内存节省​​:若用结构体(struct),每个成员独立占用内存;而联合体复用内存,避免冗余。

  另外,我们从isa_t的定义中还可以知道,这里提供了两个成员,即 cls 和 bits ,由于这两者是互斥的,这就意味着,当初始化 isa 指针时,有两种初始化方式:

  • 通过 cls 初始化(设置类指针):

调用 setClass 方法,修改 bits 中的类指针字段(高位),保留其他位域(如引用计数)的原始值。仅修改 bits 中类指针对应的位,其他位域(如 extra_rc)​​未被初始化​​,保持未定义的垃圾值。

初始化结果:
cls(通过 getClass 方法解密后)是有效的类指针。
bits 的其他字段(如引用计数)​​无默认值​​(可能是随机值)。
请添加图片描述

  • 通过 bits 初始化(直接操作内存位域):

直接赋值 bits,覆盖所有字段(包括类指针和位域)。如果赋值的 bits 包含有效的类指针(加密后)和位域,则 cls 会同步更新;如果 bits 未正确设置类指针,则 cls 可能为无效值。
​​
初始化结果​​:
bits 的所有字段(包括类指针)被显式设置。
cls 的值由 bits 中对应的位决定:如果 bits 包含有效类指针,则 cls 有效;否则为无效值(非默认值)。
请添加图片描述

所以,我们通过cls初始化,bits无默认值;通过bits进行初始化,cls有默认值。

上面提到两种初始化方法时,一直在提到位域,这到底是什么,在isa_t定义源码中提供了一个结构体定义的位域内容:

public:
#if defined(ISA_BITFIELD)
    //条件编译:位域结构(由isa.h定义)
    struct {
        ISA_BITFIELD;  // defined in isa.h
    };

其中,这里的结构体成员ISA_BITFIELD是一个宏定义,我们具体来看一下这里面都有什么内容:

/*_arm64(对应ios移动端)*/ 
#     define ISA_MASK        0x0000000ffffffff8ULL
#     define ISA_MAGIC_MASK  0x000003f000000001ULL
#     define ISA_MAGIC_VALUE 0x000001a000000001ULL
#     define ISA_HAS_CXX_DTOR_BIT 1
#     define ISA_BITFIELD                                                     \
        uintptr_t nonpointer        : 1; /*是否使用非指针格式*/                                      \
        uintptr_t has_assoc         : 1; /*是否有关联对象*/                                      \
        uintptr_t has_cxx_dtor      : 1; /*是否有C++析构函数*/                                      \
        uintptr_t shiftcls          : 33; /*MACH_VM_MAX_ADDRESS 0x1000000000*/ /*存储类指针的核心字段(加密后)*/ \
        uintptr_t magic             : 6; /*​​验证指针合法性​​ 调试器将提取的 magic_value 与预定义的合法值(ISA_MAGIC_VALUE)比较*/                                      \
        uintptr_t weakly_referenced : 1;  /*是否为弱饮用*/                                     \
        uintptr_t unused            : 1;  /*保留位(数据结构或协议中预留的未使用位)*/                                     \
        uintptr_t has_sidetable_rc  : 1;  /*是否使用弱引用表*/                                     \
        uintptr_t extra_rc          : 19  /*内联引用计数(最大2^19-1)*/

# elif __x86_64__ //__x86_64__(对应macos)(与上面一样)
#   define ISA_MASK        0x00007ffffffffff8ULL
#   define ISA_MAGIC_MASK  0x001f800000000001ULL
#   define ISA_MAGIC_VALUE 0x001d800000000001ULL
#   define ISA_HAS_CXX_DTOR_BIT 1
#   define ISA_BITFIELD                                                        \
      uintptr_t nonpointer        : 1;                                         \
      uintptr_t has_assoc         : 1;                                         \
      uintptr_t has_cxx_dtor      : 1;                                         \
      uintptr_t shiftcls          : 44; /*MACH_VM_MAX_ADDRESS 0x7fffffe00000*/ \
      uintptr_t magic             : 6;                                         \
      uintptr_t weakly_referenced : 1;                                         \
      uintptr_t unused            : 1;                                         \
      uintptr_t has_sidetable_rc  : 1;                                         \
      uintptr_t extra_rc          : 8  /*(最大为2^8-1)*/

​​(1) 公共字段(所有架构)​​

  • nonpointer有两个值,表示自定义的类等,占1位
    0:纯isa指针
    1:不只是类对象地址,isa中包含了类信息、对象的引用计数等
  • has_assoc标记对象是否有关联对象,占1位。
    0:没有关联对象
    1:有关联对象
  • has_cxx_dtor标记对象是否有 C++ 析构函数(用于自动调用 dealloc),占1位
    如果有析构函数,则需要做析构逻辑
    如果没有,则可以更快的释放对象
  • magic存储魔术字(如 0x001d800000000001ULL),用于验证 isa 指针合法性,占6位。
  • weakly_referenced标记对象是否为弱引用,占1位。
  • ​​has_sidetable_rc标记是否使用弱引用表存储引用计数(当 extra_rc 溢出时启用),占1位。

(2)架构差异字段​​

  • shiftcls(ARM64: 33 位 / x86_64: 44 位)​​存储加密后的类指针。
    ​​ARM64​​:33 位(地址空间较小,0x1000000000 是 MACH_VM_MAX_ADDRESS)。
    ​​x86_64​​:44 位(地址空间较大,0x7fffffe00000 是 MACH_VM_MAX_ADDRESS)。
    ​​
  • extra_rc(ARM64: 19 位 / x86_64: 8 位)​内联存储引用计数。
    ​​ARM64​​:最大值为 2^19-1(524,287)。
    ​​x86_64​​:最大值为 2^8-1(255)。
    ​​溢出处理​​:当引用计数超过阈值时,迁移到全局弱引用表(has_sidetable_rc 置 1)。

请添加图片描述

原理探索

  通过alloc --> _objc_rootAlloc --> callAlloc --> _objc_rootAllocWithZone --> _class_createInstanceFromZone方法路径,查找到initInstanceIsa,并进入其原理实现:
在这里插入图片描述

initInstanceIsa的底层实现代码如下:

inline void 
objc_object::initInstanceIsa(Class cls, bool hasCxxDtor)
{
    ASSERT(!cls->instancesRequireRawIsa());
    ASSERT(hasCxxDtor == cls->hasCxxDtor());
    //初始化isa
    initIsa(cls, true, hasCxxDtor);
}

这里我们主要看一下初始化isa的底层代码:

inline void 
objc_object::initIsa(Class cls, bool nonpointer, UNUSED_WITHOUT_INDEXED_ISA_AND_DTOR_BIT bool hasCxxDtor)
{ 
    ASSERT(!isTaggedPointer()); 
    
    isa_t newisa(0);//isa_t是联合体,这里创建全零的isa实例

    if (!nonpointer) {
        newisa.setClass(cls, this);//设置类指针(加密存储到shiftcls)
    } else {
        ASSERT(!DisableNonpointerIsa);
        ASSERT(!cls->instancesRequireRawIsa());


#if SUPPORT_INDEXED_ISA //!nonpointer执行的流程,即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_VALUE
        newisa.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_BIT
        newisa.has_cxx_dtor = hasCxxDtor;
#   endif
        newisa.setClass(cls, this);
#endif
#if ISA_HAS_INLINE_RC
        newisa.extra_rc = 1;
#endif
    }

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

这里初始化isa的底层代码逻辑主要分为两部分:通过 cls 初始化 isa和通过 bits 初始化 isa。

isa与类的关联

  cls 与 isa 关联原理就是isa指针中的shiftcls位域中存储了类信息,其中initInstanceIsa的过程是将 calloc 指针 和当前的 类cls 关联起来。我们这里主要通过object_getClass函数来验证以上结论。

Objective-C 运行时提供了标准 API object_getClass,它会自动处理 isa 指针的解析(包括标签指针、元类层级等),返回对象的实际类对象。

我们在main文件中导入:

#import <objc/runtime.h>
#import "MyClass.h"

然后编写主函数文件,通过object_getClass方法来验证cls与isa是否关联:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        Class expectedCls = [MyClass class];//预期对象创建
        id obj = [[MyClass alloc] init];//创建实例
        Class actualCls = object_getClass(obj);//通过isa获取实例类
        
        // 输出验证结果
        NSLog(@"\n=== 类关联验证 ===");
        NSLog(@"预期类: %@", NSStringFromClass(expectedCls));
        NSLog(@"实际类: %@", NSStringFromClass(actualCls));

        // 断言验证
        if (expectedCls != actualCls) {
            NSLog(@"\033[31m❌ 验证失败:类关联错误!\033[0m");
            NSLog(@"预期类: %@", NSStringFromClass(expectedCls));
            NSLog(@"实际类: %@", NSStringFromClass(actualCls));
            assert(NO); // 明确触发断言
        } else {
            NSLog(@"\033[32m✅ 验证成功:类关联正确\033[0m");
        }
    }
    return 0;
}

运行代码,输出如下:请添加图片描述
这证明cls 与isa已经完美关联。

总结

  这篇文章主要探究了isa与类的关联相关内容,在代码调试过程中,笔者出现了很多问题,许多地方的代码调试步骤跟预期不一样,可能是因为obj源码的版本不同,文章中若有错误,还请斧正。

参考文章:iOS-底层原理 07:isa与类关联的原理


网站公告

今日签到

点亮在社区的每一天
去签到