iOS开发基础18-深入理解 Objective-C Runtime 机制
Objective-C Runtime 深度解析:数据结构、消息发送与动态特性
Objective-C 是基于 C 语言、加入面向对象特性和消息传递机制的动态语言。Runtime 是其核心,负责方法调用、动态类型检查、方法解析与转发等。本文基于 objc4 开源代码,全面解析 Runtime 的数据结构、消息流程和高级功能。
一、核心数据结构
1. SEL — 方法选择器
SEL 是表示方法选择器的类型,本质是映射到方法名的唯一字符串指针:
typedef struct objc_selector *SEL;
获取方式:
SEL sel1 = @selector(methodName); // 编译器生成
SEL sel2 = sel_registerName("methodName"); // Runtime 函数注册
转换为字符串:
NSString *name = NSStringFromSelector(sel);
同一进程中,相同方法名的 SEL 是同一个指针(唯一且不可变),因此比较方法名应使用
sel1 == sel2而非字符串比较。
2. id — 通用对象指针
id 是通用类型指针,可表示任意 Objective-C 对象:
typedef struct objc_object *id;
objc_object 结构体的核心是 isa 指针:
struct objc_object {
Class isa; // 指向对象所属的类
};
通过 isa 指针可以找到类对象,进而获得方法列表、实例变量列表等信息。
arm64 下的 nonpointer isa:在 64 位架构中,
isa不再是纯指针,而是一个 64 位共用体(union),仅部分位存储类指针,其余位存储引用计数、弱引用标志、关联对象标志、析构状态等信息,以节省内存。通过isa & ISA_MASK可获取真实的类指针。
3. Class — 类对象
Class 是指向 objc_class 结构体的指针,类本质上也是对象:
typedef struct objc_class *Class;
struct objc_class {
Class isa; // 指向 Meta-class
Class superclass; // 指向父类
cache_t cache; // 方法缓存(哈希表)
class_data_bits_t bits; // 指向类数据(方法列表、属性列表、协议列表等)
};
Meta-class(元类)体系
Objective-C 中类方法和实例方法分开存储,类方法存储在 Meta-class 中:
实例对象 ──isa──▶ 类对象 ──isa──▶ Meta-class ──isa──▶ 根类 Meta-class ──isa──▶ 自己(闭环)
│
superclass
│
父类对象
- 实例的
isa→ 类对象(存储实例方法) - 类对象的
isa→ Meta-class(存储类方法) - Meta-class 的
isa→ 根类(NSObject)的 Meta-class - 根类 Meta-class 的
isa→ 自己,形成闭环 - 类对象和 Meta-class 都有
superclass指针,分别指向父类和父类的 Meta-class
4. Method — 方法结构
typedef struct objc_method *Method;
struct objc_method {
SEL method_name; // 方法选择器
char *method_types; // 方法类型编码(参数和返回值类型)
IMP method_imp; // 指向方法实现的函数指针
};
IMP — 方法实现指针
typedef void (*IMP)(id, SEL, ...);
IMP 直接指向方法实现的 C 函数,调用时可绕过消息发送流程,直接执行函数,性能更高。获取方式:
IMP imp = [self methodForSelector:@selector(methodName)];
5. Ivar — 实例变量
typedef struct objc_ivar *Ivar;
struct objc_ivar {
char *ivar_name; // 变量名
char *ivar_type; // 变量类型编码
int ivar_offset; // 变量在对象内存中的偏移量
};
通过 ivar_offset 可以直接从对象内存地址偏移读取实例变量,这也是 KVC 能直接访问实例变量的底层基础。
6. Cache — 方法缓存
cache 用于缓存已查找过的方法,避免每次调用都遍历方法列表,显著提升性能:
struct cache_t {
bucket_t *buckets; // 哈希桶数组
mask_t mask; // 掩码(容量 - 1,用于哈希取模)
mask_t occupied; // 已占用数量
};
查找方法时,先在 cache 中通过 SEL 哈希查找,命中则直接调用;未命中则遍历 methodLists,找到后缓存到 cache 中供下次使用。
7. objc_msgSend — 消息发送核心
id objc_msgSend(id self, SEL op, ...);
编译器将 [receiver message] 转换为 objc_msgSend(receiver, @selector(message))。该函数用汇编实现以追求极致性能,流程如下:
- 通过
receiver->isa获取类对象。 - 在类的
cache中查找方法(哈希查找)。 - 缓存未命中则在类的
methodLists中查找。 - 类中未找到则沿
superclass链向父类查找。 - 找到后将方法缓存到
cache,并跳转到其IMP执行。 - 整个继承链都未找到则进入消息转发流程。
二、消息发送与消息转发
1. self 与 super
self 是消息的实际接收者,super 是编译器标识符,告诉编译器从父类开始查找方法,但消息接收者仍是 self:
@implementation Son : Father
- (instancetype)init {
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class])); // Son
NSLog(@"%@", NSStringFromClass([super class])); // Son
}
return self;
}
@end
两者都打印 Son,因为 class 方法在 NSObject 中实现,返回的是消息接收者的实际类,而 super 调用的接收者仍然是 self(Son 实例)。
super的本质是objc_msgSendSuper,它从父类的方法列表开始查找,但self参数不变。
2. 隐藏参数 self 和 _cmd
每个 Objective-C 方法实现都有两个隐式参数:
- (void)exampleMethod {
NSLog(@"self: %@", self); // 消息接收者
NSLog(@"_cmd: %@", NSStringFromSelector(_cmd)); // 当前方法的 SEL
}
这两个参数由 Runtime 在调用方法时自动传入,使得方法实现可以动态访问调用者和方法自身。
3. 完整消息转发流程
当对象收到未实现的消息时,Runtime 提供三次"补救"机会,均失败后才崩溃:
对象收到未实现的消息
│
▼
① +resolveInstanceMethod:(类方法)
动态添加方法实现 → 返回 YES,重新发送消息
返回 NO → 进入下一步
│
▼
② -forwardingTargetForSelector:
返回非 nil 对象 → 消息转发到该对象
返回 nil → 进入下一步
│
▼
③ -methodSignatureForSelector:
返回非 nil → 创建 NSInvocation,调用 -forwardInvocation:
返回 nil → 调用 -doesNotRecognizeSelector:(崩溃)
① Method Resolution(方法解析)
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(testMethod)) {
class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
return YES; // 返回 YES 后 Runtime 会重新发送消息
}
return [super resolveInstanceMethod:sel];
}
// 动态添加的 C 函数实现
void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"动态方法实现");
}
类方法解析使用 + resolveClassMethod:。
② Fast Forwarding(快速转发)
将消息转发给另一个对象,轻量高效:
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(testMethod)) {
return self.alternateTarget; // 转发到备用对象
}
return [super forwardingTargetForSelector:aSelector];
}
③ Normal Forwarding(完整转发)
创建 NSInvocation 封装完整的消息(目标、选择器、参数),可修改参数后转发:
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(testMethod)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}
- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = [anInvocation selector];
if ([self.alternateTarget respondsToSelector:sel]) {
[anInvocation invokeWithTarget:self.alternateTarget];
} else {
[super forwardInvocation:anInvocation];
}
}
三、Runtime 高级功能
1. Associated Objects(关联对象)
为已有的类(尤其是 Category)动态添加属性,无需修改类的原始结构:
// API
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
id objc_getAssociatedObject(id object, const void *key);
void objc_removeAssociatedObjects(id object);
内存管理策略:
| 策略 | 对应属性修饰符 |
|---|---|
OBJC_ASSOCIATION_ASSIGN |
assign |
OBJC_ASSOCIATION_RETAIN_NONATOMIC |
strong, nonatomic |
OBJC_ASSOCIATION_COPY_NONATOMIC |
copy, nonatomic |
OBJC_ASSOCIATION_RETAIN |
strong(原子) |
OBJC_ASSOCIATION_COPY |
copy(原子) |
使用示例(Category 添加属性):
// UIViewController+Toast.h
@interface UIViewController (Toast)
@property (nonatomic, copy) NSString *toastMessage;
@end
// UIViewController+Toast.m
#import <objc/runtime.h>
static const void *kToastMessageKey = &kToastMessageKey;
@implementation UIViewController (Toast)
- (void)setToastMessage:(NSString *)toastMessage {
objc_setAssociatedObject(self, kToastMessageKey, toastMessage, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)toastMessage {
return objc_getAssociatedObject(self, kToastMessageKey);
}
@end
关联对象存储在全局的
AssociationsManager哈希表中,不占用对象本身的内存。对象dealloc时 Runtime 自动调用objc_removeAssociatedObjects清理。
2. Method Swizzling(方法交换)
在运行时交换两个方法的实现(IMP),用于修改已有方法行为、添加日志、统计埋点等:
#import <objc/runtime.h>
@implementation UIViewController (Logging)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method original = class_getInstanceMethod(self, @selector(viewDidAppear:));
Method swizzled = class_getInstanceMethod(self, @selector(log_viewDidAppear:));
method_exchangeImplementations(original, swizzled);
});
}
- (void)log_viewDidAppear:(BOOL)animated {
// 由于方法已交换,这里调用 log_viewDidAppear: 实际执行的是原始 viewDidAppear:
[self log_viewDidAppear:animated];
NSLog(@"页面显示: %@", NSStringFromClass([self class]));
}
@end
关键注意事项:
- 在
+ load中执行(类被加载到 Runtime 时自动调用,早于任何实例创建)。 - 使用
dispatch_once保证线程安全且只执行一次。 - 交换后,在自定义方法中调用"自己的方法名"实际执行的是原始实现(因为 SEL 和 IMP 的对应关系已交换)。
- 注意类簇(如
NSString、NSArray)的 Swizzling 可能影响多个内部类。
3. 面向切面编程(AOP)
基于 Method Swizzling 实现横切关注点(日志、性能监控、埋点),无需修改业务代码:
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method original = class_getInstanceMethod(self, @selector(targetMethod));
Method swizzled = class_getInstanceMethod(self, @selector(aop_targetMethod));
method_exchangeImplementations(original, swizzled);
});
}
- (void)aop_targetMethod {
// 前置处理:记录开始时间
CFTimeInterval startTime = CACurrentMediaTime();
// 调用原始方法(方法已交换,调用 aop_targetMethod 实际执行原始实现)
[self aop_targetMethod];
// 后置处理:计算耗时
NSLog(@"方法耗时: %.4fms", (CACurrentMediaTime() - startTime) * 1000);
}
业界优秀的 AOP 库:Aspects,基于 Runtime 消息转发实现,API 简洁,支持实例和类方法的 Hook。
四、Swift 与 Runtime
纯 Swift 类使用静态派发(直接调用或 vtable),不支持 Runtime 动态特性。继承自 NSObject 的 Swift 类可以使用 Runtime:
class Person: NSObject {
@objc dynamic func sayHello() { // dynamic 关键字强制使用消息发送
print("Hello")
}
}
let p = Person()
p.perform(Selector(("sayHello"))) // Runtime 消息发送
| 特性 | 纯 Swift 类 | 继承 NSObject 的 Swift 类 | Objective-C 类 |
|---|---|---|---|
| 消息发送 | 否(静态派发) | 是(@objc dynamic) |
是 |
| Method Swizzling | 否 | 是 | 是 |
| Associated Objects | 否 | 是 | 是 |
| KVO | 需手动实现 | @objc dynamic 属性支持 |
支持 |
五、总结
- 数据结构:
isa指针是 Runtime 的核心,通过它可以找到类对象、方法列表、实例变量等。arm64 下isa是优化过的 nonpointer 共用体。 - 消息发送:
objc_msgSend用汇编实现,通过isa → cache → methodLists → 父类的流程查找方法,找到后缓存并执行。 - 消息转发:未找到方法时依次经过
resolveInstanceMethod:→forwardingTargetForSelector:→methodSignatureForSelector:+forwardInvocation:,均失败后调用doesNotRecognizeSelector:崩溃。 - 动态特性:Associated Objects 为 Category 添加属性,Method Swizzling 交换方法实现,两者是 Runtime 最常用的高级功能。
- Swift 中:继承
NSObject并标记@objc dynamic才能使用 Runtime 动态特性,纯 Swift 类使用静态派发。
理解 Runtime 机制是排查崩溃(unrecognized selector)、实现 AOP、Hook 系统方法、优化性能的基础。

浙公网安备 33010602011771号