一、核心概念
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 提供了三次补救机会,这是面试必问重点。
- 动态方法解析(Dynamic Method Resolution)
- 调用类方法:+resolveInstanceMethod:(实例方法)或 +resolveClassMethod:(类方法)。
- 你可以做什么:在这里,可以使用 class_addMethod 函数动态地为该方法添加一个实现。
- 返回值:返回 YES 表示已处理,系统会重启消息发送流程。
- 备援接收者(Fast Forwarding)
- 调用实例方法:-forwardingTargetForSelector:。
- 你可以做什么:返回另一个能响应该消息的对象,让这个对象去处理。性能好,相当于“甩锅”。
- 注意:不能返回 self,否则会形成无限循环。
- 完整消息转发(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 解析),这会让面试官觉得你不仅懂理论,还有实践经验。