Day6-Runtime 实战技巧

YVTU

一、定义

Runtime 是什么?

  • Objective-C 的 Runtime 实际上是 C 语言的一套函数和类库,用于支持 Objective-C 编程语言的动态性,包括类的创建、方法调用、属性访问等。

二、Runtime 实战技巧

1.Method Swizzling(方法交换)

1. 什么是 Method Swizzling?

  • 定义:在程序运行时,交换两个方法实现的过程。
  • 本质:修改方法对应的 IMP(指向函数实现的指针)。
  • 常用场景:
    • 给系统方法加功能
    • 替换系统方法行为
    • AOP

2. 如何实现 Swizzling?

常用 API:

1
2
Method class_getInstanceMethod(Class cls, SEL name);
void method_exchangeImplementations(Method m1, Method m2);

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@implementation UIViewController (Swizzling)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method originalMethod = class_getInstanceMethod(self, @selector(viewWillAppear:));
Method swizzledMethod = class_getInstanceMethod(self, @selector(my_viewWillAppear:));
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}

- (void)my_viewWillAppear:(BOOL)animated {
NSLog(@"页面将要出现:%@", self);
[self my_viewWillAppear:animated];
}

@end

3. 注意事项

  • +loaddispatch_once 保证只执行一次。
  • 确保方法存在,避免引起程序崩溃。
  • 注意继承关系和子类覆盖问题。

2.消息转发机制

1. 什么是消息转发?

  • 当对象收到无法响应的消息,有三次机会处理:
    • 动态方法解析(resolveInstanceMethod:
    • 备用接收者(forwardingTargetForSelector:
    • 完整消息转发(forwardInvocation:

2. 详细流程

(1)动态方法解析

1
+ (BOOL)resolveInstanceMethod:(SEL)sel

示例:

1
2
3
4
5
6
7
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(run)) {
class_addMethod(self, sel, (IMP)runFunction, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

(2)快速转发

1
- (id)forwardingTargetForSelector:(SEL)aSelector

示例:

1
2
3
4
5
6
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(run)) {
return self.delegate;
}
return [super forwardingTargetForSelector:aSelector];
}

(3)完整消息转发

1
2
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector
- (void)forwardInvocation:(NSInvocation *)anInvocation

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(run)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
SEL sel = [anInvocation selector];
if ([self.delegate respondsToSelector:sel]) {
[anInvocation invokeWithTarget:self.delegate];
} else {
[super forwardInvocation:anInvocation];
}
}

3. 关联对象 (Associated Objects)

定义:
在不改动原类的基础上,给分类、扩展类增加实体属性。

常用 API:

  • objc_setAssociatedObject
  • objc_getAssociatedObject

应用场景:

  • UIButton 增加防重复点击时间间隔

4. 动态生成类和方法

常用 API:

  • objc_allocateClassPair
  • class_addMethod

应用场景:

  • KVO 内部原理(创建子类)
  • ORM框架自动生成对应属性

5. 完全自动 NSCoding 完成

思路:
通过 Runtime 遍历 ivar,自动 encode/decode,无需手写代码。

应用场景:

  • Model 持久化
  • 简化用户数据处理

6. 实现安全防护(防 Crash)

思路:
更换 NSArray, NSDictionary, NSString 等类的方法,防止路径错误、索引越界等 crash。

应用场景:

  • 安全性框架 SafeKit
  • 项目安全级别提升

7. 数据模型实体化(完全自动化等)

思路:
通过 property 列表,根据 key-value 将 JSON 转为 Model,或反转。

应用场景:

  • YYModel
  • MJExtension

8. 方法替换 IMP

应用:

  • hook 系统方法,并加自己逻辑
  • 控制方法执行流程

示例:

  • 在 App 进入前后台时,统一管理封装

9. 高级消息转发 (NSProxy)

应用:

  • 弱代理(避免循环引用、Timer 泄漏)
  • 多代理同步分发
  • 性能强化

三、实战案例

案例1:防止 UIButton 重复点击

背景

  • UIButton 连续点击容易造成接口重复请求。
  • 想给所有 UIButton 加防抖,不改动现有业务代码。

思路

用 Method Swizzling,替换 sendAction:to:forEvent: 方法,在里面加节流逻辑。


代码示例

UIButton+AntiRepeat.h

1
2
3
4
5
6
7
8
#import <UIKit/UIKit.h>

@interface UIButton (AntiRepeat)

@property (nonatomic, assign) NSTimeInterval eventInterval; // 点击间隔
@property (nonatomic, assign) NSTimeInterval lastEventTime; // 上次点击时间

@end

UIButton+AntiRepeat.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
#import "UIButton+AntiRepeat.h"
#import <objc/runtime.h>

@implementation UIButton (AntiRepeat)

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Method original = class_getInstanceMethod(self, @selector(sendAction:to:forEvent:));
Method swizzled = class_getInstanceMethod(self, @selector(my_sendAction:to:forEvent:));
method_exchangeImplementations(original, swizzled);
});
}

// 关联对象 eventInterval
- (NSTimeInterval)eventInterval {
return [objc_getAssociatedObject(self, _cmd) doubleValue];
}

