iOS 开发中的 Runtime

由浅入深,从基本概念到源码解析,带你全面理解 Objective-C 运行时机制


一、什么是 Runtime?

1.1 从「面向对象」说起

在 C++ 这类静态语言中,方法的调用在编译期就确定了,编译器会把方法调用翻译成确定的函数地址。而 Objective-C 是一门「消息型」语言,方法调用在编译期只是生成了「发消息」的代码,真正要调用哪个方法,要到运行期才能确定。

这种「推迟到运行时再决定」的能力,就是 Runtime 的精髓。

1.2 Runtime 是什么?

Runtime 是 Objective-C 的运行时系统,是一套用 C 和汇编实现的底层库。它负责:

  • 类与对象的创建、布局
  • 方法的查找、派发(Message Dispatch)
  • 消息传递机制(Message Passing)
  • 动态添加/修改类、方法、属性
  • 方法交换(Method Swizzling)
  • KVO、KVC、Block 等特性的底层支撑

可以理解为:Runtime 是 Objective-C 的「操作系统」,没有它,OC 就无法运行。

1.3 两种版本

版本 说明 适用场景
Legacy Runtime 较早版本 32 位 macOS、老设备
Modern Runtime 当前主流 64 位 macOS、iOS、模拟器

Modern Runtime 支持:非 fragile instance variables、属性自动合成、Objective-C 2.0 语法等。


二、核心概念

2.1 对象(Object)与类(Class)

在 C 中,结构体就是「数据 + 布局」。在 OC 中,对象 本质上是一个指向结构体的指针,结构体第一个成员是指向 类对象(Class) 的指针 isa

1
2
3
4
5
6
7
8
9
10
11
12
13
// 简化理解
struct objc_object {
Class isa; // 指向所属类
};

struct objc_class {
Class isa; // 类对象的 isa 指向元类(metaclass)
Class superclass; // 父类
// ... 方法列表、属性列表等
};

typedef struct objc_object *id;
typedef struct objc_class *Class;
  • 实例对象isa → 类对象
  • 类对象isa → 元类(metaclass)
  • 元类isa → 根元类,superclass → 父类的元类

2.2 消息传递(Message Passing)

OC 中「调用方法」实际上是「发消息」:

1
2
3
[person sayHello];
// 等价于
objc_msgSend(person, @selector(sayHello));

objc_msgSend 是 Runtime 提供的 C 函数,流程大致为:

  1. 检查 receiver 是否为 nil(若为 nil,直接返回,不崩溃)
  2. receiverisa 指向的类中查找方法
  3. 若未找到,沿 superclass 链向上查找
  4. 找到后跳转执行实现(IMP)
  5. 若最终未找到,进入「消息转发」流程

2.3 SEL、IMP、Method

类型 说明 示例
SEL 方法选择器,方法名的唯一标识 @selector(sayHello)
IMP 函数指针,方法的实际实现 void (*)(id, SEL, ...)
Method 方法结构体,包含 SEL 和 IMP struct objc_method
1
2
3
4
5
6
7
typedef struct objc_method *Method;

struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
};

三、消息查找与转发

3.1 方法查找流程(快速查找 + 慢速查找)

1
2
3
4
5
6
7
8
9
10
objc_msgSend(receiver, sel)

├─ 1. 检查 receiver 是否为 nil

├─ 2. 从类/父类缓存中查找 (objc_cache) —— 快速路径

└─ 3. 缓存未命中 → 调用 lookUpImpOrForward 慢速查找
├─ 在当前类 method_list 中查找
├─ 沿 superclass 链向上查找
└─ 若仍未找到 → 进入动态方法解析

3.2 动态方法解析(Dynamic Method Resolution)

在找不到方法时,Runtime 会先给类一次「补救」机会:

1
2
3
4
5
6
7
8
9
10
11
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(dynamicMethod)) {
class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"动态添加的方法被调用了");
}

3.3 消息转发(Message Forwarding)

