在 iOS 中,类的信息存储在 Objective-C 运行时系统(Runtime) 中,其机制允许动态操作类结构,具体原因如下:
核心原因是:编译时确定的是类对象(Class)的「固定内存布局」,而类的「动态信息(方法、属性列表等)」存储在可动态修改的数据结构中,Objective-C 的 runtime 机制 正是通过操作这些动态结构,实现了运行时添加方法、属性等能力。
1. 先澄清:“编译时类的内存已确定”的真实含义
编译时确定的是「类对象(Class 类型)本身的初始内存布局」,而非类的所有内容。根据 Objective-C runtime 的 objc_class 结构体(简化版):
struct objc_class {
Class isa; // 指向元类,编译时确定位置
Class superclass; // 指向父类,编译时确定位置
cache_t cache; // 方法缓存,初始结构固定但内容可动态扩容
class_data_bits_t bits; // 关键!存储类的动态数据(方法列表、属性列表等)
};
其中,isa、superclass 的内存位置在编译时确定,但 bits 内部封装了 可动态修改的列表(如 method_list_t 方法列表、property_list_t 属性列表),这是运行时动态修改的核心入口。
2. 运行时动态添加的核心原理(分模块解析)
(1)运行时添加方法:修改类的「方法列表」
方法的存储载体是 method_list_t(动态数组),runtime 提供 class_addMethod 等 API,直接向类的方法列表中 追加新方法(而非修改类对象的固定内存布局)。
• 原理:编译时类的方法列表仅包含初始方法(如类定义中的方法),运行时通过 class_addMethod 将新方法的 objc_method 结构体添加到 bits 指向的 method_list_t 中,后续方法查找时会遍历该列表。
• 代码示例:
// 1. 定义一个类(编译时仅包含初始方法)
@interface Person : NSObject
- (void)eat; // 初始实例方法
@end
@implementation Person
- (void)eat { NSLog(@"Eat"); }
@end
// 2. 运行时动态添加新方法
void runMethod(id self, SEL _cmd) {
NSLog(@"Run (动态添加的方法)");
}
int main(int argc, const char * argv[]) {
@autoreleasepool {
Person *p = [[Person alloc] init];
// 动态添加实例方法 - (void)run
class_addMethod([Person class], @selector(run), (IMP)runMethod, "v@:");
// 调用动态添加的方法(运行时可正常执行)
[p performSelector:@selector(run)]; // 输出:Run (动态添加的方法)
}
return 0;
}
(2)运行时添加属性:分「关联属性」和「成员变量」两种场景
• 场景1:添加关联属性(最常用)
直接添加「成员变量(ivar)」受限于类的内存布局(编译时 ivar 的偏移量已确定,运行时无法新增 ivar 到类的 ivar 列表),因此常用 关联对象(Associated Objects) 实现:
原理:runtime 在实例对象的 isa 指针旁维护了一个「关联属性哈希表」,通过 objc_setAssociatedObject 可将属性值绑定到实例上,本质是“外挂式”存储,不修改类的原有内存布局。
代码示例:
// 运行时给 Person 实例添加关联属性 "age"
objc_setAssociatedObject(p, @selector(getAge), @(20), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// 获取关联属性
NSNumber *age = objc_getAssociatedObject(p, @selector(getAge));
NSLog(@"Age: %@", age); // 输出:Age: 20
• 场景2:添加成员变量(ivar)
需在类的「初始化完成前」(如 +load 方法中)调用 class_addIvar,因为类初始化后 ivar 列表会被锁定。原理是:编译时 ivar 列表是动态数组,初始化前可通过 class_addIvar 追加新 ivar,确定其偏移量后锁定列表。
(3)运行时添加分类(Category):合并分类的动态信息
分类的方法、属性并非在编译时合并到主类,而是在 程序启动时(runtime 加载阶段),通过 runtime 的 _read_images 函数将分类的 method_list、property_list 合并到主类的对应列表中,本质仍是修改类的动态数据结构,不改变主类的核心内存布局。
关键总结
“编译时类的内存已确定”仅指 类对象(Class)的核心结构(isa、superclass 等)的内存位置固定,而类的「方法、属性、分类信息」存储在 bits 指向的 动态列表/哈希表 中。Objective-C 的 runtime 机制正是通过操作这些动态结构,突破了编译时的限制,实现了运行时的动态修改能力。









网友评论