Runtime

205 阅读9分钟

一、核心概念

1. 什么是 Runtime?

  • 官方定义:Runtime 是一个用 C、C++ 和汇编语言编写的库,为 Objective-C 语言提供了动态特性的支持。
  • 通俗理解:它是一套底层的 API,是 Objective-C 的幕后工作者。OC 之所以是动态语言,正是因为 Runtime。它负责在程序运行时(而非编译时)处理类的创建、方法的查找、消息转发等关键任务。
  • 延伸:Swift 为了保持与 OC 的兼容性,目前也运行在同一个 Runtime 上,但其设计理念更倾向于静态和安全,未来可能有自己的专属 Runtime。

2. 消息机制(Messaging) - Runtime 的核心

方法调用本质

  • [receiver message] 在编译时会被编译器转化为:objc_msgSend(receiver, selector)。

objc_msgSend 流程

  • 消息发送:检查 receiver 是否为 nil(OC 中向 nil 发消息不会崩溃)。
  • 查找缓存:在 receiver 的类对象的方法缓存 cache 中快速查找方法实现(IMP)。
  • 查找方法列表:如果缓存未命中,则在其类对象的方法列表 method_list 中查找。
  • 向上继承链查找:如果当前类没找到,就沿着继承体系向父类重复步骤 2 和 3。
  • 动态方法解析:如果最终没找到,Runtime 会给我们一次“亡羊补牢”的机会(+resolveInstanceMethod: 或 +resolveClassMethod:)。
  • 消息转发:如果动态解析也没处理,就会进入完整的消息转发机制。

3. 消息转发(Message Forwarding) - 三道防线

当对象无法响应某个方法时,Runtime 提供了三次补救机会,这是面试必问重点。

  1. 动态方法解析(Dynamic Method Resolution)
  • 调用类方法:+resolveInstanceMethod:(实例方法)或 +resolveClassMethod:(类方法)。
  • 你可以做什么:在这里,可以使用 class_addMethod 函数动态地为该方法添加一个实现。
  • 返回值:返回 YES 表示已处理,系统会重启消息发送流程。
  1. 备援接收者(Fast Forwarding)
  • 调用实例方法:-forwardingTargetForSelector:。
  • 你可以做什么:返回另一个能响应该消息的对象,让这个对象去处理。性能好,相当于“甩锅”。
  • 注意:不能返回 self,否则会形成无限循环。
  1. 完整消息转发(Normal Forwarding)
  • 调用方法:-methodSignatureForSelector: 和 -forwardInvocation:。
  • 流程:
    • 先调用 -methodSignatureForSelector: 获取方法签名(包含参数和返回值类型)。
    • 然后用这个签名生成一个 NSInvocation 对象。
    • 最后调用 -forwardInvocation:,你可以在这里随意处理这个 invocation:可以转发给多个对象,可以修改参数,甚至可以吞掉消息什么都不做。
  • 最灵活,但性能开销最大

面试常见问题

  • “消息转发有哪几个步骤?请详细说一下。”
  • “如果 -forwardingTargetForSelector: 返回了 nil 或者 self,会发生什么?”
  • “-doesNotRecognizeSelector: 是在哪一步调用的?”

二、重要数据结构(理解即可,无需死记)

这些结构体在 objc/runtime.h 中定义,了解它们有助于理解对象模型。

  • objc_object:所有对象的根类型。它有一个 isa 指针(现在可能优化为 isa_t 联合体),指向对象所属的类。
  • objc_class:类对象的类型,它继承自 objc_object(所以类本身也是一个对象,称为元类 Meta Class 的实例)。
    • isa:指向其元类(Meta Class)。
    • superclass:指向其父类。
    • cache:方法缓存,用于加速方法查找(散列表结构)。
    • bits:存储类的具体信息,通过 bits & FAST_DATA_MASK 可以得到class_rw_t。
  • class_rw_t(可读可写):存放类的运行时信息。
    • methods:方法列表(包含分类的方法)。
    • properties:属性列表。
    • protocols:协议列表。
  • class_ro_t(只读):存放类的编译时就确定的信息。
    • name:类名。
    • ivars:成员变量列表(Ivar List)。
    • baseMethodList:基础方法列表(编译时确定的方法)。

