iOS中,编译时期类的内存已确定,为什么在运行时可以添加方法,属

2025-04-23  本文已影响0人  博文得礼

在 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 机制正是通过操作这些动态结构,突破了编译时的限制,实现了运行时的动态修改能力。

上一篇 下一篇

猜你喜欢

热点阅读