iOS 动态方法调用 - NSInvocation
NSInvocation 是 iOS/macOS 开发中 Objective-C 运行时的一个核心类,它用于封装并动态调用方法。该类允许将方法调用(包括目标对象、方法选择器、参数和返回值)存储为一个对象,从而实现延迟执行或跨上下文传递。
核心特性
-
动态方法调用
在运行时动态构造任意对象的方法调用,无需在编译时确定具体的方法。 -
完整调用信息
NSInvocation包含以下元素:- Target(目标对象):方法调用的接收者。
- Selector(方法选择器):要调用的方法名(如
@selector(method:))。 - Arguments(参数):所有输入参数(支持基本类型、对象、指针等)。
- Return Value(返回值):存储方法执行后的返回值。
-
延迟执行
可以将NSInvocation存入队列、定时器或其他数据结构中,以便后续触发调用。
使用步骤
1. 创建方法签名(NSMethodSignature)
方法签名描述了方法的参数和返回值类型。
NSMethodSignature *signature = [myInstance methodSignatureForSelector:@selector(appendString:toString:)];
// 或者
NSMethodSignature *signature =
[MyClass instanceMethodSignatureForSelector:@selector(concat:with:)];
2. 初始化 NSInvocation 对象
NSInvocation *invocation =
[NSInvocation invocationWithMethodSignature:signature];
invocation.target = myClassInstance; // 设置目标对象
invocation.selector = @selector(concat:with:); // 设置方法选择器
3. 设置参数
注意:参数索引从 2 开始(索引 0 是 target,1 是 selector)。
NSString *arg1 = @"Hello";
NSString *arg2 = @"World!";
[invocation setArgument:&arg1 atIndex:2]; // 第一个参数索引=2
[invocation setArgument:&arg2 atIndex:3]; // 第二个参数索引=3
4. 执行调用
[invocation invoke]; // 触发方法调用
5. 获取返回值
__unsafe_unretained NSString *result = nil;
[invocation getReturnValue:&result];
NSLog(@"Result: %@", result); // 输出:@"HelloWorld!"
关键注意事项
-
内存管理:
NSInvocation默认强引用target和所有对象类型的参数,可能导致循环引用。建议使用__weak或及时释放对象:__weak typeof(self) weakSelf = self; [invocation setTarget:weakSelf]; -
参数索引偏移:
参数索引从2开始(0和1预留给target和selector)。 -
类型匹配:
必须确保NSMethodSignature的类型编码与参数/返回值的实际类型严格一致,否则可能导致崩溃。 -
返回值处理:
返回值的内存管理由调用者负责(例如,NSString*需要注意引用计数)。
典型应用场景
-
Undo/Redo 系统
存储操作调用的NSInvocation,以实现撤销/重做栈。 -
异步调用
将调用封装后提交到其他线程(例如,使用performSelector:onThread:withObject:waitUntilDone:)。 -
分布式对象(Distributed Objects)
在跨进程通信中传递方法调用。 -
动态代理(Dynamic Proxy)
结合forwardInvocation:实现消息转发(例如,NSProxy的子类)。 -
定时任务
与NSTimer结合,实现延迟方法调用:[NSTimer scheduledTimerWithTimeInterval:2.0 invocation:invocation repeats:NO];
局限性
-
不支持 Swift:
NSInvocation仅适用于 Objective-C 代码(Swift 的强类型安全与动态性存在冲突)。 -
代码冗余:
与 Block 或直接调用相比,NSInvocation的创建流程较为繁琐。 -
性能开销:
运行时构造的性能开销高于编译时直接调用(微秒级差异,但通常可以忽略)。
替代方案
-
Block(代码块):
更简洁地封装延迟操作,推荐优先使用:dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{ [myClassInstance concat:@"Hello" with:@"Block!"]; }); -
performSelector:系列方法:
适用于无参数或单个参数的方法调用。
总结
NSInvocation 是 Objective-C 动态特性的核心工具之一,适用于需要高度灵活性的场景(如动态调用和复杂转发)。不过在现代开发中,更安全的 Block 或协议(Protocol)方案应优先考虑,除非必须处理未知方法或历史遗留代码。