面试常见问题:

  • “一个 NSObject 对象占用多少内存?”(引申出 isa 指针,64位下占8字节,但系统会至少分配16字节)
  • “方法的缓存机制是怎样的?为什么用散列表?”(提高查找效率,SEL 作为 key,IMP 作为 value)

三、Runtime 的常见应用场景

1. 关联对象(Associated Objects)

  • 功能:使用 objc_setAssociatedObject 和 objc_getAssociatedObject 为已存在的类动态地添加属性(尤其是在分类 Category 中,因为分类无法直接添加实例变量)。
  • 内存管理策略:OBJC_ASSOCIATION_RETAIN_NONATOMIC, OBJC_ASSOCIATION_ASSIGN 等,与 @property 的属性类似。

2. 方法交换(Method Swizzling)

  • 功能:在运行时交换两个方法的实现(method_exchangeImplementations)。
  • 用途:AOP(面向切面编程),例如无侵入地统计按钮点击事件、全局页面生命周期追踪等。
  • 注意要点:
    • 应该在 +load 方法中进行(+initialize 可能不会调用或调用多次)。
    • 交换前先调用 class_addMethod,防止本类中没有原始方法的实现。
    • 使用 dispatch_once 来确保只交换一次。

3. 动态地操作类和成员

  • 动态创建类:objc_allocateClassPair, objc_registerClassPair。
  • 动态添加方法/属性/协议:class_addMethod, class_addProperty, class_addProtocol。
  • 遍历类的属性/方法/协议:class_copyPropertyList, class_copyMethodList, class_copyProtocolList。
  • 用途:JSON 模型转换库(如 MJExtension, YYModel)、字典转模型、自动归档解档等。

四、面试高频问题清单

1. 讲一下 Objective-C 的消息机制。

  • (参考上面的 objc_msgSend 流程)

2. 消息转发机制是怎样的?请详细说明。

  • (参考上面的“三道防线”)

3. [obj foo] 和 objc_msgSend() 之间有什么关系?

  • 前者是后者的语法糖,编译后就是后者。

4. Runtime 如何通过 Selector 找到 IMP 的地址?

  • 通过消息查找流程:缓存 -> 当前类方法列表 -> 父类链 -> 消息转发。

5. 能否向编译后的类中增加实例变量?能否向运行时创建的类中添加实例变量?

  • 不能向已编译好的已存在的类添加实例变量。因为类的内存布局在编译时就已经确定,class_ro_t 是只读的。
  • 可以向运行时动态创建的类中添加实例变量,但必须在 objc_registerClassPair 之前。

6. Category 的实现原理?为什么不能添加实例变量?

  • 原理:Category 在运行时会被合并到主类 class_rw_t 的 methods, properties, protocols 列表中。
  • 不能加实例变量:因为实例变量是存储在 class_ro_t 的 ivars 中的,而 ivars 是只读的,在编译期就确定了内存偏移。Runtime 无法修改已编译类的内存布局。但可以通过关联对象来模拟添加属性。

7. +load 和 +initialize 方法的区别?

  • 调用时机:
    • +load:在 Runtime 加载类、分类时必定调用(仅调用一次),且不遵循继承规则。
    • +initialize:在类第一次收到消息时调用(惰性调用),遵循继承规则(如果子类没实现,会调用父类的)。
  • 调用顺序:
    • +load:父类 -> 子类 -> 分类。
    • +initialize:父类 -> 子类(如果分类实现了,会覆盖主类的)。

8. class 方法和 objc_getClass 方法有什么区别?

- [obj class]:如果 obj 是实例对象,返回其类对象;如果是类对象,则返回自身。
- objc_getClass("ClassName"):传入类名字符串,返回对应的类对象。
- object_getClass(obj):返回的是 obj 的 isa 指针指向的对象。如果 obj 是实例对象,返回类对象;如果是类对象,返回元类。

9. isKindOfClass 和 isMemberOfClass 的区别?

- isMemberOfClass:判断是否是某个特定类的实例。
- isKindOfClass:判断是否是某个类或其派生类的实例。

