一、核心概念
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:判断是否是某个类或其派生类的实例。
Method Swizzling 的注意事项?
(参考上面的“注意要点”)
总结
准备 Runtime 面试,关键在于理解其 “动态” 的本质,核心是 “消息传递” 和 “消息转发” 机制。不仅要能说出流程,最好能结合一两个实际应用场景(如埋点、热修复、JSON 解析),这会让面试官觉得你不仅懂理论,还有实践经验。