NSObject 中的消息发送应用

众所周知,Objective-C 是使用消息传递机制来实现的面向对象。那么 NSObject 作为 Cocoa 中的基类,提供一些消息发送的方法就显得理所当然了。

简介

NSObject 中的消息发送更加复杂一些,它还受到线程和 Runloop 的影响

  • 可以指定发送消息到主线程或者任意一条自定义线程中执行
  • 可以通过 Runloop 指定消息发送的时间,实现延迟执行的效果
  • 可以指定消息执行时的 Runloop mode
  • 可以取消 Runloop 中等待执行的方法

原理

当调用 NSObjectperformSelecter:afterDelay: 时,其内部会创建一个 Timer Item 并添加到当前线程的 RunLoop 中;所以如果当前线程没有 RunLoop,则这个方法会失效

由于 Cocoa 中 Runloop 是惰性加载的,所以在子线程中如果不主动获取 RunloopRunloop 是不存在的,因此在子线程中通过 NSObject 的消息发送延迟执行以及指定 Runloop mode 的方法都不会被执行

用途

基于以上消息发送机制的特性,我们可以用来做一些实用的小功能

将短时间内的大量调用合并为一次

在开发中,常常会遇到这样的问题:如果用户飞快点击一个按钮,就会不停的调用对应的方法,导致出现 bug。通常的解决方案是点击后将按钮的状态在一定时间内置为 disable 防止用户再次点击,这个方法的具体实现五花八门,有继承重写按钮类的,有通过运行时 swizzle 方法达到目的的。

但我认为最优雅的方法当属通过 NSObject 的消息发送方法实现,我们只要给 NSObject 添加一个分类,添加一个可以取消的消息发送方法即可

1
2
3
4
5
6
7
8
- (void)coalescePerformSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:aSelector object:anArgument];
[self performSelector:aSelector withObject:anArgument afterDelay:delay inModes:modes];
}

- (void)coalescePerformSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay {
[self coalescePerformSelector:aSelector withObject:anArgument afterDelay:delay inModes:@[NSDefaultRunLoopMode]];
}

延迟执行一个 block

可以通过消息机制延迟执行一个 block,类似于 dispatch_after 方法,但是用起来更加方便

1
2
3
4
5
6
7
8
// Private function
- (void)_privateExecuteBlock:(void(^)(void))block {
block();
}

- (void)performBlock:(void(^)(void))block afterDelay:(NSTimeInterval)delay {
[self performSelector:@selector(_privateExecuteBlock:) withObject:[block copy] afterDelay:delay];
}

这种方式通常只能在主线程使用,如果是其他线程,建议老实用 dispatch_after

发送多个参数

系统提供的消息 Api 都只能传送一个参数,但是如果有发送多个参数,除了将参数打包之外,还可以使用 NSInvocation 将方法包装起来再调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)performSelector:(SEL)aSelector afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes withFirstArgument:(id)firstArgument argumentsList:(va_list)arguments {
NSMethodSignature *signature = [self methodSignatureForSelector:aSelector];

NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature];
[invocation setTarget:self];
[invocation setSelector:aSelector];

id argument = firstArgument;
for (NSUInteger idx = 2; idx < signature.numberOfArguments; idx++) {
[invocation setArgument:&argument atIndex:idx];
argument = va_arg(arguments, NSObject *);
}
[invocation retainArguments];
[invocation performSelector:@selector(invoke) withObject:nil afterDelay:delay inModes:modes];
}

- (void)performSelector:(SEL)aSelector afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes withArguments:(id)anArgument,... {
va_list ap;
va_start(ap, anArgument);
[self performSelector:aSelector afterDelay:delay inModes:modes withFirstArgument:anArgument argumentsList:ap];
va_end(ap);
}

performSelector 内存泄漏的警告

在 ARC 环境下,编译器是通过 newalloccopy 等开头的方法判断对象的持有者来进行内存管理的;因此在通过 performSelector 调用方法时,编译器不能准确的识别调用的方法是否属于上诉方法,所以编译时会有内存泄漏的警告。
解决方案也很简单,加一个编译宏关闭这个检查即可。但是这样一来又不安全,所以我们需要先自行检查

performSelector 调用的所有方法都是拿不到返回值的,所以我们干脆就判定,如果一个方法有返回值,则它就是不安全的

1
2
3
4
5
6
7
8
9
- (void)safePerformSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes {

NSAssert(strcmp([self methodSignatureForSelector:aSelector].methodReturnType, @encode(void)) == 0, @"Only methods that return a void value can be executed");

#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[self performSelector:aSelector withObject:anArgument afterDelay:delay inModes:modes];
#pragma clang diagnostic pop
}

内存警告的讨论

  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码
  • Copyrights © 2017-2021 HonQi

请我喝杯咖啡吧~