开发中,经常会遇到将耗时操作放到子线程中执行,来提升应用性能的场景。当子线程中的任务执行完毕后,线程就被立刻销毁。
如果开发中需要经常在子线程中执行任务,那么频繁的创建和销毁线程就会造成资源的浪费,这不符合我们的初衷。 此时就需要我们对线程进行保活,保证线程在应该处理事情的时候醒来,空闲的时候休眠。
我们知道 RunLoop 可以在需要处理事务的时候醒来执行任务,空闲的时候休眠来节省资源,利用这个特性就可以来处理线程的保活,控制线程的生命周期。
从探索到成功
- (void)viewDidLoad {
[super viewDidLoad];
LSThread *thread = [[LSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
}
- (void)run {
NSLog(@"func -- %s thread -- %@", __func__, [NSThread currentThread]);
[[NSRunLoop currentRunLoop] run];
NSLog(@"--- 结束 ---");
}
LSThread
继承自 NSThread
,重写了 dealloc
方法
- (void)dealloc {
NSLog(@"%s", __func__);
}
执行之后的结果:
可以看到线程没能保活:
- 虽然启动了 RunLoop,依然执行了下面的结束 log
- 线程在执行完毕之后被销毁了
为了保证线程执行完毕不被销毁,可以强引用线程
self.thread = [[LSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
但是这样并不能解决 RunLoop 问题。那么已经启动了 RunLoop,为什么并没有保持它的持续运行呢?
我们来看一下 run
方法的定义
If no input sources or timers are attached to the run loop, this method exits immediately.
意思是如果没有sources或timers附加到RunLoop,那么这个方法会立即退出。
那么我们给 RunLoop 添加一个 source 或者 timer 应该就可以解决这个问题了
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
再次运行并没有执行结束 log,线程保活成功。
下面继续来完善我们的需求。当持有线程的控制器销毁时,新建的子线程也应该跟着被销毁,在控制器里添加 dealloc
- (void)dealloc {
NSLog(@"--- 销毁控制器 --- %s", __func__);
}
在控制器出现时,创建子线程,控制器消失时控制台输出如下
控制器和子线程都没有被销毁。查看代码
self.thread = [[LSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
这里控制器强引用了 thread,thread 又在内部持有了控制器 self,造成了引用循环。
那么要打破这个引用循环可以使用 Block
self.thread = [[LSThread alloc] initWithBlock:^{
NSLog(@"func -- %s thread -- %@", __func__, [NSThread currentThread]);
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"--- 结束 ---");
}];
执行结果
控制器被成功释放,但是子线程并没有被销毁,那么这个子线程变成了一个全局性质的。到这里就要说一下 RunLoop 的启动了。
RunLoop 有三种启动方式
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
run
方法内部会在NSDefaultRunLoopMode
模式下重复调用runMode:beforeDate:
方法。
runUntilDate:
方法可以设置超时时间,在超时时间到达之前,RunLoop会一直运行,在此期间RunLoop会处理来自sources的数据,并且 像 run
一样,也会在NSDefaultRunLoopMode
模式下重复调用runMode:beforeDate:
方法。
runMode:beforeDate:
方法RunLoop会运行一次,超时时间到达或者第一个source被处理,则RunLoop就会退出。
关于 run
方法 Apple 文档中有说如果希望退出 RunLoop,不应使用此方法。
如果RunLoop没有input sources或者附加的timer,RunLoop就会退出。虽然这样可以将RunLoop退出,但是Apple不建议我们这么做,系统内部有可能会在当前线程的RunLoop中添加一些输入源,所以通过手动移除input source或者timer这种方式,并不能保证RunLoop一定会退出。
那么问题就很明了了,我们不应该使用 run
方法来启动 RunLoop,因为它创建的是一个不会退出的循环,使用这个方法的子线程自然无法被销毁。我们可以像run
一样利用runMode:beforeDate:
方法来创建一个符合我们条件的子线程:
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
把它放到一个 while
循环中,利用一个是否停止 RunLoop 的全局标记来辅助处理线程的生命周期问题
__weak typeof(self) weakSelf = self;
self.thread = [[LSThread alloc] initWithBlock:^{
NSLog(@"func -- %s thread -- %@", __func__, [NSThread currentThread]);
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
// [[NSRunLoop currentRunLoop] run];
while (!weakSelf.isStopedThread) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"--- 结束 ---");
}];
停止 RunLoop 的方法
- (void)stop {
self.isStopedThread = YES;
CFRunLoopStop(CFRunLoopGetCurrent());
}
这里有一点需要注意,停止操作一定要在我们的目标线程执行,比如我们直接调用 stop
方法并不能达到我们预期的效果,这是因为stop
默认在主线程执行,没有拿到目标线程,停止无效。
- (void)stopAction {
[self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
self.thread = nil;
}
在当前线程调用stop
,我们的目的就达到了,顺利的结束了 RunLoop,线程也跟着销毁了。