本大纲针对资深 iOS 工程师,聚焦Runtime核心原理、底层实现、实战应用及面试高频考点,兼顾技术深度与面试适配性,涵盖“基础原理-底层结构-核心机制-实战场景-面试真题”五大模块,助力系统复习与面试突破。
一、Runtime核心基础(面试基础必问)
1.1 Runtime本质与作用
核心定义:Runtime是Objective-C的运行时系统,包含一套C语言API,负责OC语言的动态特性实现(动态类型、动态绑定、动态加载),是OC语言“编译时确定部分,运行时确定部分”的核心支撑
核心价值:
- 使OC具备动态特性:运行时确定对象类型、绑定方法实现、修改类结构
- 为上层框架提供底层支撑:KVO、Category、Block、属性自动合成等功能的底层实现依赖Runtime
- 支持跨语言交互:OC与C/C++、Swift的混编底层衔接
Runtime源码关联:熟悉objc4源码结构(开源地址:opensource.apple.com),核心文件:objc.h、objc-runtime.h、runtime.h
面试考点:Runtime的核心作用?OC为什么是动态语言?Runtime与编译期的区别?
1.2 编译期与运行时的区别
编译期:完成词法/语法分析、类型检查、生成IR、编译为Mach-O二进制文件,确定静态可知信息(如类的声明、方法声明)
运行时:由Runtime系统主导,完成类的初始化、对象创建、方法查找与调用、动态修改类结构等动态操作
典型案例对比:
- 编译期:`int a = 10;` 确定变量类型与初始值
- 运行时:`id obj = [NSObject new];` 对象类型仅在运行时确定;`[obj performSelector:@selector(test)]` 方法调用在运行时绑定实现
- 面试考点:举一个编译期与运行时差异的例子?Runtime如何弥补编译期的局限性?
二、Runtime底层核心结构(深度核心,面试高频)
2.1 objc_class结构体(Runtime核心)
-
最新objc_class结构(objc4-818.2版本):
`struct objc_class {
Class _Nonnull isa OBJC_ISA_AVAILABILITY; // 指向元类
if !OBJC2
Class _Nullable super_class OBJC2_UNAVAILABLE; // 父类
const char * _Nonnull name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE;
long info OBJC2_UNAVAILABLE;
long instance_size OBJC2_UNAVAILABLE;
struct objc_ivar_list * _Nullable ivars OBJC2_UNAVAILABLE; // 实例变量列表
struct objc_method_list * _Nullable * _Nullable methodLists OBJC2_UNAVAILABLE; // 方法列表(可变)
struct objc_cache * _Nonnull cache OBJC2_UNAVAILABLE; // 方法缓存
struct objc_protocol_list * _Nullable protocols OBJC2_UNAVAILABLE; // 协议列表
endif
} OBJC2_UNAVAILABLE;`
- 核心成员深度解析:
- isa指针:
- 作用:关联类与实例,实例的isa指向类,类的isa指向元类(Meta Class),元类的isa指向根元类,根元类的isa指向自身
- isa_mask:64位系统下isa包含更多信息(如引用计数、是否被弱引用),通过掩码提取类信息(OBJC_ISA_MASK = 0x00007ffffffffff8ULL)
- 面试考点:isa指针的指向链条?元类的作用?如何通过isa获取类信息?
- cache_t(方法缓存):
- 结构:哈希表实现,存储最近调用的方法(SEL-IMP映射),目的是提升方法查找效率
- 缓存策略:LRU淘汰机制,当缓存满时清理不常用方法;方法调用时优先查缓存,未命中再查方法列表
- 面试考点:cache_t的实现原理?为什么要设计方法缓存?缓存的淘汰策略是什么?
- methodLists:可变数组,存储类的实例方法/类方法,Category的方法会被附加到该列表(编译期不合并,运行时合并)
- ivars:实例变量列表,存储成员变量的类型、名称、偏移量,属性的实例变量由编译器自动合成并加入该列表
2.2 元类(Meta Class)深度理解
核心定义:元类是“类的类”,类对象的isa指向元类,元类中存储类方法(+方法)的实现
元类的继承链条:
- 子类元类的父类是父类的元类
- 根类(NSObject)的元类的父类是根类本身
- 根元类的isa指向自身
核心作用:统一方法调用机制(实例调用-方法查类,类调用-方法查元类),使类方法的调用逻辑与实例方法一致
面试考点:元类存在的意义?类方法存储在哪里?如何调用元类中的方法?
2.3 核心数据结构(SEL、IMP、Method、Ivar、Property)
-
SEL(选择器):
本质:字符串的哈希值,唯一标识方法名(相同方法名的SEL相同,与参数类型无关)
创建与获取:
@selector(test)编译期创建、NSSelectorFromString(@"test")运行时创建、sel_getName(SEL)获取方法名字符串注意:SEL仅区分方法名,不区分参数和返回值(如
- (void)test和- (int)test的SEL相同,会导致方法覆盖)
IMP(方法实现指针):
- 本质:函数指针,指向方法的具体实现代码,格式为`id (*IMP)(id, SEL, ...)`(第一个参数是self,第二个是SEL,后续是方法参数)
- 核心价值:通过IMP可直接调用方法,绕过Runtime的方法查找流程,提升性能
- Method(方法结构体):
- 结构:包含SEL(方法名)、IMP(方法实现)、types(方法签名,描述参数和返回值类型)
- 核心API:`class_getInstanceMethod`(获取实例方法)、`class_getClassMethod`(获取类方法)、`method_getImplementation`(获取IMP)
- Ivar(实例变量):
- 结构:存储成员变量的名称、类型、偏移量(offset),通过偏移量可直接访问成员变量的值
- 核心API:`class_copyIvarList`(获取类的所有实例变量)、`ivar_getName`(获取变量名)、`object_getIvar`(获取变量值)
- Property(属性):
- 与Ivar的区别:Property是封装后的成员变量,包含setter/getter方法,编译器自动合成实例变量(_属性名)
- 核心API:`class_copyPropertyList`(获取类的所有属性)、`property_getName`(获取属性名)、`property_getAttributes`(获取属性特性,如nonatomic、strong)
- 面试考点:SEL与IMP的关系?Method结构体包含哪些核心信息?如何通过Runtime获取类的属性和成员变量?
三、Runtime核心机制(深度难点,面试核心)
3.1 消息发送机制(objc_msgSend)
- 核心流程(从调用[obj test]到方法执行):
1. 快速查找:通过obj的isa找到类,先查类的cache_t缓存,若命中直接获取IMP执行
2. 慢速查找:缓存未命中时,遍历类的methodLists查找方法,未找到则沿父类链向上查找(直到NSObject)
3. 动态方法解析:若未找到方法,调用`+ (BOOL)resolveInstanceMethod:(SEL)sel`(实例方法)/`+ (BOOL)resolveClassMethod:(SEL)sel`(类方法),允许动态添加方法实现
4. 消息转发:解析失败后,进入消息转发流程:
- 第一步:`- (id)forwardingTargetForSelector:(SEL)aSelector`,返回可处理该消息的对象(快速转发)
- 第二步:`- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector`,返回方法签名;再调用`- (void)forwardInvocation:(NSInvocation *)anInvocation`,手动转发消息(完整转发)
5. 消息转发失败:调用`doesNotRecognizeSelector:`,抛出异常崩溃
- 深度细节:
- objc_msgSend的优化:汇编实现快速查找流程,减少函数调用开销;缓存命中时直接跳转IMP执行
- 父类链查找顺序:子类→父类→...→NSObject→nil
- 类方法的查找:类对象的isa指向元类,查找流程与实例方法一致,只是查找的是元类及其父类链
- 面试考点:详细描述objc_msgSend的完整流程?动态方法解析与消息转发的区别?如何通过消息转发避免“unrecognized selector sent to instance”崩溃?
3.2 动态方法解析与消息转发实战
-
动态方法解析案例:
`+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(test)) {
// 动态添加方法实现
class_addMethod(self, sel, (IMP)dynamicTest, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
void dynamicTest(id self, SEL _cmd) {
NSLog(@"动态解析的test方法");
}`
消息转发案例(快速转发):
- (id)forwardingTargetForSelector:(SEL)aSelector { if (aSelector == @selector(test)) { return [[OtherClass alloc] init]; // 由OtherClass处理test方法 } return [super forwardingTargetForSelector:aSelector]; }消息转发案例(完整转发):
`- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(test)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}(void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = anInvocation.selector;
OtherClass *other = [[OtherClass alloc] init];
if ([other respondsToSelector:sel]) {
[anInvocation invokeWithTarget:other];
} else {
[super forwardInvocation:anInvocation];
}
}`面试考点:动态方法解析和消息转发的应用场景?快速转发和完整转发的区别?如何实现一个通用的消息转发机制避免崩溃?
3.3 Category底层实现与加载机制
底层结构(category_t):
struct category_t { const char *name; // 所属类名 classref_t cls; // 所属类(运行时绑定) struct method_list_t *instanceMethods; // 实例方法列表 struct method_list_t *classMethods; // 类方法列表 struct protocol_list_t *protocols; // 协议列表 struct property_list_t *instanceProperties; // 实例属性列表 };加载机制(编译期→运行期):
1. 编译期:Category被编译为category_t结构体,存储在Mach-O的__DATA段,此时未与所属类关联
2. 运行期(dyld加载阶段):
- 通过runtime的`_objc_init`函数初始化,遍历所有category_t
- 将Category的方法列表、协议列表、属性列表合并到所属类的对应列表中
- 合并规则:Category的方法排在类原有方法之后,多个Category的合并顺序由编译顺序决定(最后编译的Category方法优先)
- Category与Class Extension的区别:
- Class Extension(类扩展):编译期合并到类中,可添加私有属性、方法声明,属于类的一部分
- Category:运行期合并到类中,不能添加实例变量(可通过关联对象间接添加),主要用于扩展方法
- 面试考点:Category的底层实现?Category的方法为什么会覆盖类的原有方法?如何解决多个Category方法同名的优先级问题?Category能否添加实例变量?为什么?
3.4 关联对象(Associated Objects)
- 核心原理:
- Category不能直接添加实例变量,关联对象通过Runtime API在运行时为对象动态附加“属性”,本质是将对象与关联值存储在全局哈希表中
- 底层结构:系统维护一个全局哈希表,key是对象的指针地址,value是另一个哈希表(存储关联的key-value对)
-
核心API:`// 设置关联对象
void objc_setAssociatedObject(id _Nonnull object, const void * _Nonnull key,
id _Nullable value, objc_AssociationPolicy policy);
// 获取关联对象
id _Nullable objc_getAssociatedObject(id _Nonnull object, const void * _Nonnull key);
// 移除所有关联对象
void objc_removeAssociatedObjects(id _Nonnull object);`
- 关联策略(objc_AssociationPolicy):
- ASSIGN:弱引用,不持有value,value释放后变为野指针
- RETAIN_NONATOMIC:强引用,非原子性
- COPY_NONATOMIC:拷贝,非原子性
- RETAIN:强引用,原子性
- COPY:拷贝,原子性
- 注意事项:
- key的选择:推荐使用`static const void *key = &key;`(唯一且不占用内存),避免使用字符串(可能存在哈希冲突)
- 内存管理:关联对象的生命周期与被关联对象一致,被关联对象释放时,关联对象会根据策略自动释放(强引用策略会释放value)
- 面试考点:关联对象的底层实现原理?关联策略与@property属性修饰符的对应关系?关联对象是否会导致内存泄漏?如何避免?
3.5 Method Swizzling(方法交换)
核心原理:通过Runtime API交换两个Method的IMP指针,实现“钩子”效果,在不修改原有代码的情况下拦截方法调用
-
标准实现(线程安全):
`+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [self class];
SEL originalSel = @selector(test);
SEL swizzledSel = @selector(swizzledTest);Method originalMethod = class_getInstanceMethod(cls, originalSel); Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel); // 先尝试添加方法,避免原有方法不存在(如父类的方法) BOOL didAddMethod = class_addMethod(cls, originalSel, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod)); if (didAddMethod) { class_replaceMethod(cls, swizzledSel, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod)); } else { method_exchangeImplementations(originalMethod, swizzledMethod); }});
}` 关键注意事项:
- 线程安全:在+load方法中执行,且用dispatch_once保证唯一执行(+load在dyld加载时执行,单线程)
- 避免递归:交换后的方法中若调用原方法名,会触发交换后的方法,需通过`class_getInstanceMethod`获取原IMP直接调用避免递归
- 父类与子类:Method Swizzling仅影响当前类,若子类未重写方法,交换子类的方法会影响父类方法的调用
应用场景:埋点统计、崩溃监控、性能监控、方法拦截与替换(如拦截UIButton的点击事件)
面试考点:Method Swizzling的底层原理?为什么要在+load方法中实现?如何保证线程安全?如何避免方法交换后的递归调用?
四、Runtime与上层框架的关联(资深工程师必备)
4.1 KVO底层实现(基于Runtime)
- 核心原理:
- 当对象被添加KVO监听时,Runtime动态创建该对象所属类的子类(NSKVONotifying_XXX)
- 重写子类的setter方法,在setter中触发`willChangeValueForKey:`和`didChangeValueForKey:`,通知观察者
- 将对象的isa指针指向动态子类,使对象变为子类的实例
与Runtime的关联:动态创建类(
objc_allocateClassPair)、重写方法(class_addMethod)、修改isa指针面试考点:KVO的底层实现原理?为什么KVO只能监听属性的setter方法?如何手动触发KVO?
4.2 Block底层与Runtime的关联
底层结构:Block本质是一个包含isa指针的结构体,属于对象类型,isa指向_NSConcreteStackBlock/_NSConcreteMallocBlock/_NSConcreteGlobalBlock
与Runtime的关联:
- Block的copy操作:栈Block拷贝到堆时,通过Runtime的`_Block_copy`函数处理,自动retain捕获的对象
- Block的销毁:堆Block释放时,通过`_Block_dispose`函数释放捕获的对象
- Block的调用:本质是调用Block结构体中的IMP指针,遵循objc_msgSend的调用逻辑
- 面试考点:Block的底层结构?Block捕获self时为什么会导致循环引用?__block修饰符的作用(底层原理)?
4.3 属性自动合成与Runtime
自动合成流程:编译器在编译期为@property自动合成实例变量(_属性名)和setter/getter方法,运行时通过Runtime将实例变量添加到类的ivars列表,setter/getter方法添加到methodLists
手动实现setter/getter的影响:若手动实现setter和getter,编译器不会自动合成实例变量,需手动声明或通过
@synthesize指定面试考点:@property的自动合成原理?@synthesize和@dynamic的区别?如何手动实现一个atomic的setter/getter方法?
五、面试高频真题与解答思路
5.1 基础类真题
真题1:OC的动态特性有哪些?Runtime如何支撑这些特性?
解答思路:先列出动态特性(动态类型、动态绑定、动态加载),再分别说明Runtime的支撑机制(动态类型通过isa判断类、动态绑定通过objc_msgSend运行时绑定方法、动态加载通过Runtime加载类/分类)真题2:SEL、IMP、Method三者的关系?
解答思路:SEL是方法名标识,IMP是方法实现指针,Method是包含SEL和IMP的结构体;通过SEL可找到对应的Method,再从Method中获取IMP执行方法
5.2 核心机制真题
真题1:详细描述objc_msgSend的完整流程,若方法未找到会如何处理?
解答思路:按“快速查找→慢速查找→动态方法解析→消息转发→崩溃”的顺序展开,重点说明每个阶段的核心操作和API,体现对底层流程的理解真题2:Method Swizzling的实现步骤和注意事项?如何避免线程安全问题?
解答思路:先说明核心原理(交换IMP),再列出标准实现代码,重点解释+load方法和dispatch_once的作用,分析线程安全、避免递归、父类子类影响等注意事项真题3:关联对象的底层实现?为什么Category不能直接添加实例变量?
解答思路:关联对象通过全局哈希表存储;Category不能添加实例变量是因为编译期类的ivars列表已固定,运行时无法修改类的内存布局,而关联对象是额外的哈希表存储,不影响类的原有结构
5.3 实战应用真题
真题1:如何通过Runtime实现一个通用的崩溃监控工具(拦截unrecognized selector崩溃)?
解答思路:利用消息转发机制,通过Category为NSObject添加forwardingTargetForSelector:或methodSignatureForSelector:/forwardInvocation:方法,拦截未识别的SEL,记录崩溃信息并返回默认处理对象,避免崩溃真题2:如何通过Runtime获取一个类的所有属性和成员变量,并修改私有成员变量的值?
解答思路:使用class_copyPropertyList获取属性,class_copyIvarList获取成员变量;通过ivar_getOffset获取私有成员变量的偏移量,再通过object_getIvar/object_setIvar修改值真题3:KVO的底层实现与Runtime的关系?如何自定义KVO?
解答思路:说明KVO动态创建子类、重写setter、修改isa指针的流程,关联Runtime的动态类创建、方法添加API;自定义KVO可通过Runtime手动创建子类、重写setter、添加监听逻辑实现
复习建议:1. 结合objc4源码阅读核心结构(objc_class、category_t)和核心函数(objc_msgSend、method_exchangeImplementations),加深底层理解;2. 动手实现关键机制(Method Swizzling、消息转发、关联对象),验证原理;3. 梳理“底层原理→上层应用→面试考点”的关联逻辑,形成知识体系。
(注:文档部分内容可能由 AI 生成)