- (void)setEventInterval:(NSTimeInterval)eventInterval {
objc_setAssociatedObject(self, @selector(eventInterval), @(eventInterval), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

// 关联对象 lastEventTime
- (NSTimeInterval)lastEventTime {
return [objc_getAssociatedObject(self, _cmd) doubleValue];
}

- (void)setLastEventTime:(NSTimeInterval)lastEventTime {
objc_setAssociatedObject(self, @selector(lastEventTime), @(lastEventTime), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

// 新的方法
- (void)my_sendAction:(SEL)action to:(id)target forEvent:(UIEvent *)event {
if (NSDate.date.timeIntervalSince1970 - self.lastEventTime < self.eventInterval) {
return; // 点击间隔太短,忽略
}
self.lastEventTime = NSDate.date.timeIntervalSince1970;
[self my_sendAction:action to:target forEvent:event];
}

@end

使用

1
2
UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
btn.eventInterval = 1.0; // 设置 1 秒内不能重复点击

讲解要点

  • sendAction:to:forEvent: 是 UIButton 处理点击事件的入口。
  • 通过 Swizzling,在不改业务代码的情况下统一加了防抖。
  • 使用 objc_setAssociatedObject 给分类动态添加属性,记录最后一次点击时间。

案例2:用 NSProxy 实现弱代理(防止 Timer 循环引用)

  • NSProxy 作为中介
  • 弱持 target

背景

  • NSTimer 默认强引用 target,容易造成内存泄漏(循环引用)。

思路

使用 NSProxy 中转,让 Timer 弱引用真正的 target。


代码示例

TimerWeakProxy.h

1
2
3
4
5
6
7
#import <Foundation/Foundation.h>

@interface TimerWeakProxy : NSProxy

+ (instancetype)proxyWithTarget:(id)target;

@end

TimerWeakProxy.m

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#import "TimerWeakProxy.h"

@interface TimerWeakProxy ()
@property (nonatomic, weak) id target;
@end

@implementation TimerWeakProxy

+ (instancetype)proxyWithTarget:(id)target {
TimerWeakProxy *proxy = [TimerWeakProxy alloc];
proxy.target = target;
return proxy;
}

- (id)forwardingTargetForSelector:(SEL)selector {
return self.target;
}

- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:self.target];
}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [self.target methodSignatureForSelector:sel];
}

@end

使用

1
2
3
4
5
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:[TimerWeakProxy proxyWithTarget:self]
selector:@selector(timerAction)
userInfo:nil
repeats:YES];

讲解要点

  • TimerWeakProxy 持有 target 的弱引用,防止循环引用。
  • forwardingTargetForSelector: 提供快速消息转发,提高性能。

案例3:自动 JSON -> Model 映射

  • 利用 property 列表遍历
  • 高效方便,性能好

案例4:安全防护套件

  • NSArray、NSDictionary 进行方法替换,防止错误操作

四、完整思维图

技术 作用 具体场景
Method Swizzling 修改系统方法 AOP 打点
消息转发 动态处理未知方法 弱代理
Associated Object 扩展分类属性 UIButton 防重点
自动 NSCoding 自动化存储连贯 Model 持久化
NSProxy 高级转发 Timer 弱代理

五、常考问题

  1. 说说你对 Runtime 的理解?
  • Runtime 是 OC 的运行时系统,允许程序在运行时进行对象操作(如消息发送、方法交换、类创建等)。
  • 动态性是 Objective-C 最重要的特性之一,基于 Runtime 实现。

  1. 什么是 Method Swizzling?使用时注意什么?
  • 定义:交换两个方法的实现。
  • 注意:
  • 使用 dispatch_once 保证只交换一次。
  • 确认方法存在再交换,避免崩溃。
  • 维护好调用链,防止死循环。
  • 避免过度使用,破坏封装性。

  1. Method Swizzling 常用在哪些场景?
  • 统一加日志打点(比如 UIViewController 生命周期)
  • 替换系统方法功能(如统计点击事件)
  • AOP 编程实践
  • 框架内部增强(如 AFNetworking 的某些 hook)

  1. 消息发送流程是怎样的?
  • 调用方法 -> 转成 objc_msgSend 发送消息。
  • 如果找不到方法:
    1. 动态方法解析(resolveInstanceMethod:)
    2. 快速转发(forwardingTargetForSelector:)
    3. 完整消息转发(forwardInvocation:)
    4. 最后抛出 unrecognized selector sent to instance 异常。

  1. 消息转发和代理模式有什么关系?
  • 代理(delegate)本质就是消息转发的一种应用。
  • 可以通过 forwardingTargetForSelector: 把不支持的方法交给代理对象执行。
  • 复杂代理模式(如 NSProxy 多代理)就是用完整消息转发实现的。

  1. Swizzling 和 Hook 的区别?
  • Swizzling:指“方法交换”,主要改变对象的方法实现。
  • Hook:更广义,指拦截并修改程序执行过程,可以是方法层面、函数层面(Fishhook)、甚至是汇编指令层面。

  1. 为什么 +load 方法适合做 Swizzling?
  • +load 是在类(或分类)被加载到内存时立即调用的,优先于 main()。
  • 保证在对象使用前就完成方法交换。

补充:如果想更灵活控制,某些框架(如 Aspects)会在 +initialize 做 Swizzling,但要注意 initialize 只会在第一次发送消息前调用。


  1. 使用 Runtime 有哪些风险?
  • 方法交换过多,容易导致难以维护和调试。
  • 消息转发链过长,可能导致性能下降。
  • 动态添加方法时参数类型签名不正确,可能导致运行时崩溃。
  • 无法被编译器静态检查,出错时很难定位。

  1. NSProxy 是什么?为什么需要它?
  • NSProxy 是一个轻量级的基类,专门用于实现消息转发。
  • 典型场景:
  • 远程方法调用(RPC)
  • 多代理管理(如 YYTextKeyboardManager)
  • 相比 NSObject,NSProxy 没有实例变量,直接走完整消息转发流程。