10. Method Swizzling 的注意事项?

(参考上面的“注意要点”)

11. objc_msgSend的执行流程(消息传递的步骤)?

步骤 0: 检查接收者 (Receiver Check)
 首先,函数会检查消息的接收者 `receiver` 是否为 `nil`。
 如果是 `nil`,那么整个消息发送过程会被短路(short-circuited),函数直接返回 `nil`(或相应类型的 0 值)。这就是为什么**向 nil 发送消息不会崩溃**。
步骤 1: 快速查找 (Fast Path - Cache Lookup)
1.1  **获取类对象**:通过 `receiver` 的 `isa` 指针(或在 ARM64 利用 `objc_object` 的优化)获取其类对象 `Class`。
1.2  **检查缓存**:在类的 `cache_t`(方法缓存)中,使用选择器 `SEL` 作为键进行查找。这是一个高度优化的哈希表,目的是为了极速查找。
1.3  **命中缓存**:如果在缓存中找到了对应的函数指针(`IMP`),`objc_msgSend` 会直接跳转到该 `IMP` 执行。**绝大多数正常的方法调用都会在这一步完成,因此效率极高**
步骤 2: 慢速查找 (Slow Path - Method Table Lookup)
如果缓存中没有找到,则会进入更复杂的慢速查找流程(实际上是由另一个函数 `objc_msgSend_cached` 或 `lookUpImpOrForward` 处理)。
2.1  **遍历方法列表**:在自己的类的方法列表(`class_rw_t` 中的 `methods`)中查找。
2.2  **继承链查找**:如果在自己类中没找到,就通过 `super_class` 指针沿着继承链往父类中查找,同样会检查父类的缓存和方法列表。
2.3  **找到 IMP**:一旦在某个类中找到方法,会将其 **`IMP` 缓存到最初接收者的类中**(这样下次同一个接收者类的实例再调用此方法时,就可以直接在快速查找中命中),然后跳转执行。
步骤 3: 动态方法解析 (Dynamic Method Resolution)
如果慢速查找在整个继承链中都找不到实现,运行时会给接收者类最后一次机会。

3.1  运行时调用 `+resolveInstanceMethod:`(实例方法)或 `+resolveClassMethod:`(类方法)。
3.2  你可以在这些方法中,使用 `class_addMethod` 函数动态地为这个未知的选择器添加一个实现。
3.3  如果添加成功,运行时会**重新启动消息发送过程**,这次就能成功找到刚添加的方法了。
步骤 4: 消息转发 (Message Forwarding)
如果动态方法解析也没有处理,运行时就会启动完整的消息转发机制,这是最后的安全网。

4.1  ** forwardingTargetForSelector:**  
运行时先询问接收者:“你能不能把这个消息转给另一个能处理它的对象?”(`-forwardingTargetForSelector:`)。  
如果你返回了一个非 nil 的对象,那么运行时会把消息原封不动地发送给那个对象,流程从头开始。这叫做**备用接收者**。
4.2  ** methodSignatureForSelector: 和 forwardInvocation:**  
如果上一步返回 nil,运行时就会创建一个 `NSInvocation` 对象(包含了消息的全部细节:接收者、选择器、参数等)。  
它首先调用 `-methodSignatureForSelector:` 来获取方法签名(参数和返回类型信息),然后创建 `NSInvocation` 对象并调用 `-forwardInvocation:` 方法。  
你可以在 `-forwardInvocation:` 里做任何事:比如将消息转发给多个对象,吞掉消息,修改参数等等。这是最强大的转发阶段。
4.3  ** doesNotRecognizeSelector:**  
如果连 `-forwardInvocation:` 都没有处理,运行时就会调用 `-doesNotRecognizeSelector:` 方法,默认实现是抛出我们熟悉的 `unrecognized selector sent to instance` 异常。
 

总结

准备 Runtime 面试,关键在于理解其 “动态” 的本质,核心是 “消息传递” 和 “消息转发” 机制。不仅要能说出流程,最好能结合一两个实际应用场景(如埋点、热修复、JSON 解析),这会让面试官觉得你不仅懂理论,还有实践经验。