核心说明:聚焦面试高频提问,全程直击考点,无冗余表述,覆盖Runtime底层本质、核心数据结构、核心机制(消息传递、动态解析等)、实操应用及面试延伸点,兼顾理论深度与实操应答性,可直接用于面试背诵。
一、Runtime 核心定义(面试开篇必答)
面试必记:Runtime(运行时)是 Objective-C 的动态运行时系统,本质是一套用C语言编写的底层API(函数库),核心作用是让OC语言在运行期(而非编译期)完成消息分发、方法解析、动态扩展等操作,是OC语言“动态性”的核心支撑。OC的方法调用本质上不是直接调用函数,而是通过Runtime发送消息实现的。
补充考点:Runtime分为两种—— Legacy Runtime(早期iOS设备/32位设备)和 Modern Runtime(当前主流,64位设备),面试重点考察Modern Runtime,核心差异是类结构的存储方式不同。
二、Runtime 核心数据结构(必背,面试高频)
Runtime的所有机制都基于底层核心结构体实现,无需背诵完整源码,重点掌握结构组成、核心作用及关联关系,以下是面试必考的4个核心结构体:
2.1 objc_object(对象结构体,所有OC对象的底层本质)
OC中所有对象(实例对象、类对象、元类)的底层都是objc_object结构体,核心只有一个成员——isa指针,这是对象能关联到类、实现动态特性的核心。
// 简化源码(面试重点记忆) struct objc_object { isa_t isa; // 核心:指向类对象/元类,决定对象的类型 };isa指针(面试重中之重):现代Runtime中采用非指针isa(non-pointer isa),利用64位系统的空闲位存储引用计数、是否被弱引用等额外信息,减少内存访问层级,提升性能;
延伸:id类型的本质是
typedef struct objc_object *id,即指向objc_object结构体的指针,因此id可以指向任何OC对象。
2.2 objc_class(类结构体,类对象/元类的底层本质)
类对象、元类的底层都是objc_class结构体,且objc_class继承自objc_object(因此类本身也是一个对象),核心存储类的相关信息,决定类的方法、属性、协议等。
// 简化源码(面试重点记忆核心成员) struct objc_class : objc_object { isa_t isa; // 指向元类(类对象的isa) Class superclass; // 指向父类,形成继承链 cache_t cache; // 方法缓存,优化方法查找效率 class_data_bits_t bits; // 存储类的方法、属性、协议列表(核心) };isa指针:类对象的isa指向元类(存储类方法),元类的isa指向根元类,根元类的isa指向自身;
superclass指针:类对象的superclass指向父类对象,元类的superclass指向父类的元类,根元类的superclass指向根类对象(如NSObject),形成继承链;
cache缓存:存储最近调用过的方法(SEL+IMP),下次调用时直接从缓存中查找,避免重复遍历方法列表,提升性能;
bits字段:通过bits可访问class_rw_t和class_ro_t结构体,是类信息的核心存储载体。
2.3 class_ro_t 与 class_rw_t(类信息存储结构体)
二者不直接属于objc_class,而是通过bits字段间接访问,核心区别是“只读/可读写”,面试常考二者差异:
class_ro_t(只读结构体):编译期确定,存储类的只读信息,无法修改,如类名、编译期确定的方法列表(baseMethodList)、成员变量(ivars)、属性(baseProperties)等;
class_rw_t(可读写结构体):类首次使用前(realize阶段)创建,存储可动态扩展的信息,如Category添加的方法、属性、协议,以及类的子类列表等;
面试延伸:Category的方法能“覆盖”原类方法,本质是Category的方法被添加到class_rw_t的方法列表中,且优先级高于原类方法(后编译的Category方法优先级更高)。
2.4 Method / SEL / IMP(方法相关核心结构体,面试必背)
三者是Runtime方法调用的核心,需明确各自含义、关联关系,面试常考“三者关系”:
SEL(选择器):
本质:
typedef struct objc_selector *SEL,是方法名在Runtime中的唯一标识,相同方法名对应同一个SEL(无论所属哪个类);获取方式:
@selector(methodName)或sel_registerName("methodName");核心作用:用于查找方法的实现(IMP)。
IMP(方法实现指针):
本质:
typedef id (*IMP)(id, SEL, ...),是指向方法具体实现代码的函数指针;核心作用:拿到IMP后,可直接调用方法(跳过Runtime消息查找流程),是Method Swizzling的核心。
Method(方法结构体):
本质:
typedef struct objc_method *Method,是SEL和IMP的配对关系,还包含方法的类型编码(method_types);核心作用:关联方法名(SEL)和方法实现(IMP),Runtime通过SEL查找对应的Method,再通过Method获取IMP。
三者关系(面试必答):SEL是方法的“名字”,IMP是方法的“实现地址”,Method是将“名字”和“地址”绑定起来的“桥梁”。
2.5 补充:元类(Meta-Class,面试难点)
元类是类对象的“类”,核心作用是存储类方法,面试需牢记以下逻辑(必背):
实例对象的isa → 类对象(存储实例方法);
类对象的isa → 元类(存储类方法);
元类的isa → 根元类(NSObject的元类);
根元类的isa → 自身;
核心结论:实例方法存在类对象中,类方法存在元类中,元类也遵循继承链(与类的继承链一致)。
三、Runtime 核心机制(面试重中之重,必考)
Runtime的核心机制围绕“动态性”展开,重点掌握4个核心机制,每个机制需牢记“原理+流程+面试考点”,其中消息传递和消息转发是高频难点。
3.1 核心机制1:消息传递(objc_msgSend,OC方法调用的本质)
面试必记:OC中所有方法调用(如[obj method]),编译后都会被转换为Runtime的objc_msgSend函数调用,本质是“发送消息”,而非直接调用函数,流程如下(必背):
通过实例对象obj的isa指针,找到其对应的类对象;
在类对象的cache(方法缓存)中查找对应的SEL,找到则获取IMP,调用方法实现,流程结束;
若cache中未找到,遍历类对象的methodList(方法列表),找到对应的Method,获取IMP并调用,同时将该方法加入cache(缓存优化);
若类对象中未找到,通过superclass指针找到父类对象,重复步骤2-3,直到根类(NSObject);
若根类中仍未找到,进入“消息转发”流程(若未实现转发,报错
unrecognized selector sent to instance)。
面试延伸1:objc_msgSend的参数的核心是“接收者(obj)”和“选择器(SEL)”,还可传递额外参数,底层会自动处理参数和返回值;
面试延伸2:cache的缓存策略——LRU(最近最少使用),当缓存满时,会移除最久未使用的方法,保证缓存的都是常用方法。
3.2 核心机制2:消息转发(解决“方法未找到”的核心,面试难点)
当消息传递流程中,从自身到根类都未找到对应SEL时,Runtime会触发消息转发机制,给开发者一次“补救机会”,避免程序崩溃,转发流程分3步(必背,按优先级排序):
第一步:动态方法解析(最优先,尝试自己添加方法实现)
实例方法未找到:调用类的
+ (BOOL)resolveInstanceMethod:(SEL)sel;类方法未找到:调用类的
+ (BOOL)resolveClassMethod:(SEL)sel;核心逻辑:在该方法中,通过
class_addMethod动态添加SEL对应的IMP,返回YES表示解析成功,消息传递流程继续;返回NO,进入下一步转发。代码示例(实例方法解析):
// 动态解析未实现的实例方法 + (BOOL)resolveInstanceMethod:(SEL)sel { if (sel == @selector(unimplementedMethod)) { // 动态添加方法实现,参数:类、SEL、IMP、类型编码 class_addMethod(self, sel, (IMP)dynamicMethodImpl, "v@:"); return YES; } return [super resolveInstanceMethod:sel]; } // 方法实现 void dynamicMethodImpl(id self, SEL _cmd) { NSLog(@"动态解析方法执行"); }
第二步:快速转发(尝试将消息转发给其他对象)
调用方法:
- (id)forwardingTargetForSelector:(SEL)aSelector(实例方法);核心逻辑:返回一个能处理该SEL的对象(非nil),Runtime会将消息转发给该对象,由该对象执行消息传递流程;返回nil,进入下一步转发;
面试延伸:快速转发仅能转发给“一个对象”,无法修改消息的SEL和参数。
第三步:慢速转发(完整转发,可修改消息内容)
核心逻辑:将消息包装成NSInvocation对象,开发者可修改消息的接收者、SEL、参数,再转发给其他对象,流程如下:
调用
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector,返回该SEL的方法签名(描述参数和返回值类型);若返回nil,程序崩溃;若返回有效方法签名,调用
- (void)forwardInvocation:(NSInvocation *)anInvocation;在forwardInvocation中,通过
[anInvocation invokeWithTarget:target],将消息转发给目标对象,还可修改anInvocation的参数、SEL等。
面试延伸:慢速转发比快速转发更灵活,但性能略低,因为需要包装NSInvocation对象。
3.3 核心机制3:动态方法决议(Method Swizzling,黑魔法,面试高频)
Method Swizzling(方法交换)是Runtime最常用的实操场景,本质是“交换两个Method的IMP”,实现“偷换方法实现”,常用于AOP(面向切面编程)、Hook方法、统一埋点、崩溃防护等。
核心原理:通过Runtime API,获取两个Method的IMP,然后交换二者的IMP,使得调用原方法时,实际执行的是交换后的方法实现;
核心API(必背):
- class_getInstanceMethod(Class cls, SEL sel):获取实例方法; - class_getClassMethod(Class cls, SEL sel):获取类方法; - method_exchangeImplementations(Method m1, Method m2):交换两个方法的IMP。代码示例(Hook UIViewController的viewDidLoad方法):
@implementation UIViewController (Swizzling) + (void)load { // 确保只执行一次(避免Category多次加载重复交换) static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ // 获取原方法和自定义方法 Method originalMethod = class_getInstanceMethod(self, @selector(viewDidLoad)); Method swizzledMethod = class_getInstanceMethod(self, @selector(swizzled_viewDidLoad)); // 交换方法实现 method_exchangeImplementations(originalMethod, swizzledMethod); }); } // 自定义方法(替换后的实现) - (void)swizzled_viewDidLoad { // 执行原viewDidLoad逻辑(此时调用swizzled_viewDidLoad,实际执行原实现) [self swizzled_viewDidLoad]; // 新增自定义逻辑(如埋点、打印日志) NSLog(@"viewDidLoad 被调用了"); } @end面试注意事项(必背,避免踩坑):
在+load方法中执行交换,且用dispatch_once保证只交换一次(+load方法会在类加载时自动调用,且每个类只调用一次);
避免交换系统方法后,未调用原方法实现,导致系统逻辑异常;
若原方法可能不存在,需先通过class_addMethod添加方法,再进行交换;
命名规范:自定义方法名加前缀,避免与其他Category方法重名。
3.4 核心机制4:动态属性与关联对象(Associated Objects)
OC中,Category不能直接添加成员变量,但可通过Runtime的关联对象机制,给分类动态添加“伪属性”,本质是将属性值与对象关联存储,面试常考使用场景和API。
核心API(必背):
- objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy):给对象添加关联对象; - objc_getAssociatedObject(id object, const void *key):获取对象的关联对象; - objc_removeAssociatedObjects(id object):移除对象的所有关联对象(不推荐,建议按需移除)。关联策略(objc_AssociationPolicy,面试必记):
OBJC_ASSOCIATION_ASSIGN:弱引用,不增加引用计数,类似assign,可能产生野指针;
OBJC_ASSOCIATION_RETAIN_NONATOMIC:强引用,非原子性,类似strong、nonatomic;
OBJC_ASSOCIATION_COPY_NONATOMIC:复制,非原子性,类似copy、nonatomic;
OBJC_ASSOCIATION_RETAIN:强引用,原子性;OBJC_ASSOCIATION_COPY:复制,原子性(较少用)。
代码示例(给Category添加动态属性):
@interface NSObject (AssociatedProperty) // 动态添加的伪属性 @property (nonatomic, strong) NSString *customName; @end @implementation NSObject (AssociatedProperty) // 关联对象的key(需唯一,常用static const void *) static const void *CustomNameKey = &CustomNameKey; - (void)setCustomName:(NSString *)customName { // 添加关联对象,强引用、非原子性 objc_setAssociatedObject(self, CustomNameKey, customName, OBJC_ASSOCIATION_RETAIN_NONATOMIC); } - (NSString *)customName { // 获取关联对象 return objc_getAssociatedObject(self, CustomNameKey); } @end面试延伸:关联对象的存储位置——不存储在对象本身(objc_object),而是存储在Runtime的全局哈希表中,通过对象指针和key关联查找,因此不会影响对象的内存布局。
四、Runtime 其他面试高频考点(延伸,必记)
1. Runtime的动态性体现(面试必答):
动态消息传递:运行时才确定调用哪个方法,而非编译期;
动态方法解析:运行时动态添加方法实现;
动态交换方法:运行时交换两个方法的实现;
动态添加属性:通过关联对象给对象动态添加属性;
动态创建类:通过
objc_allocateClassPair动态创建类,objc_registerClassPair注册类。
2. 分类(Category)的底层实现(结合Runtime):
Category的底层是objc_category结构体,存储类名、协议、方法、属性等信息;
编译时,Category不会修改原类的结构,而是在运行时,通过Runtime将Category的方法、属性、协议合并到原类的class_rw_t中;
若Category与原类有同名方法,Category的方法会覆盖原类方法(本质是Category的方法被添加到方法列表前面,查找时先找到);
多个Category同名方法,编译顺序决定优先级,后编译的Category方法覆盖先编译的。
3. weak指针的底层实现(结合Runtime):
weak指针的底层依赖Runtime的SideTable结构(包含weak_table哈希表);
当对象被销毁(引用计数为0)时,Runtime会遍历weak_table中该对象的所有weak指针,将其自动置为nil,避免野指针;
weak指针不会增加对象的引用计数,这是与strong指针的核心区别。
4. Runtime的应用场景(面试必答,结合实操):
崩溃防护:Hook系统方法(如NSArray的objectAtIndex:),避免数组越界、字典nil值等崩溃;
统一埋点:通过Method Swizzling Hook页面跳转、按钮点击等方法,统一添加埋点逻辑;
组件化通信:通过关联对象、动态方法调用,实现组件间解耦通信;
JSON模型转换:通过Runtime遍历对象的属性,自动将JSON字典转换为模型对象(如MJExtension底层原理);
自定义KVO:基于Runtime的消息转发、方法交换,实现KVO的底层逻辑。
五、面试高频问答(直接应答,无需修改)
问题1:Runtime是什么?OC的动态性靠什么实现?
应答:Runtime是OC的动态运行时系统,本质是一套C语言API,核心作用是让OC在运行期完成消息传递、方法解析、动态扩展等操作;OC的动态性完全靠Runtime实现,编译期仅确定消息的发送,运行期才确定方法的实现。
问题2:OC方法调用的本质是什么?消息传递的流程是什么?
应答:本质是Runtime发送消息,编译后转换为objc_msgSend函数调用;流程:1. 通过对象isa找到类对象;2. 查找类对象的cache和方法列表,找到则调用IMP;3. 未找到则遍历父类继承链;4. 仍未找到则进入消息转发流程,未实现转发则崩溃。
问题3:消息转发的流程是什么?分几步?
应答:分3步,按优先级排序:1. 动态方法解析(resolveInstanceMethod:),尝试动态添加方法实现;2. 快速转发(forwardingTargetForSelector:),转发给其他对象;3. 慢速转发(methodSignatureForSelector: + forwardInvocation:),包装成NSInvocation转发,可修改消息内容。
问题4:Method Swizzling的原理是什么?使用时需要注意什么?
应答:原理是通过Runtime API交换两个Method的IMP,实现方法实现的偷换;注意事项:1. 在+load方法中执行,用dispatch_once保证只交换一次;2. 交换后需调用原方法实现,避免系统逻辑异常;3. 避免方法名重名,自定义方法加前缀;4. 原方法不存在时,需先添加方法再交换。
问题5:Category为什么不能直接添加成员变量?如何给Category添加属性?
应答:因为Category的底层结构中没有存储成员变量的字段,编译时不会修改原类的内存布局,因此不能直接添加成员变量;可通过Runtime的关联对象机制,给Category添加“伪属性”,本质是将属性值与对象关联存储在全局哈希表中。
问题6:SEL、IMP、Method三者的关系是什么?
应答:SEL是方法的唯一标识(名字),IMP是方法的具体实现指针(地址),Method是将SEL和IMP绑定起来的桥梁,Runtime通过SEL找到对应的Method,再通过Method获取IMP,从而调用方法。
问题7:元类是什么?作用是什么?
应答:元类是类对象的“类”,底层也是objc_class结构体;核心作用是存储类方法,类对象的isa指向元类,因此调用类方法时,Runtime会去元类中查找方法实现。
六、面试总结(核心提炼,快速背诵)
1. 底层核心:Runtime是C语言API,是OC动态性的核心,所有OC方法调用本质是消息传递(objc_msgSend);
2. 数据结构:重点记objc_object(isa)、objc_class(isa、superclass、cache、bits)、SEL/IMP/Method,以及元类的作用;
3. 核心机制:消息传递(流程必背)、消息转发(3步流程必背)、Method Swizzling(原理+注意事项+示例)、关联对象(API+策略);
4. 延伸考点:Category底层实现、weak指针底层、动态性体现、应用场景;
5. 面试关键:能说出核心机制的流程、原理,结合代码示例应答,重点掌握消息转发和Method Swizzling(面试必考)。