Runtime

10 阅读7分钟

一、核心概念

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:判断是否是某个类或其派生类的实例。

Method Swizzling 的注意事项?

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

总结

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