若动态解析也返回 NO,则进入转发流程:

  1. Fast Forwarding- (id)forwardingTargetForSelector:(SEL)aSelector

    • 返回一个能响应该 Selector 的对象,消息将转给该对象
    • 不修改方法签名,性能较好
  2. Normal Forwarding- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector + - (void)forwardInvocation:(NSInvocation *)anInvocation

    • 返回方法签名,再在 forwardInvocation: 中处理
    • 可灵活重定向、修改参数、多对象分发等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Fast Forwarding
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHello)) {
return self.backupObject; // 转给其他对象处理
}
return [super forwardingTargetForSelector:aSelector];
}

// Normal Forwarding
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHello)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (anInvocation.selector == @selector(sayHello)) {
[anInvocation invokeWithTarget:self.backupObject];
} else {
[super forwardInvocation:anInvocation];
}
}

3.4 流程小结

1
2
3
4
5
6
7
8
9
10
11
12
13
查找 IMP

├─ 1. 缓存命中 → 直接调用

├─ 2. 当前类及父类 method_list 中找到 → 写入缓存并调用

├─ 3. 未找到 → resolveInstanceMethod / resolveClassMethod

├─ 4. 解析失败 → forwardingTargetForSelector

├─ 5. 返回 nil → methodSignatureForSelector + forwardInvocation

└─ 6. 仍无法处理 → doesNotRecognizeSelector: crash

四、Runtime 源码结构

4.1 主要头文件

1
2
3
4
5
6
7
8
objc4 源码(可在 opensource.apple.com 获取):
├── objc-runtime.mm # 运行时初始化、类加载
├── objc-msg-x86_64.s # objc_msgSend 汇编实现(x86_64)
├── objc-msg-arm64.s # objc_msgSend 汇编实现(ARM64)
├── objc-class.mm # 类结构、方法列表
├── objc-object.mm # 对象、isa、关联对象
├── message.mm # 消息查找、转发
└── runtime.mm # 动态创建类、方法等

4.2 objc_object 与 isa

1
2
3
4
5
6
7
8
9
// objc-private.h (简化)
struct objc_object {
private:
isa_t isa; // 非指针时存储类地址;Tagged Pointer 优化时存更多信息
public:
Class getIsa();
void initIsa(Class cls);
// ...
};

4.3 objc_class 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// objc-runtime-new.h (简化)
struct objc_class : objc_object {
Class superclass;
cache_t cache; // 方法缓存
class_data_bits_t bits; // 方法列表、属性、协议等

class_rw_t *data() const {
return bits.data();
}

const method_array_t methods() const;
const property_array_t properties() const;
const protocol_array_t protocols() const;
};

4.4 方法缓存(cache_t)

为提高查找效率,每个类都有方法缓存。查找顺序:先查缓存,再查方法列表

1
2
3
4
5
6
7
8
9
10
11
// 简化理解
struct cache_t {
bucket_t *buckets; // 哈希表
mask_t mask; // 容量 - 1
mask_t occupied; // 已缓存数量
};

struct bucket_t {
SEL sel;
IMP imp;
};

五、常用 Runtime API

5.1 类与对象

1
2
3
4
5
6
7
8
9
10
11
// 获取类
Class cls = [MyObject class];
Class cls2 = object_getClass(obj);

// 创建实例
id obj = class_createInstance(cls, 0);

// 判断类型
BOOL isMeta = class_isMetaClass(cls);
BOOL isMember = [obj isMemberOfClass:[MyObject class]];
BOOL isKind = [obj isKindOfClass:[NSObject class]];

5.2 方法操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取方法
Method m = class_getInstanceMethod(cls, @selector(sayHello));

// 获取 SEL 和 IMP
SEL sel = method_getName(m);
IMP imp = method_getImplementation(m);

// 交换实现
Method m1 = class_getInstanceMethod(cls, @selector(methodA));
Method m2 = class_getInstanceMethod(cls, @selector(methodB));
method_exchangeImplementations(m1, m2);

// 添加方法
class_addMethod(cls, @selector(newMethod), (IMP)newMethodIMP, "v@:");

5.3 属性与成员变量

1
2
3
4
5
6
7
8
9
10
11
// 获取属性列表
unsigned int count;
objc_property_t *properties = class_copyPropertyList(cls, &count);

// 获取成员变量
Ivar *ivars = class_copyIvarList(cls, &count);
for (unsigned int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
const char *name = ivar_getName(ivar);
const char *type = ivar_getTypeEncoding(ivar);
}

