面试官让你介绍runtime的初始化 - 怎么回答才能体现你对系统底层认知的广度和深度

127 阅读5分钟

一句话开场(总览全局)

Objective-C的Runtime初始化,指的是App启动时,系统加载libobjc.A.dylib动态库,并完成一系列关键的数据结构初始化和方法注册的过程,为之后对象创建、消息发送、方法调用提供支持。

(开场要显得宏观、体系化,不要一上来就死讲细节。)

OC runtime初始化

阶段一:libobjc动态库加载

  • 加载libSystem.dylib 调用初始化方法, 最终调用了runtime的_objc_init方法
static void libSystem_initializer(const void* args, int argc, const char* argv[], const char* envp[], const char* apple[], const struct ProgramVars* vars) {
    ...
    libdispatch_init();
    ...
}

void libdispatch_init(void)
{
    _os_object_init();  // 👈 初始化底层对象系统(OSObject)
    ...
}
void _os_object_init(void)
{
    _objc_init();  // 👈 调用 libobjc 中的 runtime 初始化方法
}
_objc_init 做了哪些事情?
  1. 环境准备

    • 初始化锁、环境变量(如 OBJC_DEBUG_* 系列的环境变量)。
    • 初始化 Runtime 自身的一些基础数据结构(如类表、选择器表等)。
  2. 注册回调函数

    • 调用 _dyld_objc_notify_register 向 dyld 注册三个回调:

      • map_images:每一个镜像被映射进内存时触发。
      • load_images:类、分类完成注册后触发(用于 +load 方法调用)。
      • unmap_images:镜像被卸载时触发。
注册回调函数做了哪些事情?
  1. map_images做了哪些事情

    • arr_init初始化
    void arr_init(void)  {
        AutoreleasePoolPage::init();
        SideTablesMap.init();
        _objc_associations_init();
    }
    1. 自动释放池的初始化 
    2. SideTablesMap 初始化 这个静态全局变量开辟空间
    3. AssociationsManager 的初始化 即为全局使用的关联对象表开辟空间
    
    • read_images

      它的主要作用是解析 dyld 加载到内存中的 Mach-O 文件中的 Objective-C 元数据(类、分类、协议等),并将它们构建成 Runtime 实际运行所需的对象体系,完成注册和初始化,保证后续消息发送、动态特性能够正常工作。

      • 工作时机:read_images 是在 dyld 通知每个镜像加载完成后,通过 __DATA 和 __DATA_CONST 段中的节区(比如 __objc_classlist, __objc_catlist)拿到 ObjC 相关的数据。

      • 解析每个类(class_ro_t -> class_t)

      • 处理 metaclass(保证 isa 和 superclass 链接正确)

      • 将分类(category_t)挂接到原有类的方法列表中(runtime 会合并 category 的方法到类的 method list)

      • 处理协议(protocol_t)

      • 处理选择器 (selector) 去重(sel_registerName)

      • 对某些延迟初始化(如协议附加)做基本登记,但不会马上展开

      • 将解析出的类注册到 Runtime 全局哈希表(如 NXMapTable)中,供 objc_msgSend、类查找、动态特性使用。

    • 注意:如果是 继承自 NSObject 的 Swift 类,Runtime 是支持的。

      • Runtime 会走一个专门的 Swift realize 流程:

      • 读取 Swift 的只读数据(Swift 特有的 metadata 格式)

      • 把 Swift 的通过关键词@objc修饰的方法 methodlist、property list、protocol list拷贝到 Runtime能认识的结构体里

      • 给 class_rw_t 初始化方法列表、属性列表等(让它能被消息发送等机制识别)

  2. map_images处理完会回调load_images方法

    • 加载所有分类
    • 将分类中的实例(类)方法,协议,属性添加到类的结构体中
    • 将类方法load放到数组中通过函数地址直接调用不走msgSend
      • 从父类递归查找load方法
    • 调用分类的load方法

注意:load的调用顺序是 父类 -> 子类 -> 分类

swift runtime初始化

swift 类 信息会被编译器编译成元数据 存储在section __swift5_types 中

  • NominalTypeDescriptor:描述名义类型(如类、结构体、枚举、协议等)的元数据。
  • ProtocolDescriptor:描述协议(Protocol)的元数据。
  • Type Metadata:用于描述类型信息(如类型的大小、布局、继承关系等)。

当加载动态库 读取加载mach-o文件的__swift5_types,将数据映射到内存中

举例说明

class MyClass {
    var name: String
    init(name: String) {
        self.name = name
    }
}
这个类的元数据会包含以下几部分内容:
  1. Class Descriptor

    • 类名、继承关系、方法列表、属性列表等。
  2. Type Metadata

    • 类型的大小、对齐要求、是否是类类型、是否是协议等。
  3. Vtable(虚表):

    • 类的方法表,记录了类的实例方法和类方法。
  4. Instance Layout

    • 类的实例内存布局,存储属性的偏移量、大小等信息。
  5. Protocol Conformance

    • 如果类遵守了某个协议,协议的描述符也会被记录。
生成时机与过程
  1. 编译时生成 Type Metadata

    • 编译器会为 MyClass 类生成类型描述符,记录类的大小、内存布局(例如 name 字段的位置)、初始化方法(init(name:))和实例方法(greet())。
    • 如果 MyClass 继承自某个类或者遵循某个协议,编译器也会生成相关的继承和协议 conformance 信息。
  2. 链接时合并 Type Metadata

    • 编译器将 MyClass 类的类型元数据合并到 .o 文件,并通过链接器将其合并到最终的可执行文件中,通常放在 __swift5_types 区段中。
  3. 运行时加载 Type Metadata

    • 当程序启动时,运行时会加载 __swift5_types 区段,获取所有类型的元数据。
  4. 首次使用时的加载

    • 当你第一次访问 MyClass(例如,创建实例 let obj = MyClass(name: "John"))时,运行时会通过 Type Metadata 确定如何分配内存、设置字段值、调用方法。并将isa指针指向元数据

swift runtime 和 OC runtime 的区别

  1. mach-o中存储内容不同

    • OC是在初始化的时候将内容转成runtime需要的类结构体。创建对象并将isa指向该结构体
    • swfit 编译器生成元数据 启动时候加载到内存中即可

两者存储内容不同,swift占用空间比较多,