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

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

作者: 博文得礼 | 来源:发表于2025-04-23 09:30 被阅读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 机制正是通过操作这些动态结构,突破了编译时的限制,实现了运行时的动态修改能力。

相关文章

  • [iOS 知识总结二] 为什么说Objective-C 是一门动

    理解 静态、动态是相对的,这里动态语言指的是不需要在编译时确定所有的东西,在运行时还可以动态的添加变量、方法和类 ...

  • 分类(categroy),类扩展(Extension)

    分类、类扩展区别 分类-运行时决议,类扩展-编译时决议,类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中...

  • 【iOS】消息转发机制

    在编译期向某类发送了其无法理解的消息并不会报错,因为在运行期可以继续向类中添加方法,所以编译器在编译时还不确定类中...

  • 带着问题深入了解Category底层实现

    引子 有道常见的面试题:为什么分类中无法定义实例变量?答案很简单:每个类的内存布局在编译时期就已经确定了,运行时才...

  • 为什么说OC是一门动态语言?

    自己理解:OC语言是在运行时才确定类型的,通过Runtime运行时机制,在运行时动态的添加变量,方法,类等,所以说...

  • runtime添加属性

      运行期不能对类对象添加ivar,因为在编译期类的内存大小布局已经确定,在运行期不能修改类对象的内存空间,所以不...

  • Java反射教程

    Java的反射机制,使得我们可以方便的在运行时检视:类、接口、变量和方法,而不需要在编译时期就知道类或方法等的名字...

  • 解读objc源码:重新认识类和对象

    为什么说类也是对象? 为什么不能在运行时添加成员变量? 为什么可以动态添加方法? 个人觉得,带着问题看答案,应该会...

  • java 反射

    1、在运行时期获取对象类型信息的操作。object.getClass() 返回此 Object 的内存中运行时类。...

  • runtime常见问题

    1 . iOS runtime 运行时,动态添加属性方法首先, 要明白为什么要动态给类添加方法? 如果一个类方法很...

网友评论

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

      本文链接:https://www.haomeiwen.com/subject/ampibjtx.html