5.4 动态创建类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建新类
Class newClass = objc_allocateClassPair([NSObject class], "MyDynamicClass", 0);

// 添加实例变量(需在 allocate 之后、register 之前)
class_addIvar(newClass, "title", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));

// 添加方法
class_addMethod(newClass, @selector(doSomething), (IMP)doSomethingIMP, "v@:");

// 注册类
objc_registerClassPair(newClass);

// 使用
id instance = [[newClass alloc] init];

六、Method Swizzling(方法交换)

6.1 基本原理

通过 method_exchangeImplementations 交换两个方法的 IMP,使调用 A 时实际执行 B 的实现。常用于:

  • 无侵入式 Hook 系统或第三方方法
  • 统计埋点、日志
  • 修复 Bug、兼容旧版本

6.2 基础写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 交换 viewDidLoad 实现
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [UIViewController class];
SEL originalSel = @selector(viewDidLoad);
SEL swizzledSel = @selector(swizzled_viewDidLoad);

Method originalMethod = class_getInstanceMethod(cls, originalSel);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);

method_exchangeImplementations(originalMethod, swizzledMethod);
});
}

- (void)swizzled_viewDidLoad {
NSLog(@"ViewController viewDidLoad 被调用");
[self swizzled_viewDidLoad]; // 交换后,这里实际调用原 viewDidLoad
}

6.3 安全写法:处理子类未实现的情况

若子类未重写 viewDidLoadclass_getInstanceMethod 会拿到父类的方法。直接交换会导致:父类方法被交换,影响所有子类。更安全的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [UIViewController class];
SEL originalSel = @selector(viewDidLoad);
SEL swizzledSel = @selector(swizzled_viewDidLoad);

Method originalMethod = class_getInstanceMethod(cls, originalSel);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);

