iOS八股文之 Runtime
一、Runtime是啥
这个看苹果文档的描述即可:
Objective-C 语言尽可能将诸多决策从编译时和链接时推迟到运行时。只要有可能,它都会以动态方式处理事务。
这意味着该语言不仅需要编译器,还需要一个运行时系统来执行已编译的代码。
运行时系统就像是Objective-C 语言的一种操作系统,正是它让这种语言能够正常运行。
runtime 主要用 C 语言编写(如objc.h、runtime.h中的 API),核心逻辑(如objc_msgSend)用汇编编写,目的是提升性能(汇编能直接操作寄存器,减少函数调用开销)。
总之,OC动态处理事务的方式决定了运行时的诞生;本质上是一套基于 C语言的 API 和 数据结构,负责在程序运行时处理类、对象、方法调用等底层操作。
二、Runtime有啥东西
1. 关于实例(instance)、类、(Class)、元类(Meta-Class)的认识
- 关于isa指针
– 他们的 isa指针 指向:实例 → 类 → 元类 → 根元类(NSObject 的元类),(根元类指向自己);
– 这样形成 isa 链表。这是 “对象能找到自己方法” 的基础。 - 关于类的结构定义,内部包含 3 个关键部分
– super_class:指向父类,用于继承链查找;
– cache:缓存最近调用的方法,避免每次查找都遍历方法列表,提升性能;
– bits:存储类的方法列表(method_list)、属性列表(ivar_list)、协议列表(protocol_list)等。 - 关于元类,类方法的 “归属地”
– 实例方法存储在 “类对象” 中,类方法存储在 “元类” 中;
– 当调用类方法[Class doClassMethod]时,runtime 会通过 “类对象的 isa” 找到元类,再从元类的方法列表中查找该类方法。
2. 消息机制:OC 的 “调用方法” 本质是 “发消息”
OC 中[obj doSomething]的本质,不是直接调用方法,而是通过 runtime 发送消息,流程分 3 步:
- 消息发送(objc_msgSend):通过obj的isa找到类对象,先查cache,再查method_list;若没找到,通过super_class向上遍历父类,直到根类(NSObject)。
- 动态方法解析(resolve):若遍历完没找到方法,会先调用
+resolveInstanceMethod:(实例方法)或+resolveClassMethod:(类方法)
,允许开发者 “动态添加” 该方法(比如给分类动态补方法)。 - 消息转发(forwarding):若解析失败,会进入转发流程:先通过
-forwardingTargetForSelector:
找 “替代对象” 处理消息;若没找到,再通过-methodSignatureForSelector:和-forwardInvocation:
手动处理消息(比如打印日志、返回默认值),若这步也没处理,才会崩溃(unrecognized selector)
。
3. 动态操作 API:开发者能主动修改的行为
这是 runtime 最常被使用的部分,核心 API 分 3 类:
- 动态操作方法:class_addMethod(添加方法)、class_replaceMethod(替换方法)、method_exchangeImplementations(交换方法实现,即 “Method Swizzling”)。
- 动态操作属性:class_addIvar(添加成员变量,仅能在 “动态创建类” 时用)、objc_setAssociatedObject/objc_getAssociatedObject(关联对象,给分类加 “伪属性”)。
- 动态操作类:objc_allocateClassPair(创建类)、objc_registerClassPair(注册类)、objc_getClass/object_getClass(获取类对象)。
三、Runtime有啥用(应用举例)
1. 分类添加 “属性”:关联对象(Associated Object)
问题:OC 分类默认不能添加成员变量,直接写@property只会生成 getter/setter 声明,没有实现;
解决方案:用objc_setAssociatedObject
和 objc_getAssociatedObject
实现属性存储;
比如:给 UIButton 加 “点击回调 Block”:
objc
// UIButton+Block.h
@interface UIButton (Block)
@property (nonatomic, copy) void(^clickBlock)(UIButton *);
@end// UIButton+Block.m
@implementation UIButton (Block)
- (void)setClickBlock:(void (^)(UIButton *))clickBlock {objc_setAssociatedObject(self, @selector(clickBlock), clickBlock, OBJC_ASSOCIATION_COPY_NONATOMIC);[self addTarget:self action:@selector(btnClick) forControlEvents:UIControlEventTouchUpInside];
}
- (void(^)(UIButton *))clickBlock {return objc_getAssociatedObject(self, @selector(clickBlock));
}
- (void)btnClick {if (self.clickBlock) {self.clickBlock(self);}
}
@end
2. 方法交换:埋点、防崩溃、统一拦截
场景 1:给所有 UIViewController 的viewDidAppear:加页面曝光埋点,无需每个 VC 都写代码;
@implementation UIViewController (Track)
+ (void)load {// 确保只执行一次(load方法会被父类/子类多次调用,需过滤)static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{// 1. 获取原方法和自定义方法Method originalMethod = class_getInstanceMethod(self, @selector(viewDidAppear:));Method swizzledMethod = class_getInstanceMethod(self, @selector(swizzled_viewDidAppear:));// 2. 交换方法实现method_exchangeImplementations(originalMethod, swizzledMethod);});
}
// 自定义方法:加埋点逻辑
- (void)swizzled_viewDidAppear:(BOOL)animated {// 先调用原方法(此时swizzled方法已经和原方法交换,调用swizzled方法等于调用原方法)[self swizzled_viewDidAppear:animated];// 埋点逻辑NSString *pageName = NSStringFromClass([self class]);[TrackManager logPageExpose:pageName];
}
@end
3. JSON 模型转换:自动映射属性
主流框架(如 MJExtension、YYModel)的核心原理:通过 runtime 遍历模型类的ivar_list(成员变量列表),获取属性名,再和 JSON 的 key 匹配,自动赋值;
关键 API:class_copyIvarList(获取成员变量列表)、ivar_getName(获取成员变量名)、object_setIvar(给成员变量赋值)。
4. KVO 底层支撑:动态生成 “中间类”
当给对象添加 KVO 时,runtime 会动态生成一个 “中间类”(如NSKVONotifying_XXX),让原对象的isa指向这个中间类;
中间类会重写setter方法
,在赋值时触发observeValueForKeyPath:ofObject:change:context:,这是 KVO 能监听属性变化的底层逻辑。
四、Runtime在实践中常见问题(持续收集中)
1. 方法交换(Method Swizzling)的 “失效” 或 “重复交换”
Bug 原因:
没在+load方法中执行交换(+initialize会延迟调用,可能导致交换时机晚于方法调用);
没加dispatch_once_t,导致子类 / 父类重复交换,覆盖原实现;
解决方案:
必须在+load方法中执行交换(+load会在类加载时调用,且每个类只调用一次);
用dispatch_once确保交换逻辑只执行一次(如上文实例中的代码)。
2. 关联对象的 “内存泄漏” 或 “野指针”
Bug 原因:
关联对象的内存策略选错,比如用OBJC_ASSOCIATION_RETAIN关联 “self”,导致循环引用(如给 VC 加关联对象,值是 VC 的 Block);
关联对象没主动移除,导致对象释放后仍持有资源;
解决方案:
关联 Block 时用OBJC_ASSOCIATION_COPY_NONATOMIC(Block 拷贝后才能安全存储);
若关联对象是 “临时资源”,在对象销毁时(如 VC 的dealloc)用objc_removeAssociatedObjects(self)移除;
避免关联 “self”,若必须关联,用__weak typeof(self) weakSelf = self打破循环引用。
3. 消息转发未处理导致的 “隐性崩溃”
Bug 原因:
动态调用方法时(如performSelector:withObject:),没判断对象是否能响应该方法,导致消息转发流程走到最后仍未处理,触发unrecognized selector sent to instance;
解决方案:
调用前用respondsToSelector:判断对象是否能响应方法;
若需动态处理,重写+resolveInstanceMethod:或-forwardingTargetForSelector:,给消息一个 “兜底” 处理(如返回空值、打印日志)。
4. KVO 未移除导致的 “崩溃”
Bug 原因:
KVO 底层依赖 runtime 动态生成的中间类,若观察者销毁前没调用removeObserver:forKeyPath:,中间类仍会尝试给观察者发通知,导致野指针崩溃;
解决方案:
在观察者的dealloc方法中强制移除 KVO 监听;
用@try @catch包裹removeObserver(避免重复移除导致崩溃)。
五、Runtime 的一些其他相关(持续补充中)
1. runtime 与 Swift 的关系
- Swift 有自己的运行时(Swift Runtime),但当 Swift 代码中用@objc修饰类、方法、属性时,会桥接到 Objective-C runtime,支持动态特性(如performSelector、KVO);
- 纯 Swift 类(不继承 NSObject,且不加@objc)不支持 Objective-C runtime,无法动态修改行为。
2. runtime 的性能影响
动态操作(如objc_msgSend、关联对象)比静态调用(如 C 函数、Swift 直接调用方法)慢,因为需要额外的查找、遍历逻辑;所以避免在 “高频调用场景”(如tableView:cellForRowAtIndexPath:)中频繁使用 runtime 动态操作,优先用静态方法;若必须用,尽量缓存结果(如缓存遍历到的属性列表)。
3. App Store 审核风险
动态创建类(objc_allocateClassPair)、修改私有方法实现等操作,可能被 App Store 判定为 “违规修改系统行为”,存在拒审风险;所以要注意仅在 “必要场景”(如模型转换、埋点)使用 runtime,避免修改系统类(如 NSObject、UIView)的私有方法。