一、Runtime 是什么
Objective-C Runtime 是 Objective-C 运行时系统。它负责维护类、对象、方法、属性、协议等元数据,并在程序运行期间完成对象创建、消息发送、方法查找、动态方法解析和消息转发等工作。
Objective-C 的一条重要特征是:方法调用本质上是消息发送,而不是编译期确定的普通函数调用。
[person sayHello];
编译后可以近似理解为:
objc_msgSend(person, @selector(sayHello));
Runtime 让 Objective-C 具备以下动态能力:
- 运行时判断对象属于哪个类
- 根据
SEL查找方法实现 - 在运行期间增加或替换方法
- 动态创建类和属性
- 获取类的元数据
- 在对象释放时处理弱引用
- 通过关联对象为已有对象附加数据
需要注意,objc_msgSend 是理解 Runtime 的入口,但 Runtime 不等于只有消息发送。类结构、内存管理、KVO、关联对象和消息转发都属于运行时能力的一部分。
二、对象、Class、Meta-Class 和 isa
以以下代码为例:
@interface Person : NSObject
- (void)sayHello;
+ (void)run;
@end
Person *person = [[Person alloc] init];
[Person run];
它们之间的关系可以简化为:
person 实例对象
| isa
v
Person Class
| isa
v
Person Meta-Class
- 实例对象的
isa指向它所属的 Class。 - Class 保存实例方法列表、实例变量、属性、协议、父类和方法缓存等信息。
- Meta-Class 保存类方法。调用
[Person run]时,Runtime 会沿着Person类对象的isa找到 Person 的 Meta-Class,再查找run。 - 类对象本身也是对象,所以类对象同样有
isa。
isa 在现代 Objective-C Runtime 中通常不是一个简单的裸指针,而是可能包含标记位、引用计数相关信息等内容。具体布局受架构和运行时版本影响,业务代码不应依赖内部位域。
三、消息发送的完整过程
当执行:
[person sayHello];
Runtime 大致按以下顺序处理:
1. 找到接收者的 Class
通过对象的 isa 找到 Person Class。
2. 查找方法缓存
Runtime 会先在当前类的方法缓存中查找 sayHello 对应的 SEL。缓存命中后,直接取得 IMP,避免每次都遍历方法列表。
3. 查找方法列表
缓存没有命中时,Runtime 会在当前 Class 的方法列表中查找。如果仍然没有找到,就沿着 superclass 逐级向父类查找。
4. 找到 IMP 并缓存
IMP 是方法实现的函数地址。找到后,Runtime 通常会将结果写入缓存,下一次调用可以更快完成。
5. 执行方法实现
方法实现接收的隐藏参数包括:
id self
SEL _cmd
因此实例方法可以近似看作:
void sayHello(id self, SEL _cmd) {
// 方法实现
}
方法签名还会受到返回值和参数类型影响,不能在类型不匹配时随意把 IMP 强制转换调用,否则可能造成崩溃或数据损坏。
四、方法列表、SEL、IMP 和 Type Encoding
SEL
SEL 是方法名的唯一标识,例如:
SEL selector = @selector(sayHello);
SEL selector2 = NSSelectorFromString(@"sayHello");
相同的方法名通常对应相同的选择子,但 SEL 只表示方法名,不包含完整的方法实现和参数类型信息。
IMP
IMP 是函数指针,指向真正执行的方法代码:
IMP imp = [person methodForSelector:@selector(sayHello)];
imp(person, @selector(sayHello));
实际项目中不建议随意直接调用 IMP。只有在确认函数签名完全匹配时才这样做。
Type Encoding
Runtime 使用类型编码描述返回值和参数类型。常见编码包括:
v 表示返回 void,@ 表示对象参数,: 表示 _cmd/SEL
i 表示 int,q 表示 long long,B 表示 BOOL,f 表示 float,d 表示 double
例如 v@: 可以表示一个无返回值、接收 self 和 _cmd 的无参数方法。不同架构下结构体、长整型和指针大小可能不同,不能只凭经验拼接复杂编码。
五、动态方法解析
如果 Runtime 找不到方法实现,会先给类一次动态添加方法的机会:
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(logMessage)) {
class_addMethod(self,
sel,
(IMP)replacementLogMessage,
"v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}
对应的实现函数:
static void replacementLogMessage(id self, SEL _cmd) {
NSLog(@"dynamic method");
}
动态方法解析适合处理一些确实需要延迟决定实现的场景,例如消息代理、懒加载方法或兼容不同版本的 API。常规业务代码不应通过它掩盖拼写错误,因为这会降低代码可读性。
六、消息转发机制
动态方法解析仍然无法处理时,Runtime 会进入消息转发流程。
1. 快速转发
可以返回另一个能够处理该消息的对象:
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(trackEvent:)) {
return self.eventTracker;
}
return [super forwardingTargetForSelector:aSelector];
}
返回 nil 或返回 self 后,Runtime 会继续进行完整消息转发。不能把 NSNull 当作未处理消息的通用返回值,除非它本身确实实现了对应方法。
2. 完整消息转发
完整转发会先提供方法签名,再通过 NSInvocation 交给代码决定如何处理:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(trackEvent:)) {
return [self.eventTracker methodSignatureForSelector:aSelector];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
if ([self.eventTracker respondsToSelector:anInvocation.selector]) {
[anInvocation invokeWithTarget:self.eventTracker];
return;
}
[self doesNotRecognizeSelector:anInvocation.selector];
}
面试中可以这样概括:先动态解析,再快速转发,最后完整转发;都无法处理时才抛出 unrecognized selector 异常。
七、Associated Objects
关联对象可以在不修改类成员变量布局的情况下,为对象附加数据:
static const void *kNameKey = &kNameKey;
@implementation NSObject (UserInfo)
- (void)setUserName:(NSString *)userName {
objc_setAssociatedObject(self,
kNameKey,
userName,
OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)userName {
return objc_getAssociatedObject(self, kNameKey);
}
@end
常见场景:
- 给分类增加属性
- 给按钮、视图绑定业务回调
- 为第三方类保存少量状态
- 给手势或异步任务保存上下文
需要注意:
- 关联对象会增加对象的管理复杂度和内存开销。
- key 必须稳定且唯一,常用静态地址作为 key。
- 关联对象不适合作为大量数据的主要存储方案。
RETAIN、COPY、ASSIGN等策略应根据属性语义选择。
八、Method Swizzling
Method Swizzling 是在运行时交换两个方法实现:
Method originalMethod = class_getInstanceMethod(self, @selector(viewDidAppear:));
Method swizzledMethod = class_getInstanceMethod(self, @selector(xxx_viewDidAppear:));
method_exchangeImplementations(originalMethod, swizzledMethod);
交换后的实现中,调用原方法名实际上会进入新的实现:
- (void)xxx_viewDidAppear:(BOOL)animated {
[self xxx_viewDidAppear:animated]; // 交换后调用原实现
NSLog(@"页面出现");
}
适合场景包括统一埋点、日志、兼容性处理和给系统类补充行为。但它有明显风险:
- 影响范围可能是整个类,甚至影响所有业务模块。
- 多个库同时交换同一个方法时容易产生顺序和递归问题。
- 方法签名、初始化时机和线程安全处理不当会崩溃。
- 交换系统类方法会增加系统升级后的兼容风险。
生产代码中应优先使用公开扩展点、代理、子类或组合。确实需要 Swizzling 时,通常放在 +load 或受控初始化流程中,并保证只执行一次。
九、Runtime 获取类信息
Runtime 可以读取类的方法、属性、成员变量和协议:
unsigned int count = 0;
Method *methods = class_copyMethodList([Person class], &count);
for (unsigned int i = 0; i < count; i++) {
SEL selector = method_getName(methods[i]);
const char *types = method_getTypeEncoding(methods[i]);
NSLog(@"%s %s", sel_getName(selector), types);
}
free(methods);
常用 API:
class_getInstanceMethod
class_getClassMethod
class_copyMethodList
class_copyPropertyList
class_copyIvarList
class_copyProtocolList
class_addMethod
class_replaceMethod
method_getImplementation
method_setImplementation
class_copy... 系列通常返回需要调用 free 释放的 C 数组。使用时应注意内存管理,并且不要依赖私有成员变量名称或系统内部布局。
十、KVO 为什么和 Runtime 有关
KVO 默认实现会在运行时为被观察对象创建一个动态子类,并把对象的 isa 指向这个动态子类。这个子类会重写被观察属性的 setter,在 setter 前后自动发送通知。
可以用以下方式理解:
原对象 Person
| isa 修改
v
NSKVONotifying_Person 动态子类
| 重写 setter
v
willChangeValueForKey / didChangeValueForKey
所以在调试 KVO 时,打印对象的实际类名可能看到 NSKVONotifying_...。这不是对象被替换成了另一个业务对象,而是 Runtime 创建了动态子类并改变了对象的类指针指向。
KVO 的实现细节属于系统行为,业务代码应优先使用公开的观察 API,不应依赖动态子类名称。
十一、weak 和 Runtime 的关系
weak 不是简单地把指针置为 nil。ARC 和 Objective-C Runtime 会维护弱引用表,在对象销毁时找到指向该对象的弱引用,将它们自动清零。
弱引用的大致过程是:
- 对象被注册到弱引用相关的数据结构中。
weak指针指向对象,但不增加对象的引用计数。- 对象销毁时,Runtime 清理相关弱引用。
- 后续读取
weak指针得到nil,避免继续访问已释放对象。
因此,weak 与 Runtime 的内存管理能力密切相关,但开发者不应直接操作 weak 表或依赖其私有实现细节。常见的 delegate、定时器回调、闭包捕获关系都可以通过正确使用 weak 避免循环引用;weak 本身不能解决所有生命周期问题,也不能替代对线程安全和对象持有关系的分析。
十二、私有 UIKit 成员的访问风险
有些代码会通过 Runtime 访问 UIKit 的私有成员,例如:
id placeholderLabel = [textField valueForKey:@"_placeholderLabel"];
这类做法可能在某个系统版本中有效,但存在明显风险:
- 私有成员名和类型可能随系统升级变化。
- 可能触发 KVC 异常或类型不匹配。
- App Store 审核和维护风险更高。
- 一旦内部实现改变,样式可能失效或直接崩溃。
更稳妥的方案是使用公开 API、自己添加 UILabel、通过子类重写公开方法,或使用系统支持的 attributed placeholder 等能力。Runtime 能做到不代表这种做法适合生产环境。
十三、Swift 和 Objective-C Runtime
Swift 并没有“完全没有 Runtime”。Swift 主要使用 Swift 自己的编译器和运行时机制,但在与 Objective-C、Foundation、UIKit 互操作时,仍然可以使用 Objective-C Runtime。
Swift 中使用 Objective-C Runtime
类需要继承 NSObject,成员通常需要加 @objc,动态派发可以使用 dynamic:
final class EventHandler: NSObject {
@objc dynamic func handleEvent() {
print("event")
}
}
常见入口包括:
NSObjectProtocol和NSObject@objc暴露给 Objective-Cdynamic强制使用动态派发#selector获取选择子Mirror做 Swift 反射objc_getAssociatedObject和objc_setAssociatedObject做关联对象
需要区分 Mirror 和 Objective-C Runtime:Mirror 是 Swift 的反射 API,适合查看部分 Swift 值的结构;它不等同于 Objective-C 的类、消息发送和 Swizzling 机制。
Swift 的结构体、枚举、泛型类型通常不能直接按 Objective-C 对象方式使用 Runtime。即使声明了 @objc,也只有符合 Objective-C 表达能力的成员能够暴露给 Objective-C。
十四、Runtime 在项目中的实际场景
1. KVO 和属性观察
系统在运行时创建子类并重写 setter,实现属性变化通知。
2. Associated Objects
分类中增加属性,为控件绑定回调、状态或业务对象。
3. Method Swizzling
统一页面埋点、日志、兼容系统差异,但要严格控制影响范围。
4. 消息转发和代理转发
将方法转交给其他对象,或实现多代理、事件路由和动态代理。
5. JSON 或字典转模型
通过属性列表和类型信息减少重复映射代码。生产项目通常会结合成熟的模型转换库,而不是手写全部 Runtime 逻辑。
6. 自动埋点
在页面生命周期或控件事件中统一插入统计逻辑。使用时必须避免重复交换和影响业务原有实现。
7. 动态注册类和插件化
在框架或组件系统中根据配置查找类、创建对象并调用约定方法。
8. 弱引用和内存管理
Runtime 参与 weak 引用的注册、清零和对象销毁过程。
十五、常见面试问法与回答模板
问:Runtime 是什么?
答:Objective-C Runtime 是运行时系统,负责对象、类、方法、属性和协议等元数据的管理,并在运行期间完成消息发送、方法查找、动态方法解析和消息转发。Objective-C 的方法调用本质是向对象发送消息,因此可以在运行时动态决定具体实现。
问:objc_msgSend 做了什么?
答:它根据接收者的 isa 找到 Class,先查方法缓存,再查当前类和父类的方法列表,找到对应的 IMP 后执行;如果找不到,会依次进入动态方法解析、快速转发和完整消息转发。
问:消息转发有几步?
答:通常分为动态方法解析、快速消息转发和完整消息转发。动态解析可以动态添加方法;快速转发返回另一个对象;完整转发通过方法签名和 NSInvocation 决定最终处理对象。全部失败后抛出无法识别选择子的异常。
问:KVO 为什么和 Runtime 有关?
答:KVO 默认会动态创建一个子类,重写被观察属性的 setter,并将对象的 isa 指向这个动态子类,由重写后的 setter 发送变化通知。
问:weak 为什么能自动置 nil?
答:weak 指针不增加引用计数,Runtime 会维护弱引用关系。对象销毁时,Runtime 清理并将相关 weak 指针置为 nil,从而避免访问悬空指针。
问:Swizzling 有什么风险?
答:它会改变方法实现的全局行为,多个库同时交换、重复交换、签名不匹配或系统内部实现变化都可能导致递归、逻辑异常或崩溃。因此应优先使用公开扩展点,并保证交换只执行一次、影响范围可控。
十六、关键词速记
Runtime:运行时系统
消息发送:objc_msgSend
SEL:方法名标识
IMP:方法实现函数地址
方法缓存:加速方法查找
动态解析:resolveInstanceMethod:
快速转发:forwardingTargetForSelector:
完整转发:methodSignatureForSelector: + forwardInvocation:
关联对象:objc_setAssociatedObject
方法交换:method_exchangeImplementations
KVO:动态子类 + 重写 setter
weak:Runtime 维护弱引用并在销毁时清零
十七、一句话总结
Objective-C Runtime 让对象的类型、方法实现和消息处理过程可以在运行期间动态决定;理解 isa、objc_msgSend、方法缓存、动态解析、消息转发、关联对象、Swizzling、KVO 和 weak,就能覆盖 Runtime 面试中最常见的核心问题。真正使用时还要同时考虑可维护性、线程安全、系统兼容性和公开 API 边界。