Shaula Blog

美丽的东西都是肤浅的

iOS 动态方法调用 - NSInvocation

NSInvocation 是 iOS/macOS 开发中 Objective-C 运行时的一个核心类,它用于封装并动态调用方法。该类允许将方法调用(包括目标对象、方法选择器、参数和返回值)存储为一个对象,从而实现延迟执行或跨上下文传递。

核心特性

  1. 动态方法调用
    在运行时动态构造任意对象的方法调用,无需在编译时确定具体的方法。

  2. 完整调用信息
    NSInvocation 包含以下元素:

    • Target(目标对象):方法调用的接收者。
    • Selector(方法选择器):要调用的方法名(如 @selector(method:))。
    • Arguments(参数):所有输入参数(支持基本类型、对象、指针等)。
    • Return Value(返回值):存储方法执行后的返回值。
  3. 延迟执行
    可以将 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 开始(索引 0target1selector)。

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 开始(01 预留给 targetselector)。

  • 类型匹配
    必须确保 NSMethodSignature 的类型编码与参数/返回值的实际类型严格一致,否则可能导致崩溃。

  • 返回值处理
    返回值的内存管理由调用者负责(例如,NSString* 需要注意引用计数)。

典型应用场景

  1. Undo/Redo 系统
    存储操作调用的 NSInvocation,以实现撤销/重做栈。

  2. 异步调用
    将调用封装后提交到其他线程(例如,使用 performSelector:onThread:withObject:waitUntilDone:)。

  3. 分布式对象(Distributed Objects)
    在跨进程通信中传递方法调用。

  4. 动态代理(Dynamic Proxy)
    结合 forwardInvocation: 实现消息转发(例如,NSProxy 的子类)。

  5. 定时任务
    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)方案应优先考虑,除非必须处理未知方法或历史遗留代码。