BOOL didAdd = class_addMethod(cls, originalSel,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAdd) {
class_replaceMethod(cls, swizzledSel,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

6.4 实际应用:全局统计页面访问

1
2
3
4
5
6
7
8
9
10
11
// UIViewController+PageTrack.m
+ (void)load {
[self swizzleInstanceMethod:@selector(viewDidAppear:)
withMethod:@selector(track_viewDidAppear:)];
}

- (void)track_viewDidAppear:(BOOL)animated {
[self track_viewDidAppear:animated];
// 统计逻辑
[Analytics trackPageView:NSStringFromClass([self class])];
}

七、关联对象(Associated Objects)

7.1 为什么需要关联对象?

分类(Category)不能添加实例变量,但我们有时需要给已有类「挂」一些额外数据。关联对象可以在不修改原类的情况下,为实例绑定键值对

7.2 API

1
2
3
4
5
6
7
8
// 设置
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); // 移除该对象所有关联,慎用

7.3 内存策略(policy)

Policy 对应属性修饰符 说明
OBJC_ASSOCIATION_ASSIGN assign 弱引用
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, strong 强引用,非原子
OBJC_ASSOCIATION_RETAIN atomic, strong 强引用,原子
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy 拷贝,非原子
OBJC_ASSOCIATION_COPY atomic, copy 拷贝,原子

7.4 示例:为 UIButton 绑定 Block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// UIButton+Block.h
- (void)addAction:(void (^)(UIButton *sender))block forControlEvents:(UIControlEvents)events;

// UIButton+Block.m
#import <objc/runtime.h>

static const void *kButtonBlockKey = &kButtonBlockKey;

- (void)addAction:(void (^)(UIButton *))block forControlEvents:(UIControlEvents)events {
objc_setAssociatedObject(self, kButtonBlockKey, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
[self addTarget:self action:@selector(block_buttonTapped:) forControlEvents:events];
}

- (void)block_buttonTapped:(UIButton *)sender {
void (^block)(UIButton *) = objc_getAssociatedObject(self, kButtonBlockKey);
if (block) block(sender);
}

八、实际项目中的应用案例

8.1 场景一:无侵入埋点

通过 Method Swizzling Hook viewDidAppear:viewDidDisappear:,自动统计页面停留时长,无需在每个 VC 里手动写埋点代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// PageAnalytics.m
- (void)tracked_viewDidAppear:(BOOL)animated {
[self tracked_viewDidAppear:animated];
self.pageEnterTime = CACurrentMediaTime();
[Tracking logEvent:@"page_enter" properties:@{@"page": NSStringFromClass([self class])}];
}

- (void)tracked_viewDidDisappear:(BOOL)animated {
NSTimeInterval duration = CACurrentMediaTime() - self.pageEnterTime;
[Tracking logEvent:@"page_leave" properties:@{
@"page": NSStringFromClass([self class]),
@"duration": @(duration)
}];
[self tracked_viewDidDisappear:animated];
}

8.2 场景二:防崩溃( unrecognized selector)

利用消息转发,在 forwardInvocation: 中统一处理未实现的方法调用,记录日志并优雅降级,避免 Crash。

1
2
3
4
5
6
7
8
9
10
11
12
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *sig = [super methodSignatureForSelector:aSelector];
if (!sig) {
sig = [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 兜底签名
}
return sig;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
[CrashGuard logUnrecognizedSelector:anInvocation.selector onObject:self];
// 可选择上报、降级处理等
}

8.3 场景三:字典转模型(JSON → Model)

遍历类的属性列表,根据属性名从字典中取值并赋值,实现自动 JSON 转 Model。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+ (instancetype)modelWithDictionary:(NSDictionary *)dict {
id model = [[self alloc] init];
unsigned int count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
const char *name = property_getName(properties[i]);
NSString *key = [NSString stringWithUTF8String:name];
id value = dict[key];
if (value && ![value isKindOfClass:[NSNull class]]) {
[model setValue:value forKey:key];
}
}
free(properties);
return model;
}

8.4 场景四:KVO 防崩溃

KVO 常见崩溃:未配对 removeObserver、重复 remove、对象已释放。可通过 Hook addObserver:forKeyPath:options:context:removeObserver:forKeyPath:,用关联对象维护观察者链表,在 dealloc 时自动移除,实现「自释放」KVO。

8.5 场景五:调试时打印对象属性

利用 class_copyIvarListclass_copyPropertyList 遍历所有属性,valueForKey: 取值并拼接成字符串,方便调试时查看对象完整状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (NSString *)debugDescription {
NSMutableString *desc = [NSMutableString stringWithFormat:@"<%@: %p>\n", [self class], self];
unsigned int count;
Ivar *ivars = class_copyIvarList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[desc appendFormat:@" %@ = %@\n", key, value];
}
free(ivars);
return desc;
}

九、注意事项与最佳实践

9.1 线程安全

  • objc_msgSend 等查找流程内部有锁,但动态修改类结构(如 class_addMethod)需注意线程安全
  • Method Swizzling 建议在 +load 中、单次执行完成,避免并发问题

9.2 Swizzling 陷阱

  • 只在 +load 中执行,且用 dispatch_once 保证只执行一次
  • 注意子类未实现父类方法时的交换范围
  • 避免多个库对同一方法重复 Swizzling,易产生难以排查的 Bug

9.3 性能考虑

  • Runtime 动态特性有开销,高频路径慎用
  • 方法缓存使大部分调用命中缓存,性能可接受
  • 关联对象查找为哈希查找,量不大时影响较小

9.4 Swift 与 Runtime

  • Swift 类继承自 NSObject 时,仍可使用 Runtime
  • 纯 Swift 类(不继承 NSObject)使用更静态的派发方式,Runtime 能力受限
  • 若需在 Swift 中使用 Runtime,需将类/方法标记为 @objcdynamic

十、总结

主题 要点
本质 OC 是消息型语言,方法调用 = 发消息,由 Runtime 在运行期决定实际执行的 IMP
查找 缓存 → 当前类 → 父类 → 动态解析 → 快速转发 → 完整转发 → 崩溃
Swizzling 交换 IMP,实现无侵入 Hook,注意子类与并发
关联对象 为实例动态绑定数据,弥补 Category 不能加实例变量的限制
应用 埋点、防崩溃、JSON 转模型、调试工具等

理解 Runtime,不仅有助于排查「消息转发」「KVO 崩溃」等问题,还能在需要时写出更灵活、可扩展的架构。建议结合 objc4 源码 和实际项目实践,逐步加深理解。