PerformSelector 与 NSInvocation

PerformSelector

  • PerformSelector 简介

    performSelector: 方法等价于直接向消息接受者(即方法调用者)发送一个 aSelector 消息。例如,以下消息都会执行相同的事情:

    id aClone = [anObject copy];
    id aClone = [anObject performSelector:@selector(copy)];
    id aClone = [anObject performSelector:sel_getUid("copy")];
    

    performSelector: 方法允许开发者发送直到运行时才确定的消息。这意味着可以传递一个 aSelector 变量作为方法的参数:

    SEL aSelector = findTheAppropriateSelectorForTheCurrentSituation();
    id returnedObject = [anObject performSelector:aSelector];
    

    但是在这样做的时候要小心。不同的消息对其返回的对象需要不同的内存管理策略,而且对于返回的对象具体使用哪一种内存管理策略,我们可能并不清楚

    通常调用者不需要负责由消息返回的对象的内存管理,但当 aSelector 代表的是一种创建方法时,比如 copy,就不是这样了

    关于内存管理的描述,请参考 Advanced Memory Management Programming Guide 中的 Memory Management Policy

    根据代码的结构,编译器可能不清楚开发者对任意给定的调用使用哪种类型的方法选择器。由于这种不确定性,如果开发者在使用 ARC 管理内存时使用了 aSelector 变量,则编译器会生成警告。由于 ARC 无法在编译时确定返回的对象的所有权,因此 ARC 假设调用者不需要获取返回的对象的所有权,但事实可能并不是如此。编译器警告会提醒开发者可能发生内存泄漏

    为了避免编译器的警告,如果知道 aSelector 没有返回值,则可以使用 performSelectorOnMainThread:withObject:waitUntilDone: 或者 NSObject 中的相关方法

    对于更一般的解决方案,请使用 NSInvocation 来构造一个可以使用任意参数列表进行调用并返回值的消息

    或者,也可以考虑重组代码,以使用 Block 作为通过 API 传递功能模块的一种方法。有关详细信息,请参考 Blocks Programming Topics

  • NSObject

    performSelector:

    /*
    向消息接受者(即方法调用者)发送给定的消息,并获取消息的结果作为返回值
    
    @param aSelector 方法选择器,用于标识要发送的消息的名称
    @return 消息的结果(实例对象)
    @note 要发送的消息需要不带任何参数
    @warning 如果参数 aSelector 传 NULL,则会引发 NSInvalidArgument 异常
    */
    -(id)performSelector:(SEL)aSelector;
    

    performSelector:withObject:

    /*
    使用给定的参数向消息接受者(即方法调用者)发送给定的消息,并获取消息的结果作为返回值
    
    @param aSelector 方法选择器,用于标识要发送的消息的名称
    @param object 消息的参数(实例对象)
    @return 消息的结果(实例对象)
    @note 要发送的消息需要带 1 个参数
    @warning 如果参数 aSelector 传 NULL,则会引发 NSInvalidArgument 异常
    */
    -(id)performSelector:(SEL)aSelector withObject:(id)object;
    

    performSelector:withObject:withObject:

    /*
    使用给定的参数向消息接受者(即方法调用者)发送给定的消息,并获取消息的结果作为返回值
    
    @param aSelector 方法选择器,用于标识要发送的消息的名称
    @param object1 消息的参数(实例对象)
    @param object2 消息的参数(实例对象)
    @return 消息的结果(实例对象)
    @note 要发送的消息需要带 2 个参数
    @warning 如果参数 aSelector 传 NULL,则会引发 NSInvalidArgument 异常
    */
    -(id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2;
    
  • NSObject (NSDelayedPerforming)

    performSelector:withObject:afterDelay:

    /*
    在当前线程上在给定的延迟之后,使用默认的模式和给定的参数向消息接受者(即方法调用者)发送给定的消息
    
    @param aSelector 方法选择器,用于标识要发送的消息的名称
    @param anArgument 消息的参数(实例对象)。如果消息不接受参数,则传 nil
    @param delay 在发送消息之前的最短延迟时间。设置为 0 并不一定会立即发送该消息,aSelector 仍然在线程的 runloop 队列中并会尽快发送
    @note 要发送的消息需要没有返回值
    	  要发送的消息需要有一个 id 类型的参数,或者没有参数	
    @note 此方法设置了一个定时器以在当前线程的 runloop 上执行方法选择器 aSelector 所标识的消息
    	  定时器被设置为在默认模式下运行(NSDefaultRunLoopMode)
    	  当定时器触发时,线程会尝试从 runloop 的消息队列中取出消息,并发送消息
    	  如果 runloop 正在默认模式上运行,则消息会成功发送。否则,定时器会等待 runloop 处于默认模式
    @note 如果希望在 runloop 处于默认模式以外的模式时,将消息取出队列并发送
    	  使用 performSelector:withObject:afterDelay:inModes: 代替此方法
    @see performSelector:withObject:afterDelay:inModes:
    */
    -(void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
    

    performSelector:withObject:afterDelay:inModes:

    /*
    在当前线程上在给定的延迟之后,使用给定的模式和给定的参数向消息接受者(即方法调用者)发送给定的消息
    
    @param aSelector 方法选择器,用于标识要发送的消息的名称
    @param anArgument 消息的参数(实例对象)。如果消息不接受参数,则传 nil
    @param delay 在发送消息之前的最短延迟时间。设置为 0 并不一定会立即发送该消息,aSelector 仍然在线程的 runloop 队列中并会尽快发送
    @param modes 一个字符串数组,用于标识定时器所处的模式。该数组中必须至少包含一个字符串
    			 如果参数 modes 传 nil 或者空数组,则此方法会直接返回而不发送方法选择器 aSelector 所标识的消息
    			 有关 runloop 的模式,请参考 Threading Programming Guide
    @note 要发送的消息需要没有返回值
    	  要发送的消息需要有一个 id 类型的参数,或者没有参数
    @note 此方法设置了一个定时器以在当前线程的 runloop 上执行方法选择器 aSelector 所标识的消息
    	  定时器被设置为在由参数 modes 指定的模式下运行
    	  当定时器触发时,线程会尝试从 runloop 的消息队列中取出消息,并发送消息
    	  如果 runloop 正在由参数 modes 指定的模式下运行,则消息会成功发送。否则,定时器会等待 runloop 处于这些模式
    @note 如果不确定当前的线程是否是主线程,则可以使用
    			performSelectorOnMainThread:withObject:waitUntilDone:
    			performSelectorOnMainThread:withObject:waitUntilDone:modes:
    	  来保证方法选择器 aSelector 所标识的消息会在主线程上执行
    @note 如果要取消处于队列中的消息,则可以使用
    			cancelPreviousPerformRequestsWithTarget:
    			cancelPreviousPerformRequestsWithTarget:selector:object:
    @note 此方法将其当前的上下文注册到 runloop 中,并依赖于定期运行的 runloop 来保证消息的正确发送
    	  当在一个常见的上下文中被调度队列调用时,可以调用这个方法,并最终注册一个不定期自动运行的运行循环
    	  如果在调度队列上运行时需要这种类型的功能(定时发送消息的功能),则应该使用 dispatch_after 和相关方法来获取所需的行为
    */
    -(void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
    

    cancelPreviousPerformRequestsWithTarget:selector:object:

    /*
    取消执行 之前在 runloop 中注册的 消息发送请求
    
    @param aTarget 之前的消息的接受者
    @param aSelector 之前的方法选择器,用于标识要取消的消息的名称
    @param anArgument 之前的消息的参数(实例对象)。如果之前的消息不接受参数,则传 nil
    				  因为参数 anArgument 会使用 isEqual: 方法检查是否相等
    				  所以该值不必与之前传递的消息的参数是同一个对象
    @note 具有相同 aTarget、aSelector、anArgument 的消息发送请求都将被取消
    	  此方法只会删除当前线程 runloop 中的消息发送请求,而不是所有线程 runloop 中的消息发送请求
    @note 此方法只能取消通过以下 2 个方法注册的消息
    	  performSelector:withObject:afterDelay:
    	  performSelector:withObject:afterDelay:inModes:
    */
    +(void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
    

    cancelPreviousPerformRequestsWithTarget:

    /*
    取消执行 之前在 runloop 中注册的 消息发送请求
    
    @param aTarget 之前的消息的接受者
    @note 具有相同 aTarget 的消息发送请求都将被取消
    	  此方法只会删除当前线程 runloop 中的消息发送请求,而不是所有线程 runloop 中的消息发送请求
    @note 此方法只能取消通过以下 2 个方法注册的消息
    	  performSelector:withObject:afterDelay:
    	  performSelector:withObject:afterDelay:inModes:
    */
    +(void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;
    
  • NSObject (NSThreadPerformAdditions)

    performSelectorOnMainThread:withObject:waitUntilDone:modes:

    /*
    在主线程上使用给定的模式和给定的参数向消息接受者(即方法调用者)发送给定的消息
    
    @param aSelector 方法选择器,用于标识要发送的消息的名称
    @param arg 消息的参数(实例对象)。如果消息没有参数,则传 nil
    @param wait 一个用于控制同步或异步的布尔值,用于标识当前线程是否挂起,直到在主线程上的消息接收者执行完指定的消息
    			如果设置为 YES(同步),则挂起当前线程。如果设置为 NO(异步),则此方法立即返回
    			如果当前线程正好也是主线程并且将参数 wait 设置为 YES,则立即执行指定的消息
    			如果当前线程正好也是主线程并且将参数 wait 设置为 NO,则将指定的消息注册到主线程的 runloop 队列中,等待下一次运行循环时再执行
    @param array 一个字符串数组,用于标识发送消息时 runloop 所处的模式。该数组中必须至少包含一个字符串
    			 如果参数 array 传 nil 或者空数组,则此方法会直接返回而不发送方法选择器 aSelector 所标识的消息
    			 有关 runloop 的模式,请参考 Threading Programming Guide
    @note 可以使用此方法将消息传递到应用程序的主线程中
    	  主线程包含应用程序的主运行循环(main runloop),并且是 NSApplication 对象接收事件的地方
    	  此方法使用参数 array 中指定的运行循环模式将指定的消息添加到主线程 runloop 的队列中
    	  作为正常运行循环处理的一部分,主线程会从消息队列中取出消息并调用该消息所对应的方法(假设 runloop 是在参数 array 中指定的某个模式下运行的)
    	  来自同一线程对此方法的多次调用,将会导致相应的方法选择器以同样的顺序被添加进 runloop 队列并随后以同样的顺序被取出执行,假设每个方法选择器的相关的运行循环模式是相同的
    	  如果为每个方法选择器指定不同的模式,则将跳过模式与当前运行循环模式不匹配的所有方法选择器,直到 runloop 随后在该模式下执行
    @note 无法取消以此方法注册到 runloop 队列中的消息
    	  如果想要能取消已经注册到 runloop 队列中的消息,则消息需要使用以下的两个方法注册到 runloop 中
    			performSelector:withObject:afterDelay:
    			performSelector:withObject:afterDelay:inModes:
    @note 此方法将其当前的上下文注册到 runloop 中,并依赖于定期运行的 runloop 来保证消息的正确发送
    	  当在一个常见的上下文中被调度队列调用时,可以调用这个方法,并最终注册一个不定期自动运行的运行循环
    	  如果在调度队列上运行时需要定时发送消息的功能,则应该使用 dispatch_after 和相关方法来实现
    */
    -(void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
    

    performSelectorOnMainThread:withObject:waitUntilDone:

    /*
    在主线程上使用默认的模式和给定的参数向消息接受者(即方法调用者)发送给定的消息
    
    @param aSelector 方法选择器,用于标识要发送的消息的名称
    @param arg 消息的参数(实例对象)。如果消息没有参数,则传 nil
    @param wait 一个用于控制同步或异步的布尔值,用于标识当前线程是否挂起,直到在主线程上的消息接收者执行完指定的消息
    			如果设置为 YES(同步),则挂起当前线程。如果设置为 NO(异步),则此方法立即返回
    			如果当前线程正好也是主线程并且将参数 wait 设置为 YES,则立即执行指定的消息
    			如果当前线程正好也是主线程并且将参数 wait 设置为 NO,则将指定的消息注册到主线程的 runloop 队列中,等待下一次运行循环时再执行
    @note 可以使用此方法将消息传递到应用程序的主线程中
    	  主线程包含应用程序的主运行循环(main runloop),并且是 NSApplication 对象接收事件的地方
    	  此方法使用 NSRunLoopCommonModes 运行循环模式将指定的消息添加到主线程 runloop 的队列中
    	  作为正常运行循环处理的一部分,主线程会从消息队列中取出消息并调用该消息所对应的方法(假设 runloop 是在 NSRunLoopCommonModes 模式下运行的)
    	  来自同一线程对此方法的多次调用,将会导致相应的方法选择器以同样的顺序被添加进 runloop 队列并随后以同样的顺序被取出执行,假设每个方法选择器的相关的运行循环模式是相同的
    @note 无法取消以此方法注册到 runloop 队列中的消息
    	  如果想要能取消已经注册到 runloop 队列中的消息,则消息需要使用以下的两个方法注册到 runloop 中
    			performSelector:withObject:afterDelay:
    			performSelector:withObject:afterDelay:inModes:
    @note 此方法将其当前的上下文注册到 runloop 中,并依赖于定期运行的 runloop 来保证消息的正确发送
    	  当在一个常见的上下文中被调度队列调用时,可以调用这个方法,并最终注册一个不定期自动运行的运行循环
    	  如果在调度队列上运行时需要定时发送消息的功能,则应该使用 dispatch_after 和相关方法来实现
    */
    -(void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    

    performSelector:onThread:withObject:waitUntilDone:modes:

    /*
    在给定的线程上使用给定的模式和给定的参数向消息接受者(即方法调用者)发送给定的消息
    
    @param aSelector 方法选择器,用于标识要发送的消息的名称
    @param thr 要执行方法选择器 aSelector 所对应的消息的目标线程
    @param arg 消息的参数(实例对象)。如果消息没有参数,则传 nil
    @param wait 一个用于控制同步或异步的布尔值,用于标识当前线程是否挂起,直到在目标线程上的消息接收者执行完指定的消息
    			如果设置为 YES(同步),则挂起当前线程。如果设置为 NO(异步),则此方法立即返回
    			如果当前线程正好和目标线程是同一线程并且将参数 wait 设置为 YES,则立即执行指定的消息
    			如果当前线程正好和目标线程是同一线程并且将参数 wait 设置为 NO,则将指定的消息注册到目标线程的 runloop 队列中,等待下一次运行循环时再执行
    @param array 一个字符串数组,用于标识发送消息时 runloop 所处的模式。该数组中必须至少包含一个字符串
    			 如果参数 array 传 nil 或者空数组,则此方法会直接返回而不发送方法选择器 aSelector 所标识的消息
    			 有关 runloop 的模式,请参考 Threading Programming Guide
    @note 可以使用此方法将消息传递到目标线程中
    	  此方法使用参数 array 中指定的运行循环模式将指定的消息添加到目标线程的 runloop 队列中
    	  作为正常运行循环处理的一部分,目标线程会从消息队列中取出消息并调用该消息所对应的方法(假设 runloop 是在参数 array 中指定的某个模式下运行的)
    @note 无法取消以此方法注册到 runloop 队列中的消息
    	  如果想要能取消已经注册到 runloop 队列中的消息,则消息需要使用以下的两个方法注册到 runloop 中
    			performSelector:withObject:afterDelay:
    			performSelector:withObject:afterDelay:inModes:
    @note 此方法将其当前的上下文注册到 runloop 中,并依赖于定期运行的 runloop 来保证消息的正确发送
    	  当在一个常见的上下文中被调度队列调用时,可以调用这个方法,并最终注册一个不定期自动运行的运行循环
    	  如果在调度队列上运行时需要定时发送消息的功能,则应该使用 dispatch_after 和相关方法来实现
    */
    -(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
    

    performSelector:onThread:withObject:waitUntilDone:

    /*
    在给定的线程上使用默认的模式和给定的参数向消息接受者(即方法调用者)发送给定的消息
    
    @param aSelector 方法选择器,用于标识要发送的消息的名称
    @param thr 要执行方法选择器 aSelector 所对应的消息的目标线程
    @param arg 消息的参数(实例对象)。如果消息没有参数,则传 nil
    @param wait 一个用于控制同步或异步的布尔值,用于标识当前线程是否挂起,直到在目标线程上的消息接收者执行完指定的消息
    			如果设置为 YES(同步),则挂起当前线程。如果设置为 NO(异步),则此方法立即返回
    			如果当前线程正好和目标线程是同一线程并且将参数 wait 设置为 YES,则立即执行指定的消息
    			如果当前线程正好和目标线程是同一线程并且将参数 wait 设置为 NO,则将指定的消息注册到目标线程的 runloop 队列中,等待下一次运行循环时再执行
    @note 可以使用此方法将消息传递到目标线程中
    	  此方法使用 NSRunLoopCommonModes 运行循环模式将指定的消息添加到目标线程 runloop 的队列中
    	  作为正常运行循环处理的一部分,目标线程会从消息队列中取出消息并调用该消息所对应的方法(假设 runloop 是在 NSRunLoopCommonModes 模式下运行的)
    @note 无法取消以此方法注册到 runloop 队列中的消息
    	  如果想要能取消已经注册到 runloop 队列中的消息,则消息需要使用以下的两个方法注册到 runloop 中
    			performSelector:withObject:afterDelay:
    			performSelector:withObject:afterDelay:inModes:
    @note 此方法将其当前的上下文注册到 runloop 中,并依赖于定期运行的 runloop 来保证消息的正确发送
    	  当在一个常见的上下文中被调度队列调用时,可以调用这个方法,并最终注册一个不定期自动运行的运行循环
    	  如果在调度队列上运行时需要定时发送消息的功能,则应该使用 dispatch_after 和相关方法来实现
    */
    -(void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
    

    performSelectorInBackground:withObject:

    /*
    在新的后台线程上使用给定的参数向消息接受者(即方法调用者)发送给定的消息
    
    @param aSelector 方法选择器,用于标识要发送的消息的名称
    @param arg 消息的参数(实例对象)。如果消息没有参数,则传 nil
    @note 此方法将在应用程序中创建一条新线程
    	  如果应用程序还未进入多线程模式,则将应用程序设置为多线程模式
    	  由方法选择器 aSelector 所标识的消息,必须像任何其他在应用程序中新创建的线程一样设置线程环境
    	  有关如何配置和运行线程的详细信息,请参考 Threading Programming Guide
    */
    -(void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg;
    
  • NSRunLoop (NSOrderedPerform)

    performSelector:target:argument:order:modes:

    /*
    在当前线程的 runloop 中使用给定的优先级和给定的参数向给定的对象(即消息接受者)发送给定的消息
    
    @param aSelector 方法选择器,用于标识要发送的消息的名称
    @param target 消息接受者
    @param arg 消息的参数(实例对象)。如果消息不接受参数,则传 nil
    @param order 消息调度的优先级。如果有多个消息,则 order 值较低的消息会在 order 值较高的消息之前发送
    @param modes 一个字符串数组,用于标识发送消息时 runloop 所处的模式
    			 该数组中必须至少包含一个字符串。如果参数 modes 传 nil 或者空数组,则此方法会直接返回而不发送方法选择器 aSelector 所标识的消息
    			 可以使用自定义模式或者 Run Loop Modes 中指定的模式
    			 有关 runloop 的模式,请参考 Threading Programming Guide
    @note 要发送的消息需要没有返回值
    	  要发送的消息需要有一个 id 类型的参数,或者没有参数
    @note 此方法设置了一个定时器,用于在下次运行循环开始时在消息接受者 target 上执行方法选择器 aSelector 所标识的消息
    	  定时器将在由参数 modes 指定的模式下运行
    	  当定时器触发时,线程会尝试从 runloop 的消息队列中取出消息并执行消息
    	  如果 runloop 处于参数 modes 指定的模式之一,则消息成功执行
    	  否则定时器将等待 runloop 处于由参数 modes 指定的模式
    	  此方法在消息 aSelector 发送之前就返回了
    	  runloop 将 retain 消息接受者 target 和消息参数 arg,直到方法选择器 aSelector 对应的定时器被触发。发送完消息之后,作为清理的一部分,runloop 将 releases 消息接受者 target 和消息参数 arg
    	  如果需要在处理完当前事件后发送多个消息,并且需要确保这些消息按特定的顺序发送,请使用此方法
    */
    -(void)performSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg order:(NSUInteger)order modes:(NSArray<NSRunLoopMode> *)modes;
    

    cancelPerformSelector:target:argument:

    /*
    取消执行 之前在当前 runloop 中注册的 消息发送请求
    
    @param aSelector 之前的方法选择器,用于标识要取消的消息的名称
    @param target 之前的消息的接受者
    @param arg 之前的消息的参数(实例对象)
    @note 可以使用此方法取消之前使用 performSelector:target:argument:order:modes: 方法注册的消息
    	  此方法的参数必须与之前注册的消息的参数相匹配
    	  此方法将从 runloop 的所有模式中删除已注册的指定消息
    */
    -(void)cancelPerformSelector:(SEL)aSelector target:(id)target argument:(nullable id)arg;
    

    cancelPerformSelectorsWithTarget:

    /*
    取消执行 之前在当前 runloop 中注册的 消息发送请求
    
    @param target 之前的消息的接受者
    @note 可以使用此方法取消之前使用 performSelector:target:argument:order:modes: 方法注册的消息
    @note 当前 runloop 中所有具有相同消息接受者 target 的消息发送请求都将被取消
    */
    -(void)cancelPerformSelectorsWithTarget:(id)target;
    

NSMethodSignature

  • NSMethodSignature 简介

    NSMethodSignature 用于记录(方法的返回值类型)和(方法的参数类型)
    关于类型编码的更多信息,请参考 Type Encodings

  • 创建 NSMethodSignature 对象

    ① 使用 NSMethodSignature 的类方法 +signatureWithObjCTypes:

    /*
    用给定的 Objective-C 方法类型编码创建一个 NSMethodSignature 对象
    
    @param types 包含(方法返回值类型)和(方法参数类型)的类型编码字符串
    @return 方法签名
    @param 在能获取到对应类的情况下,一般使用对应类的 -methodSignatureForSelector: 或 +instanceMethodSignatureForSelector: 创建 NSMethodSignature 对象
    	   在获取不到对应类的情况下,才使用此方法创建 NSMethodSignature 对象
    */
    +(NSMethodSignature *)signatureWithObjCTypes:(const char *)types;
    

    ② 使用 NSObject 的对象方法 -methodSignatureForSelector:

    /*
    用给定的方法选择器创建一个 NSMethodSignature 对象
    
    @param aSelector 方法选择器,用于标识要获取方法签名的方法
    				 当方法调用者(即消息接受者)是一个实例对象时,该方法选择器标识的是一个对象方法
    				 当方法调用者(即消息接受者)是一个类对象时,该方法选择器标识的是一个类方法
    @return 方法签名。如果参数 aSelector 所标识的方法未找到,则返回 nil
    */
    -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
    

    ③ 使用 NSObject 的类方法 +instanceMethodSignatureForSelector:

    /*
    用给定的方法选择器创建一个 NSMethodSignature 对象
    
    @param aSelector 方法选择器,用于标识要获取方法签名的方法
    @return 方法签名。如果参数 aSelector 所标识的方法未找到,则返回 nil
    */
    +(NSMethodSignature *)instanceMethodSignatureForSelector:(SEL)aSelector;
    
  • 获取方法参数类型的相关信息

    ① 对象方法 -getArgumentTypeAtIndex:

    /*
    获取给定索引处的方法参数的类型编码
    
    @param idx 要检视的方法参数的索引
    @return 给定索引处的方法参数的类型编码
    @note 索引从 0 开始
    	  隐式方法参数 self(id 类型) 和 _cmd(SEL 类型) 位于索引 0 和 1 处
    	  显式方法参数从索引 2 处开始
    @note 如果索引超过了方法参数的数量,则会引发 NSInvalidArgumentException 异常
    */
    -(const char *)getArgumentTypeAtIndex:(NSUInteger)idx;
    

    ② 属性 numberOfArguments

    // 方法的参数个数
    // 因为所有的方法都会包含 2 个隐式参数 self 和 _cmd,所以 1 个 NSMethodSignature 对象至少会有 2 个参数
    @property(readonly) NSUInteger numberOfArguments;
    

    ③ 属性 frameLength

    // 帧长度。方法参数在堆栈上占用的字节数,随应用程序运行的硬件体系结构的不同而不同
    @property(readonly) NSUInteger frameLength;
    
  • 获取方法返回值类型的相关信息

    ① 属性 methodReturnType

    // 方法返回值的类型编码
    @property(readonly) const char *methodReturnType;
    

    ② 属性 methodReturnLength

    // 方法返回值在堆栈上占用的字节数,随应用程序运行的硬件体系结构的不同而不同
    @property(readonly) NSUInteger methodReturnLength;
    
  • 检查同步状态

    ① 对象方法 -isOneway

    /*
    当通过分布式对象调用时,用于检查消息接受者是否是异步的
    
    @return 如果在通过分布式对象调用时,消息接受者是异步的,则返回 YES。否则返回 NO
    @note 如果该方法是单向的,则远程消息的发送者不会挂起以等待回复
    */
    -(BOOL)isOneway;
    

NSInvocation

  • NSInvocation 简介

    NSInvocation 用于将 Objective-C 的消息表示为一个对象。NSInvocation 用于在对象之间和应用程序之间(存储和转发)消息,主要由 NSTimer 对象和分布式对象系统组成

    NSInvocation 对象包含 Objective-C 消息的所有元素:消息接受者(target)、方法选择器(selector)、消息参数(arguments)、消息返回值(return value)。所有这些元素在 NSInvocation 对象中都可以直接设置,并且当 NSInvocation 对象被 invoke 时会自动设置返回值

    一个 NSInvocation 对象可以重复地将消息发送到不同的消息接受者;可以在两次发送消息之间修改消息参数以获取不同的消息返回值;甚至 NSInvocation 对象的方法选择器也可以更改为具有相同方法签名的其他方法选择器。这种灵活性使得 NSInvocation 对象有利于重复发送具有许多参数和变量的消息。可以在每次将消息发送到新的消息接受者之前根据需要修改 NSInvocation 对象,而不是为每个稍微不同的消息重新编写一次方法调用。但是 NSInvocation 对象不支持调用方法时使用(可变个数的参数)或者(联合参数)

    应该使用类方法 +invocationWithMethodSignature: 来创建一个 NSInvocation 对象,而不是使用 alloc / init 来创建一个 NSInvocation 对象

    默认情况下,NSInvocation 对象不会 retain 消息接受者和消息参数。如果消息接受者和消息参数可能在创建 NSInvocation 对象和使用 NSInvocation 对象期间消失,那么开发者应该自己显式地 retain 这些消息参数,或者调用 NSInvocation 对象的 -retainArguments 方法来使 NSInvocation 对象 retain 消息接受者和消息参数

    NSInvocation 遵守 NSCoding 协议,但只支持通过 NSPortCoder 进行编码。NSInvocation 不支持存档(archiving

  • 创建 NSInvocation 对象

    ① 类方法 +invocationWithMethodSignature:

    /*
    用给定的方法签名创建一个 NSInvocation 对象
    
    @param sig 方法签名
    @return 一个 NSInvocation 对象
    @note 新的 NSInvocation 对象必须设置消息接受者(target)、方法选择器(selector)、消息参数(arguments)后才能调用
    @note 不要使用 alloc / init 方法来创建 NSInvocation 对象
    @note 因为 NSInvocation 对象一经创建,其对应的方法签名便不能修改
    	  所以 NSInvocation 对象不支持调用方法时使用(可变个数的参数)或者(联合参数)
    */
    +(NSInvocation *)invocationWithMethodSignature:(NSMethodSignature *)sig;
    
  • 设置 NSInvocation 对象

    ① 属性 selector

    // 消息接受者的方法选择器。如果未设置,则为 NULL
    @property SEL selector;
    

    ② 属性 target

    // 消息接受者。如果未设置,则为 nil
    @property(assign) id target;
    

    ③ 对象方法 -setArgument:atIndex:

    /*
    设置消息接受者给定索引位置的参数值
    
    @param argumentLocation 一个无类型的缓冲区,包含将要分配给消息接受者的参数的值
    @param idx 一个用于标识参数索引的整数
    		  索引 0 和 1 处分别是隐藏参数 self 和 _cmd。可以直接通过设置 target 属性和 selector 属性来设置这两个隐藏参数的值
    	      使用大于等于 2 的索引来设置消息中的一般参数
    @note 此方法会复制缓冲区 argumentLocation 中的内容作为索引 idx 处的参数值,复制的字节数由方法签名中所标识的该参数的大小决定
    	  当参数值是一个基本数据类型时,请传入该基本数据的地址,以便复制该基本数据的值
    	  当参数值是一个对象类型时,请传入该对象的地址,以便复制指向该对象的指针:
    	  		NSArray *anArray;
    	  		[invocation setArgument:&anArray atIndex:3];
    @note 如果索引 idx 的值大于方法选择器所标识的实际的参数数量,则此方法将引发 NSInvalidArgumentException 异常
    */
    -(void)setArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
    

    ④ 对象方法 -getArgument:atIndex:

    /*
    获取消息接受者给定索引位置的参数值
    
    @param argumentLocation 一个无类型的缓冲区,用于存储返回的参数值
    @param idx 一个用于标识参数索引的整数
    		  索引 0 和 1 处分别是隐藏参数 self 和 _cmd。可以直接通过获取 target 属性和 selector 属性来获取这两个隐藏参数的值
    	      使用大于等于 2 的索引来获取消息中的一般参数
    @note 此方法会将索引 idx 处的参数值复制到缓冲区 argumentLocation 中。缓冲区 argumentLocation 必须足够大以存储参数值
    @note 当参数值是一个基本数据类型时,请传入对应基本数据类型变量的地址,以便复制该基本数据的值
    	  当参数值是一个对象类型时,请传入对应的对象类型变量的地址,以便复制指向该对象的指针:
    	  		NSArray *anArray;
    			[invocation getArgument:&anArray atIndex:3];
    @note 如果索引 idx 的值大于方法选择器所标识的实际的参数数量,则此方法将引发 NSInvalidArgumentException 异常
    */
    -(void)getArgument:(void *)argumentLocation atIndex:(NSInteger)idx;
    

    ⑤ 属性 argumentsRetained

    // 一个布尔值,用于标识 NSInvocation 对象是否已经 retain 了消息接受者和消息参数
    // 如果 NSInvocation 对象已经 retain 了消息接受者和消息参数,则返回 YES。否则返回 NO
    @property(readonly) BOOL argumentsRetained;
    

    ⑥ 对象方法 -retainArguments

    /*
    如果 NSInvocation 对象尚未这样做:
    则 retain 消息接受者和消息接受者所有对象类型的参数
    copy 消息接受者所有 C-string 和 Block 类型的参数
    如果消息的返回值已经设置,则同样 retain 或 copy 消息的返回值
    
    @note 在此方法调用前,属性 argumentsRetained 返回 NO
    	  在此方法调用后,属性 argumentsRetained 返回 YES
    @note 为了提高效率,新创建的 NSInvocation 对象不会执行 retainArguments 方法
    	  如果消息接受者和消息参数有可能在 NSInvocation 对象调用 invoke 方法之前被释放
    	  则可以手动调用 retainArguments 方法缓存消息接受者和消息参数
    @note 因为在定时器触发之前通常会有一个延迟
    	  所以 NSTimer 对象总是 retain 相应的消息接受者和消息参数
    */
    -(void)retainArguments;
    

    ⑦ 对象方法 -setReturnValue:

    /*
    设置消息接受者的返回值
    
    @param retLoc 一个无类型的缓冲区,它存储的内容将被复制并作为消息接受者的返回值
    */
    -(void)setReturnValue:(void *)retLoc;
    

    ⑧ 对象方法 -getReturnValue:

    /*
    获取消息接受者的返回值
    
    @param retLoc 一个无类型的缓冲区,NSInvocation 对象会将消息接受者的返回值复制到该缓冲区中
    			  缓冲区 retLoc 必须足够大以存储消息接受者的返回值
    @note 可以使用 NSMethodSignature 的属性 methodReturnLength 确定缓冲区的大小:
    			NSUInteger length = [[myInvocation methodSignature] methodReturnLength];
    			buffer = (void *)malloc(length);
    			[invocation getReturnValue:buffer];
    	  如果消息接受者的返回值是一个实例对象,则传递一个该实例对象类型的指针变量地址。该指针变量将会存储返回的实例对象指针:
    			id anObject;
    			NSArray *anArray;
    			[invocation1 getReturnValue:&anObject];
    			[invocation2 getReturnValue:&anArray];
    	  如果 NSInvocation 对象还未调用过(invoke 方法或者 invokeWithTarget: 方法),则消息接受者的返回值为空
    */
    -(void)getReturnValue:(void *)retLoc;
    
  • 调用一个 NSInvocation 对象

    ① 对象方法 -invoke

    /*
    使用给定的消息参数向给定的消息接受者发送给定的方法选择器所标识的消息,并设置消息的返回值
    
    @note 在调用此方法前,NSInvocation 对象必须设置:消息接受者(target)、方法选择器(selector)、消息参数(arguments)
    */
    -(void)invoke;
    

    ② 对象方法 -invokeWithTarget:

    /*
    使用给定的消息参数向给定的消息接受者发送给定的方法选择器所标识的消息,并设置消息的返回值
    
    @param target 消息接受者
    @note 在调用此方法前,NSInvocation 对象必须设置:方法选择器(selector)、消息参数(arguments)
    */
    -(void)invokeWithTarget:(id)target;
    
  • 获取方法签名

    ① 属性 methodSignature

    // NSInvocation 对象的方法签名(也即消息接受者的方法签名)
    @property(readonly, retain) NSMethodSignature *methodSignature;
    

PerfromSelector 与 NSInvocation 的简单使用

  • Objective-C 中调用方法的五种方式

    1. 使用 [receiver selector:argument]
    2. 使用 objc_msgSend(receiver, selector, argument)
    3. 使用 imp(receiver, selector, argument)
    4. 使用 [receiver performSelector:selector withObject:argument]
    5. 使用 NSInvocation 对象创建方法调用
  • 关于 PerfromSelector 的使用

    PerfromSelector 可以使用给定的参数(argument)调用给定对象(receiver)上的给定方法(selector):

    1. PerfromSelector 只能调用参数个数 <= 2 的方法
    2. PerfromSelector 只能调用参数为 id 类型 或者 没有参数 的方法
    3. PerfromSelector 只能调用返回值为 id 类型 或者 没有返回值 的方法
    4. PerfromSelector 调用的方法名称在编译阶段不做任何校验,等运行阶段再动态查找

    关于 PerfromSelector 更多的使用方法与注意点,请参考本文第一节

  • 关于 NSMethodSignature 的使用

    // 创建 NSMethodSignature 的 4 种方式
    -(void) NSMethodSignatureDemo {
        SEL selector = @selector(introduceWithName:age:height:sex:);
        // 1.通过 NSObject 的实例方法来获取方法签名
        NSMethodSignature* signature1 = [self methodSignatureForSelector:selector];
        // 2.通过 NSObject 的类方法来获取方法签名
        NSMethodSignature* signature2 = [self.class instanceMethodSignatureForSelector:selector];
        // 3.通过方法类型编码来获取方法签名
        NSMethodSignature* signature3 = [NSMethodSignature signatureWithObjCTypes:"@@:@ifc"];
        // 4.通过方法类型编码来获取方法签名
        NSMethodSignature* signature4 = [NSMethodSignature signatureWithObjCTypes:"@36@0:8@16i24f28c32"];
    }
    
    -(NSString *)introduceWithName:(NSString *)name age:(int)age height:(float)height sex:(char)sex {
        return [NSString stringWithFormat:@"name = %@, age = %d, height = %f, sex = %c", name, age, height, sex];
    }
    
  • 关于 NSInvocation 的使用

    Dog 类如下所示:

    // Dog.h
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @interface Dog : NSObject
    
    @property (nonatomic, strong) NSString* name;   // 名字
    @property (nonatomic, assign) int age;          // 年龄
    @property (nonatomic, assign) float height;     // 身高
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    
    // Dog.m
    #import "Dog.h"
    
    @implementation Dog
    
    @end
    

    Person 类如下所示:

    // Person.h
    #import <Foundation/Foundation.h>
    
    NS_ASSUME_NONNULL_BEGIN
    
    @class Dog;
    @interface Person : NSObject
    
    @property (nonatomic, strong) NSString* name;   // 名字
    @property (nonatomic, assign) int age;          // 年龄
    @property (nonatomic, assign) float height;     // 身高
    @property (nonatomic, strong) Dog* dog;         // 宠物
    
    // 介绍他人
    // @param name   他人姓名
    // @param age    他人年龄
    // @param height 他人身高
    // @param dog    他人宠物
    // @returen      他人
    -(Person *)introducePersonWithName:(NSString *)name age:(int)age height:(float)height dog:(Dog *)dog;
    
    // 创建一个人(默认)
    +(Person *)defaultPerson;
    
    @end
    
    NS_ASSUME_NONNULL_END
    
    
    
    // Person.m
    #import "Person.h"
    #import "Dog.h"
    
    @implementation Person
    
    -(Person *)introducePersonWithName:(NSString *)name age:(int)age height:(float)height dog:(Dog *)dog {
        Person* otherPerson = [[Person alloc] init];
        otherPerson.name = name;
        otherPerson.age = age;
        otherPerson.height = height;
        otherPerson.dog = dog;
        return otherPerson;
    }
    
    // 创建一个人(默认)
    +(Person *)defaultPerson {
        Person* person = [[Person alloc] init];
        person.name = @"hcg";
        person.age = 20;
        person.height = 170.0f;
        
        Dog* dog = [[Dog alloc] init];
        dog.name = @"husky";
        dog.age = 2;
        dog.height = 0.5f;
        person.dog = dog;
        
        return person;
    }
    @end
    

    ③ 通过 NSInvocation 对象调用 Person 类的 -introducePersonWithName:age:height:dog: 方法

    -(void)NSInvocationDemo {
        // 1.创建 invocation 对象
        SEL aSelector = @selector(introducePersonWithName:age:height:dog:);
        NSMethodSignature* aSignature = [Person instanceMethodSignatureForSelector:aSelector];
        NSInvocation* anInvocation = [NSInvocation invocationWithMethodSignature:aSignature];
        // 2.为 invocation 对象设置参数
        // 2.0 设置第 0 个参数:target
        Person* person = [Person defaultPerson];
        anInvocation.target = person;
        // 2.1 设置第 1 个参数:selector
        SEL selector = @selector(introducePersonWithName:age:height:dog:);
        anInvocation.selector = selector;
        // 2.2 设置第 2 个参数:name
        NSString* name = @"hzp";
        [anInvocation setArgument:&name atIndex:2];
        // 2.3 设置第 3 个参数:age
        int age = 30;
        [anInvocation setArgument:&age atIndex:3];
        // 2.4 设置第 4 个参数:height
        float height = 180.0f;
        [anInvocation setArgument:&height atIndex:4];
        // 2.5 设置第 5 个参数:dog
        Dog* dog = [[Dog alloc] init];
        dog.name = @"Canis lupus familiaris";
        dog.age = 4;
        dog.height = 1.0f;
        [anInvocation setArgument:&dog atIndex:5];
        // 3.让 invocation 对象缓存调用参数
        [anInvocation retainArguments];
        // 4.调用方法
        [anInvocation invoke];
        // 5.获取方法返回值
        // 在 ARC 模式下,getReturnValue: 方法仅仅是将 anInvocation 对象中 introducePersonWithName:age:height:dog: 方法返回的 person 对象的地址拷贝到 otherPerson 变量中,并没有处理相应的内存管理问题
        // 而定义的 otherPerson 变量默认是 __strong 的,此时 ARC 就会假设 introducePersonWithName:age:height:dog: 方法返回的 person 对象已经被 retain(但实际上并没有)
        // 因为 anInvocation 对象是局部变量,所以如果 ARC 在第 6 步输出方法返回值之前插入了内存回收的代码,则在执行第 6 步输出方法返回值时 anInvocation 已经被回收(即保存在 anInvocation 对象中的返回值也被回收了)
        // 此时如果再输出 anInvocation 对象中保存的返回值,就会因访问不可用内存而导致程序奔溃
        // 解决方法有 3 种:
        // 5.1 使用一个弱对象指针(__unsafe_unretained)来保存返回值的地址
        __unsafe_unretained Person* otherPerson = nil;
        [anInvocation getReturnValue:&otherPerson];
        // 5.2 使用 void* 指针来保存返回值的地址,然后再用 __bridge 将其转换成 Objective-C 对象
        // void* ptr = NULL;
        // [anInvocation getReturnValue:&ptr];
        // Person* otherPerson = (__bridge Person *)ptr;
        // 5.3 强引用 anInvocation 对象,让其不要释放
        // self.anInvocation = anInvocation;
        // 6.输出方法返回值
        NSLog(@"otherPerson.name = %@", otherPerson.name);              // otherPerson.name = hzp
        NSLog(@"otherPerson.age = %d", otherPerson.age);                // otherPerson.age = 30
        NSLog(@"otherPerson.height = %f", otherPerson.height);          // otherPerson.height = 180.000000
        NSLog(@"otherPerson.dog.name = %@", otherPerson.dog.name);      // otherPerson.dog.name = Canis lupus familiaris
        NSLog(@"otherPerson.dog.age = %d", otherPerson.dog.age);        // otherPerson.dog.age = 4
        NSLog(@"otherPerson.dog.height = %f", otherPerson.dog.height);  // otherPerson.dog.height = 1.000000
    }
    

    ④ 通过 LLDB 打印 aSignatureanInvocation

    po aSignature
    po anInvocation

  • 通过 NSInvocation 封装一个可以调用带任意参数的方法

    -(id)invocationWithTarget:(NSString *)targetName
                      selector:(NSString *)selectorName
                    parameters:(NSArray *)parameters {
        
        Class cls = NSClassFromString(targetName);
        if (nil == cls) {
            NSLog(@"找不到类:%@", targetName);
            return nil;
        }
        NSObject* target = [[cls alloc] init];
        
        SEL selector = NSSelectorFromString(selectorName);
        NSMethodSignature* signature = [cls instanceMethodSignatureForSelector:selector];
        if (nil == signature)
        {
            NSLog(@"找不到方法:%@", selectorName);
            return nil;
        }
    
        NSInvocation* anInvocation = [NSInvocation invocationWithMethodSignature:signature];
        [anInvocation setTarget:target];
        [anInvocation setSelector:selector];
        
        if (![parameters isKindOfClass:[NSArray class]] ||
            parameters.count != signature.numberOfArguments -2) {
            NSLog(@"参数列表错误");
            NSLog(@"[parameters class] = %@", [parameters class]);
            NSLog(@"parameters.count(%ld) + 2 != signature.numberOfArguments(%ld)", parameters.count, signature.numberOfArguments);
            return nil;
        }
        // 设置消息发送的参数
        // 因为 anInvocation 的第 0 个和第 1 个参数分别是:id 类型的 self 和 SEL 类型的 _cmd
        // 所以普通参数的索引从 2 开始
        for (int i = 0; i < parameters.count; i++) {
            const char * argumentType = [signature getArgumentTypeAtIndex:(i + 2)];
            // TODO:需要根据 signature 对象所标识的参数类型做对应的解析
            // 这里暂时假设所有参数类型均为 Objective-C 对象
            if (0 == strcmp(argumentType, "@")) {
                id argument = parameters[i];
                [anInvocation setArgument:&argument atIndex:(i + 2)];
            }
        }
        
        // 执行方法调用
        [anInvocation invoke];
            
        // 获取返回值
        if(0 != signature.methodReturnLength)
        {
            const char * returnType = signature.methodReturnType;
            // TODO:需要根据 signature 对象所标识的返回值类型做对应的解析,比如将基本数据类型的返回值包装成对象类型
            // 这里暂时假设所有的返回值类型均为 Objective-C 对象
            if (0 == strcmp(returnType, "@")) {
                __unsafe_unretained id returnValue = nil;
                [anInvocation getReturnValue:&returnValue];
                return returnValue;
            }
        }
        
        return nil;
    }
    

用字符串创建:Class && Selector && Protocol

  • 函数介绍

    NSClassFromString

    /*
    获取给定类名所标识的类对象
    
    @param 类的名称
    @return 类名 aClassName 所标识的类对象
    	    如果 RunTime 当前没有加载给定类名 aClassName 所标识的类对象,则返回 nil
    	    如果类名 aClassName 传 nil,则返回 nil
    */
    Class NSClassFromString(NSString *aClassName);
    

    NSStringFromClass

    /*
    获取给定类对象的名称
    
    @param 要获取名称的类对象
    @return 类对象 aClass 的名称
    	    如果类对象 aClass 传 nil,则返回 nil
    */
    NSString * NSStringFromClass(Class aClass);
    

    NSSelectorFromString

    /*
    获取给定方法名称所标识的方法选择器
    
    @param aSelectorName 方法的名称
    @return 方法名称 aSelectorName 所标识的方法选择器
    		如果方法名称 aSelectorName 传 nil,则返回 (SEL)0
    		如果方法名称 aSelectorName 不能转换为 UTF-8 编码(应该是由于内存不足导致的),则返回 (SEL)0
    */
    SEL NSSelectorFromString(NSString *aSelectorName);
    

    NSStringFromSelector

    /*
    获取给定方法选择器的名称
    
    @param aSelector 要获取名称的方法选择器
    @return 方法选择器 aSelector 的名称
    */
    NSString * NSStringFromSelector(SEL aSelector);
    

    NSProtocolFromString

    /*
    获取给定协议名称所标识的协议对象
    
    @param namestr 协议的名称
    @return 协议名称 namestr 所标识的协议对象
    		如果 RunTime 当前没有加载给定协议名称 namestr 所标识的协议对象,则返回 nil
    	    如果协议名称 namestr 传 nil,则返回 nil
    */
    Protocol * NSProtocolFromString(NSString *namestr);
    

    NSStringFromProtocol

    /*
    获取给定协议对象的名称
    
    @param proto 要获取名称的协议对象
    @return 协议对象 proto 的名称
    */
    NSString * NSStringFromProtocol(Protocol *proto);
    
  • 使用场景

    NSClassFromStringNSSelectorFromStringNSProtocolFromString 可以获取到一个类的:类对象、方法选择器、协议对象

    PerfromSelectorNSInvocation 可以隐式地执行给定对象上的给定方法

    结合两者,可以在拿不到一个类的源码(甚至是头文件)的情况下,创建该类的实例对象并调用该类相应的方法

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值