前言
从 objc_class 开始,是因为它是整个 Runtime 的基础数据结构。Runtime 管的事很多------消息发送、方法查找、内存管理、Category 加载......但这些行为最终都要落在"类长什么样"上面。搞清楚 objc_class,后面的东西才能接得上。
一、源码全貌(先看完整结构)
下面是从 Apple 开源的 objc4 里提取的核心结构体,我做了适度精简,保留所有关键字段。
建议先整体扫一遍,有个印象,后面逐个解释。
arduino
// ============================================================
// 文件:objc-runtime-new.h(objc4-818.2)
// 源码地址:https://opensource.apple.com/source/objc4/
// ============================================================
// -------------------- 1. objc_object --------------------
// 所有 OC 对象的基类,只有一个字段:isa
struct objc_object {
private:
isa_t isa; // 64位,包含类指针+引用计数+标志位
public:
Class ISA(bool authenticated = false);
Class getIsa();
// ... 省略其他方法
};
// -------------------- 2. objc_class --------------------
// 这就是"类"的底层结构,继承自 objc_object
struct objc_class : objc_object {
// 注意:isa 字段继承自 objc_object,这里不重复写
Class superclass; // 父类指针
cache_t cache; // 方法缓存(哈希表)
class_data_bits_t bits; // 指向 class_rw_t 的指针+标志位
// 取出真正的数据
class_rw_t *data() const {
return bits.data();
}
// ... 省略其他方法
};
// -------------------- 3. class_data_bits_t --------------------
// 这是 objc_class.bits 的类型,用来存储指向 class_rw_t 的指针 + 几个标志位
struct class_data_bits_t {
private:
uintptr_t bits; // 就是一个 64 位整数,低位藏标志位,高位存指针
public:
// 用掩码取出真正的 class_rw_t 指针
class_rw_t *data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
// 各种标志位的读取方法
bool isSwiftLegacy() const {
return getBit(FAST_IS_SWIFT_LEGACY);
}
bool isSwiftStable() const {
return getBit(FAST_IS_SWIFT_STABLE);
}
// ... 其他方法
};
// ARM64 下的掩码和标志位定义:
// FAST_DATA_MASK = 0x00007ffffffffff8UL (取 bit 3~46,即真正的指针)
// FAST_IS_SWIFT_LEGACY = 1 << 0 (bit 0: 是否是旧版 Swift 类)
// FAST_IS_SWIFT_STABLE = 1 << 1 (bit 1: 是否是新版 Swift 类)
// FAST_HAS_DEFAULT_RR = 1 << 2 (bit 2: 是否有默认的 retain/release)
// -------------------- 4. cache_t --------------------
// 方法缓存,加速方法查找
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask; // 桶数组地址
union {
struct {
explicit_atomic<mask_t> _maybeMask; // 桶数量-1(用于哈希取模)
uint16_t _flags;
uint16_t _occupied; // 已使用的桶数
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
public:
// ... 省略查找、插入方法
};
// 单个缓存桶
struct bucket_t {
private:
explicit_atomic<SEL> _sel; // 方法名(选择子)
explicit_atomic<uintptr_t> _imp; // 函数指针(方法实现地址)
};
// -------------------- 5. class_rw_t --------------------
// 运行时可读写数据(Category 方法会合并到这里)
struct class_rw_t {
uint32_t flags;
uint16_t witness;
uint16_t index;
explicit_atomic<uintptr_t> ro_or_rw_ext; // 指向 class_ro_t 或扩展数据
Class firstSubclass; // 第一个子类
Class nextSiblingClass; // 兄弟类(形成链表)
// 获取方法/属性/协议列表
const method_array_t methods() const;
const property_array_t properties() const;
const protocol_array_t protocols() const;
// 获取只读数据
const class_ro_t *ro() const;
};
// -------------------- 6. class_ro_t --------------------
// 编译期只读数据(源码里写死的方法、变量、属性)
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart; // 实例变量起始偏移
uint32_t instanceSize; // sizeof(实例),对象占多少字节
const uint8_t * ivarLayout; // 强引用 ivar 的内存布局
const char * name; // 类名字符串,如 "NSString"
WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods; // 方法列表
protocol_list_t * baseProtocols; // 协议列表
const ivar_list_t * ivars; // 实例变量列表
const uint8_t * weakIvarLayout; // 弱引用 ivar 的内存布局
property_list_t *baseProperties; // 属性列表
};
// -------------------- 7. method_t --------------------
// 单个方法的描述
struct method_t {
SEL name; // 方法名(选择子),本质是 const char *
const char *types; // 类型编码,如 "v16@0:8"
IMP imp; // 函数指针(真正的代码地址)
};
// -------------------- 8. ivar_t --------------------
// 单个实例变量的描述
struct ivar_t {
int32_t *offset; // 偏移量指针(Non-Fragile ABI 用)
const char *name; // 变量名,如 "_name"
const char *type; // 类型编码,如 "@"NSString""
uint32_t alignment_raw;// 对齐方式
uint32_t size; // 占多少字节
};
// -------------------- 9. isa_t --------------------
// isa 的真正定义(union,64位里塞了很多信息)
union isa_t {
uintptr_t bits; // 原始64位值
// ARM64 位域展开(iOS 真机):
struct {
uintptr_t nonpointer : 1; // bit 0: 是否是优化过的 isa
uintptr_t has_assoc : 1; // bit 1: 有关联对象?
uintptr_t has_cxx_dtor : 1; // bit 2: 有 C++ 析构?
uintptr_t shiftcls : 33; // bit 3-35: 类指针(右移3位存储)
uintptr_t magic : 6; // bit 36-41: 固定值 0x1a,调试用
uintptr_t weakly_referenced : 1; // bit 42: 被弱引用?
uintptr_t unused : 1; // bit 43: 未使用
uintptr_t has_sidetable_rc : 1; // bit 44: 引用计数溢出到 SideTable?
uintptr_t extra_rc : 19; // bit 45-63: 引用计数-1
};
};
二、结构关系图
scss
objc_class(一个类在内存里的样子)
┌─────────────────────────────────────┐
│ isa (继承自 objc_object) │ ← isa_t union,64位
├─────────────────────────────────────┤
│ superclass │ ← 指向父类的 objc_class
├─────────────────────────────────────┤
│ cache │ ← cache_t 结构体
│ └── bucket_t[] 数组 │ 每个桶存 { SEL, IMP }
├─────────────────────────────────────┤
│ bits │ ← class_data_bits_t(指针+标志位)
│ └── data() ───────────────────────────→ class_rw_t(运行时可写)
│ │ ├── methods()
│ │ ├── properties()
│ │ ├── protocols()
│ │ └── ro() ────────→ class_ro_t(只读)
│ │ ├── name
│ │ ├── baseMethods
│ │ │ └── method_t[]
│ │ ├── ivars
│ │ │ └── ivar_t[]
│ │ └── baseProperties
└─────────────────────────────────────┘
三、逐结构体解析
接下来按源码出现的顺序,逐个讲解每个结构体、每个字段的含义。
3.1 objc_object ------ 所有对象的祖宗
arduino
struct objc_object {
private:
isa_t isa;
};
这是什么?
这是 OC 里所有对象 的底层表示。不管是 NSString、UIView、还是你自定义的 MyClass 实例,底层都是 objc_object。
字段解析
| 字段 | 类型 | 含义 |
|---|---|---|
isa |
isa_t |
"is a" 的缩写,标识"这个对象是什么类型"。是一个 64 位的 union,里面藏了类指针 + 引用计数 + 各种标志位。isa_t 的详细结构会在第二篇展开讲解。 |
为什么只有一个字段?
因为 objc_object 是最小公共祖先。每个对象只需要知道"我是什么类型"(isa),其他的成员变量由具体的类定义,紧跟在 isa 后面存储。
内存布局示意
vbnet
一个 MyClass 实例的内存:
┌────────────────┐ ← 对象起始地址
│ isa │ 8 字节(objc_object 的字段)
├────────────────┤
│ _name │ 8 字节(MyClass 自己的 ivar)
├────────────────┤
│ _age │ 4 字节(MyClass 自己的 ivar)
└────────────────┘
3.2 objc_class ------ 类的完整定义
arduino
struct objc_class : objc_object {
Class superclass;
cache_t cache;
class_data_bits_t bits;
class_rw_t *data() const {
return bits.data();
}
};
这是什么?
这是 OC 里类 的底层表示。每个 @interface MyClass 在运行时都对应一个 objc_class 结构体实例。
注意它继承自 objc_object,所以"类也是对象"------类对象有自己的 isa(指向元类)。
字段逐个解析
| 字段 | 类型 | 含义 |
|---|---|---|
isa |
isa_t(继承来的) |
类对象的 isa 指向它的元类 (metaclass)。isa_t 的详细结构见第二篇。 |
superclass |
Class |
父类指针。Class 是 objc_class * 的 typedef,即指向另一个 objc_class 的指针。NSObject 的 superclass 是 nil。; |
cache |
cache_t |
方法缓存,哈希表结构。最近调用的方法会缓存在这里,加速后续调用。 |
bits |
class_data_bits_t |
一个 64 位整数,低 3 位是标志位,高位是 class_rw_t 指针。 |
Class 是什么类型?
arduino
// objc.h
typedef struct objc_class *Class;
Class 就是 objc_class * 的别名,一个指向类对象的指针。你代码里写的所有 Class 都只是这个指针,没有额外结构:
ini
Class cls = [MyClass class]; // 拿到 MyClass 的 objc_class * 指针
Class superCls = [cls superclass]; // 拿到父类的 objc_class * 指针
同理,id 也是:
arduino
typedef struct objc_object *id; // id = objc_object *,指向任意实例对象
superclass 有什么用?
实现继承 。当在当前类找不到方法时,runtime 会沿着 superclass 链往上找。
css
调用 [myObj doSomething]
↓
在 MyClass 的方法列表里找
↓ 找不到
通过 superclass 到 NSObject 里找
↓ 还找不到
触发消息转发
3.3 class_data_bits_t ------ 指针 + 标志位的混合体
arduino
struct class_data_bits_t {
private:
uintptr_t bits; // 64 位整数
public:
class_rw_t *data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
}
};
这是什么?
就是 objc_class.bits 的类型。它不是简单的指针,而是把 class_rw_t 指针 和 几个标志位 打包进同一个 64 位整数里。
为什么能这样做?
因为 class_rw_t 在内存里是 8 字节对齐 的,所以它的地址的低 3 位永远是 000。苹果就把这 3 位拿来存标志位,不浪费。
64 位的布局
arduino
class_data_bits_t.bits(64位)
63 3 2 1 0
┌────────────────────────────────┬──┬──┬──┐
│ class_rw_t 指针 (bit 3~63) │ 2│ 1│ 0│
└────────────────────────────────┴──┴──┴──┘
│ │ │
│ │ └─ FAST_IS_SWIFT_LEGACY (是旧版Swift类?)
│ └──── FAST_IS_SWIFT_STABLE (是新版Swift类?)
└─────── FAST_HAS_DEFAULT_RR (有默认retain/release?)
取指针的掩码
arduino
// ARM64
#define FAST_DATA_MASK 0x00007ffffffffff8UL
// 二进制:...11111111111111111111111111111111111111000
// 作用:与运算后,低 3 位清零,剩下的就是真正的 class_rw_t 地址
data() 方法做了什么?
arduino
class_rw_t *data() const {
return (class_rw_t *)(bits & FAST_DATA_MASK);
// bits & 掩码 → 把低 3 位标志位清掉 → 得到纯净的 class_rw_t 指针
}
一句话总结
class_data_bits_t 和 isa_t 的设计思路一样------充分利用内存对齐带来的空闲位,一个 64 位整数里塞多种信息,省内存。
3.4 cache_t ------ 方法缓存
arduino
struct cache_t {
private:
explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
union {
struct {
explicit_atomic<mask_t> _maybeMask;
uint16_t _flags;
uint16_t _occupied;
};
explicit_atomic<preopt_cache_t *> _originalPreoptCache;
};
};
struct bucket_t {
private:
explicit_atomic<SEL> _sel;
explicit_atomic<uintptr_t> _imp;
};
为什么需要缓存?
每次调用方法都去 class_rw_t 的方法列表里遍历查找,太慢了。cache_t 是一个哈希表,把最近调用过的方法缓存起来。
字段解析
| 字段 | 含义 |
|---|---|
_bucketsAndMaybeMask |
哈希桶数组的起始地址 |
_maybeMask |
桶数量 - 1,用于哈希取模(hash & mask) |
_occupied |
当前已使用的桶数量 |
bucket_t 是什么?
单个缓存条目,存储 SEL(方法名)和 IMP(函数指针)的映射。
| 字段 | 类型 | 含义 |
|---|---|---|
_sel |
SEL |
方法选择子(方法名),如 @selector(viewDidLoad) |
_imp |
uintptr_t |
方法实现的函数地址 |
查找流程
java
[obj doSomething]
↓
计算 @selector(doSomething) 的哈希值
↓
hash & _maybeMask → 得到桶的索引
↓
取出 bucket_t,比较 _sel 是否等于 @selector(doSomething)
↓
相等 → 直接调用 _imp,结束(命中缓存,极快)
不相等 → 去 class_rw_t 里慢速查找
缓存什么时候会失效?
- 调用
method_exchangeImplementations(Method Swizzle)后 - 动态添加方法后
- 类第一次加载时
失效时 runtime 会调用 flushCaches() 清空缓存。
3.5 class_rw_t ------ 运行时可读写数据
arduino
struct class_rw_t {
uint32_t flags;
uint16_t witness;
uint16_t index;
explicit_atomic<uintptr_t> ro_or_rw_ext;
Class firstSubclass;
Class nextSiblingClass;
const method_array_t methods() const;
const property_array_t properties() const;
const protocol_array_t protocols() const;
const class_ro_t *ro() const;
};
这是什么?
rw = read-write (可读写)。这里存放运行时可以修改的数据,比如 Category 添加的方法会合并到这里。
字段解析
| 字段 | 含义 |
|---|---|
flags |
各种标志位(是否已初始化、是否有 C++ 构造函数等) |
ro_or_rw_ext |
指向 class_ro_t(只读数据),或扩展数据 |
firstSubclass |
指向第一个子类,形成子类链表 |
nextSiblingClass |
指向下一个兄弟类(同一个父类的其他子类) |
获取方法/属性/协议
arduino
const method_array_t methods() const; // 返回方法列表(含 Category 方法)
const property_array_t properties() const; // 返回属性列表
const protocol_array_t protocols() const; // 返回协议列表
这些方法返回的是合并后的列表------源码里写的 + Category 加进来的。
ro() 方法
返回 class_ro_t 指针,取出编译期确定的只读数据。
3.6 class_ro_t ------ 编译期只读数据
arduino
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
const uint8_t * ivarLayout;
const char * name;
WrappedPtr<method_list_t, ...> baseMethods;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;
const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};
这是什么?
ro = read-only (只读)。这里存放编译时就确定的数据,运行时不能修改。
字段逐个解析
| 字段 | 类型 | 含义 |
|---|---|---|
flags |
uint32_t |
标志位 |
instanceStart |
uint32_t |
实例变量在对象内存中的起始偏移(通常是 8,跳过 isa) |
instanceSize |
uint32_t |
一个实例对象占多少字节(sizeof) |
ivarLayout |
const uint8_t * |
描述哪些 ivar 是强引用(ARC 用) |
name |
const char * |
类名字符串,如 "UIViewController" |
baseMethods |
method_list_t * |
源码里定义的方法列表(不含 Category) |
baseProtocols |
protocol_list_t * |
源码里遵循的协议列表 |
ivars |
ivar_list_t * |
实例变量列表 |
weakIvarLayout |
const uint8_t * |
描述哪些 ivar 是弱引用 |
baseProperties |
property_list_t * |
源码里定义的属性列表 |
class_ro_t vs class_rw_t 对比
| class_ro_t | class_rw_t | |
|---|---|---|
| 全称 | read-only | read-write |
| 什么时候确定 | 编译期(写进 Mach-O 二进制文件) | 运行时(启动时构造) |
| 能修改吗 | ❌ 不能 | ✅ 能 |
| 存什么 | 源码里写死的方法、属性、变量 | 动态添加的方法、Category 合并的方法 |
为什么要分两层?
因为 Category 是运行时加载的。编译期不知道会有哪些 Category,所以:
- 编译期:把源码里写的方法存进
class_ro_t - 运行时:遍历所有 Category,把它们的方法合并 到
class_rw_t
查找方法时,先查 class_rw_t(含 Category),它内部会访问 class_ro_t。
3.7 method_t ------ 单个方法
ini
struct method_t {
SEL name;
const char *types;
IMP imp;
};
字段解析
| 字段 | 类型 | 含义 | 例子 |
|---|---|---|---|
name |
SEL |
方法选择子(方法名) | @selector(viewDidLoad) |
types |
const char * |
类型编码(返回值+参数的类型) | "v16@0:8" |
imp |
IMP |
函数指针,指向方法的真正实现 | 0x100001234(代码段地址) |
SEL 是什么?
arduino
typedef struct objc_selector *SEL;
本质是一个唯一化的 C 字符串。同名方法在整个程序里 SEL 值相同(指针相等),所以比较方法名只需要比较指针,极快。
ini
SEL sel1 = @selector(doSomething);
SEL sel2 = @selector(doSomething);
// sel1 == sel2(指针相等,不是字符串比较)
IMP 是什么?
arduino
typedef void (*IMP)(id, SEL, ...);
函数指针,前两个参数固定是:
id self:消息接收者SEL _cmd:方法选择子
这解释了为什么 OC 方法里能直接用 self 和 _cmd------它们是函数的隐藏参数。
objectivec
// 你写的:
- (void)doSomething {
NSLog(@"%@", self);
}
// 编译器眼里的:
void doSomething(id self, SEL _cmd) {
NSLog(@"%@", self);
}
types 字符串怎么读?
以 - (NSString *)nameWithPrefix:(NSString *)prefix 为例,types 是 @24@0:8@16:
bash
@ → 返回值是 id(对象)
24 → 所有参数总共占 24 字节
@ → 第1个参数是 id(self)
0 → 从第 0 字节开始
: → 第2个参数是 SEL(_cmd)
8 → 从第 8 字节开始
@ → 第3个参数是 id(prefix)
16 → 从第 16 字节开始
这套编码叫 Type Encoding,runtime 靠它做方法签名校验。
3.8 ivar_t ------ 单个实例变量
arduino
struct ivar_t {
int32_t *offset;
const char *name;
const char *type;
uint32_t alignment_raw;
uint32_t size;
};
字段解析
| 字段 | 类型 | 含义 | 例子 |
|---|---|---|---|
offset |
int32_t * |
偏移量的指针(不是值!) | 指向存储偏移量的内存 |
name |
const char * |
变量名 | "_name" |
type |
const char * |
类型编码 | "@"NSString"" |
alignment_raw |
uint32_t |
内存对齐方式 | 通常是 3(2^3 = 8 字节对齐) |
size |
uint32_t |
占多少字节 | 指针占 8 字节 |
为什么 offset 是指针而不是值?
这是 Non-Fragile ABI(非脆弱 ABI)的设计。
假设父类 NSObject 有 8 字节的 isa,子类 MyClass 的 _name 变量在 offset 8。
如果 Apple 在新系统里给 NSObject 加了一个成员变量(变成 16 字节),按老 ABI,MyClass 的 _name 还在 offset 8,就会和 NSObject 新增的变量重叠------程序崩溃。
Non-Fragile ABI 的解决方案:
offset是指针,不是值- App 启动时,runtime 检查父类大小是否变化
- 如果变化了,自动调整所有子类 ivar 的 offset 值
- 子类不需要重新编译
objectivec
旧系统:NSObject 8字节,MyClass._name 在 offset 8
↓ Apple 升级系统
新系统:NSObject 16字节
↓ runtime 自动修正
MyClass._name 的 offset 从 8 改成 16
访问 ivar 的过程
java
// 伪代码
id value = *(id *)((char *)obj + *ivar->offset);
// 1. 取出 offset 指针指向的偏移值
// 2. 对象地址 + 偏移值 = ivar 的内存地址
// 3. 解引用得到 ivar 的值
3.9 isa_t ------ 64 位里藏了很多东西
arduino
union isa_t {
uintptr_t bits;
struct {
uintptr_t nonpointer : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33;
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t unused : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};
};
为什么不直接存指针?
在 64 位系统上,指针只需要约 40 位就能表示所有内存地址。剩下的位"浪费"了,苹果就把引用计数和各种标志位塞进去,省内存。
这叫 Non-pointer ISA(优化过的 isa)。
每一位的含义
| 位域 | 位数 | 含义 |
|---|---|---|
nonpointer |
1 | 是否是优化过的 isa(现代设备都是 1) |
has_assoc |
1 | 对象是否有关联对象(objc_setAssociatedObject) |
has_cxx_dtor |
1 | 是否有 C++ 析构函数或 ARC 的 .cxx_destruct |
shiftcls |
33 | 类指针(右移 3 位存储,取时左移还原) |
magic |
6 | 固定值 0x1a,调试用(值不对说明内存被踩了) |
weakly_referenced |
1 | 是否被 __weak 指针指向过 |
unused |
1 | 未使用,预留 |
has_sidetable_rc |
1 | 引用计数是否溢出到 SideTable |
extra_rc |
19 | 存储引用计数 - 1(最大 2^19 - 1 = 524287) |
如何取出类指针?
ini
Class cls = (Class)(isa.bits & ISA_MASK);
// ISA_MASK = 0x0000000ffffffff8ULL
// 掩码取出 bit 3~35,然后隐含左移还原
四、完整内存布局示意
把所有结构体串起来,一个类在内存里长这样:
scss
objc_class 实例(代表 MyClass 这个类)
┌─────────────────────────────────────────────────────┐
│ isa (64位 isa_t) │ → 指向 Meta-MyClass(元类)
├─────────────────────────────────────────────────────┤
│ superclass (8字节) │ → 指向 NSObject
├─────────────────────────────────────────────────────┤
│ cache (cache_t) │
│ _bucketsAndMaybeMask → [ bucket_t, bucket_t... ] │ 每个桶: { SEL, IMP }
│ _maybeMask = N-1 │
│ _occupied = 已用桶数 │
├─────────────────────────────────────────────────────┤
│ bits (class_data_bits_t) │ ← 低3位是标志位,高位是指针
│ data() ──────────────────────────────────────────│──→ class_rw_t
│ │ ├── methods() → [method_t, ...]
│ │ ├── properties()→ [property_t, ...]
│ │ ├── protocols() → [protocol_t, ...]
│ │ └── ro() ───────→ class_ro_t
│ │ ├── name = "MyClass"
│ │ ├── instanceSize = 24
│ │ ├── baseMethods
│ │ │ ├── method_t { SEL, types, IMP }
│ │ │ └── method_t { ... }
│ │ └── ivars
│ │ ├── ivar_t { offset*, "_name", "@", 3, 8 }
│ │ └── ivar_t { offset*, "_age", "i", 2, 4 }
└─────────────────────────────────────────────────────┘
五、小结
| 结构体 | 可否运行时修改 | 存放什么 |
|---|---|---|
objc_class |
不直接改 | 类的容器,持有 superclass/cache/bits |
isa_t |
部分可改(引用计数位) | 类指针 + 引用计数 + 标志位,全塞在 64 位里 |
class_data_bits_t |
不直接改 | class_rw_t 指针 + 3 个标志位,又一个"指针+标志"混合体 |
cache_t |
是(每次调用方法后更新) | 最近调用的方法 SEL → IMP 映射 |
class_rw_t |
是 | 运行时合并后的方法、属性、协议 |
class_ro_t |
否 | 编译期确定的方法、变量、属性,写死在二进制里 |
method_t |
IMP 可以换(Swizzle) | 一个方法的名字、类型编码、实现地址 |
ivar_t |
offset 可改(Non-Fragile ABI) | 一个实例变量的名字、类型、偏移量 |
下一篇:isa 指针深度解析、元类体系、完整继承链图