iOS-RunLoop详解(三):使用RunLoop线程保活方案
如果经常要在子线程中做事情,不使用保活,就会一直创建、销毁子线程,这样很耗性能,所以经常在子线程做事情最好使用线程保活。
实现线程保活
创建线程类,表示需要经常执行的任务
***********************? MJThread.h ?**************************
@interface MJThread : NSThread
@end
#import "MJThread.h"
@implementation MJThread
***********************? MJThread.m ?**************************
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
***********************? ViewController.m ?**************************
@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// NSThread 频繁创建线程
MJThread *thread = [[MJThread alloc]initWithTarget:self selector:@selector(test) object:nil];
[thread start];
// [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 子线程需要执行的任务
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
@end
我们可以用RunLoop
来延长线程的生命周期,不让线程挂掉
// 子线程需要执行的任务
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
[[NSRunLoop currentRunLoop] run];
NSLog(@"%s ----end----", __func__);
}
我们需要往RunLoop
中添加任务,任何任务都可以.
// 子线程需要执行的任务
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
NSLog(@"?????????????");
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"%s ----end----", __func__);
}
每一次点击屏幕,都是创建了[[MJThread alloc]initWithTarget:self selector:@selector(test) object:nil];
经过[[NSRunLoop currentRunLoop] addPort
和 [[NSRunLoop currentRunLoop] run];
线程进入休眠了,但是我们现在没办法唤醒线程和执行线程任务,程序继续修改。
上面的代码thread
是一个局部变量,每次执行任务都会重新创建,所以我们把线程设置成成员属性。
***********************? ViewController.m ?**************************
@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[MJThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
//触碰屏幕事件
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 子线程需要执行的任务
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
NSLog(@"?????????????");
}
// 这个方法的目的:线程保活
- (void)run {
NSLog(@"%s %@", __func__, [NSThread currentThread]);
NSLog(@"---------- start -----------");
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"%s ----end----", __func__);
}
@end
上面的代码有两个问题:
- self和thread会造成循环引用,都不会释放
- thread一直不会死
首先解决循环引用:
#import "ViewController.h"
#import "MJThread.h"
@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//如果使用如下方式创建thread,self会引用thread,thread会引用self,会造成循环引用。
//[[MJThread alloc] initWithTarget:self selector:@selector(run) object:nil];
self.thread = [[MJThread alloc] initWithBlock:^{
NSLog(@"%@----begin----", [NSThread currentThread]);
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//线程会一直阻塞这这一行,永远不会销毁
[[NSRunLoop currentRunLoop] run];
//当把NSRunLoop停掉之后,代码就会从下一行往下走,这时候任务执行完成,线程该死的时候就会死了。
NSLog(@"%@----end----", [NSThread currentThread]);
}];
[self.thread start];
}
- (void)dealloc
{
NSLog(@"%s", __func__);
//就算把thread清空,thread也不会销毁,因为任务还没结束,线程就不会死。
//self.thread = nil;
}
如果我们想要精准的控制线程的生命周期,比如说控制器销毁的时候,线程也销毁,那应该怎么做呢?我们可以像下面这样手动停止RunLoop
:
- (IBAction)stop {
// 在子线程调用stop
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 用于停止子线程的RunLoop
- (void)stopThread
{
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
线程不会死的原因就是有个RunLoop一直在运行,线程一直有任务做,所以想让线程死掉,就把RunLoop停掉,当把RunLoop停掉之后,代码就会从 [[NSRunLoop currentRunLoop] run]往下走,当线程执行完任务后,线程该死的时候(当前控制器销毁后)就会死了。
我们看run方法的解释:
it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:.
In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.
翻译过来就是:
它通过反复调用runMode:beforeDate:在NSDefaultRunLoopMode中运行接收器。换句话说,这个方法有效地开始了一个无限循环,处理来自运行循环的输入源和计时器的数据。
可以看出,通过run方法运行的RunLoop是无法停止的,它专门用于开启一个永不销毁的线程(NSRunLoop)。
既然这样,那我们可以模仿run方法,写个while循环,内部也调用runMode:beforeDate:方法,如下:
while (!weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
//while的条件判断中要使用weakSelf,不然self强引用thread,thread强引用block,block强引用self,产生循环引用
不使用run方法,我们就能停掉RunLoop了,停掉RunLoop系统有提供API是CFRunLoopStop(CFRunLoopGetCurrent()),但是这个API不能在ViewController的dealloc方法里面写,因为ViewController的dealloc方法是在主线程调用的,我们要保证在子线程调用CFRunLoopStop(CFRunLoopGetCurrent())。
#import "ViewController.h"
#import "MJThread.h"
@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.stopped = NO;
self.thread = [[MJThread alloc] initWithBlock:^{
NSLog(@"%@----begin----", [NSThread currentThread]);
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (!weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"%@----end----", [NSThread currentThread]);
// NSRunLoop的run方法是无法停止的,它专门用于开启一个永不销毁的线程(NSRunLoop)
// [[NSRunLoop currentRunLoop] run];
/*
it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:.
In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers
*/
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 子线程需要执行的任务
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
NSLog(@"?????????????");
}
- (IBAction)stop {
// 在子线程调用stop
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 用于停止子线程的RunLoop
- (void)stopThread
{
// 设置标记为NO
self.stopped = YES;
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
//就算把thread清空,thread也不会销毁,因为任务还没结束,线程就不会死。
self.thread = nil;
// [self stop];
}
@end
上面代码还有一个问题,就是我们每次都要先点击停止再返回当前VC,这样很麻烦,可能你会说可以把[self stop]方法写在ViewController的dealloc方法里面,试了下,发现报错坏内存访问:
这是为什么呢?这就是我们上面讲的waitUntilDone
造成的.我们在[self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:NO];
中把waitUntilDone
设置为NO
,就表示在子线程执行的stopRunLoop
函数和在主线程执行的- (IBAction)stop
函数是同时执行的.一旦- (IBAction)stop
函数先执行完,那么ViewController
的dealloc
函数也会立马执行完毕,ViewController
就会释放.这时候再去执行stopRunLoop
就会报坏内存访问
,因为ViewController
已经释放了.为什么会崩溃到[[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
这一行呢?
现在你应该明白为什么会在RunLoop那行代码报坏内存访问错误了吧!
解决办法也很简单,dealloc方法里面调用[self stop],并且将上面NO改成YES。
- (IBAction)stop {
// 在子线程调用stop
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self stop];
}
运行代码,直接返回当前VC,打印:
2021-05-14 15:59:38.055083+0800 Interview03-线程保活[4787:225673] -[ViewController dealloc]
2021-05-14 15:59:38.055307+0800 Interview03-线程保活[4787:225794] -[ViewController stopThread] <MJThread: 0x600000a8dd00>{number = 8, name = (null)}
我们点击返回退出控制器后ViewController
释放了,但是没有看到线程释放
其实那个RunLoop的确停掉了,但是停掉之后,他会再次来到while循环判断条件:
我们在while
循环中打一个断点:
这时候当前控制器已经被销毁,weakSelf指针已经被清空,这时候!nil获取的就是YES,所以会再次进入循环体启动RunLoop,RunLoop又跑起来了,线程又有事情干了,所以线程不会销毁。
解决办法:
while (weakSelf && !weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
再次运行项目,返回当前VC
2021-05-14 16:03:29.471773+0800 Interview03-线程保活[4863:229852] <MJThread: 0x600000452e40>{number = 9, name = (null)}----end----
2021-05-14 16:03:29.472196+0800 Interview03-线程保活[4863:229852] -[MJThread dealloc]
再次运行项目,点击暂停,返回当前VC,这时候又崩了
点击暂停之后RunLoop肯定停掉了,RunLoop停掉后,这时候的线程就不能用了,runloop
停止掉后它的任务就执行完了,线程的生命周期已经结束了,这时候它已经不能再执行任务了.但是这时候thread还没销毁(还没调用dealloc),因为thread还被self引用着,我们点击返回
按钮,又让子线程去执行stopRunLoop
任务就会报错,这时候访问一个不能用的thread就会报坏内存访问错误。
解决办法也很简单,暂停RunLoop后把thread指针置为nil,并且如果发现子线程为nil就不在子线程执行任务了。
最后的完整代码
#import "ViewController.h"
#import "MJThread.h"
@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.stopped = NO;
self.thread = [[MJThread alloc] initWithBlock:^{
NSLog(@"%@----begin----", [NSThread currentThread]);
// 往RunLoop里面添加Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"%@----end----", [NSThread currentThread]);
// NSRunLoop的run方法是无法停止的,它专门用于开启一个永不销毁的线程(NSRunLoop)
// [[NSRunLoop currentRunLoop] run];
/*
it runs the receiver in the NSDefaultRunLoopMode by repeatedly invoking runMode:beforeDate:.
In other words, this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers
*/
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 子线程需要执行的任务
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
NSLog(@"?????????????");
}
- (IBAction)stop {
// 在子线程调用stop
if (!self.thread) return;
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
// 用于停止子线程的RunLoop
- (void)stopThread
{
// 设置标记为NO
self.stopped = YES;
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
self.thread = nil;
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self stop];
}
@end