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
RUN> 🏇🏇🏇
我们每次点击,都会去执行任务:
NSThread 频繁创建线程
2021-05-14 14:55:19.054958+0800 Interview03-线程保活[4068:166884] -[ViewController test] <MJThread: 0x600000634900>{number = 7, name = (null)} 2021-05-14 14:55:19.056474+0800 Interview03-线程保活[4068:166884] -[MJThread dealloc] 2021-05-14 14:55:21.627640+0800 Interview03-线程保活[4068:166923] -[ViewController test] <MJThread: 0x60000062ddc0>{number = 8, name = (null)} 2021-05-14 14:55:21.627815+0800 Interview03-线程保活[4068:166923] -[MJThread dealloc]
可以看到,任务一执行完,线程就释放了.并且这样频繁的创建线程,很消耗资源
我们可以用RunLoop
来延长线程的生命周期,不让线程挂掉
// 子线程需要执行的任务
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
[[NSRunLoop currentRunLoop] run];
NSLog(@"%s ----end----", __func__);
}
RUN> 🏇🏇🏇
2021-05-14 15:01:54.818875+0800 Interview03-线程保活[4128:171606] -[ViewController test] <MJThread: 0x60000147d100>{number = 7, name = (null)} 2021-05-14 15:01:54.819344+0800 Interview03-线程保活[4128:171606] -[ViewController test] ----end---- 2021-05-14 15:01:54.821043+0800 Interview03-线程保活[4128:171606] -[MJThread dealloc]
运行一下,发现线程执行完任务还是会结束线程(
[MJThread dealloc])
这是因为,我们虽然获取了当前的
RunLoop
,并且调用run
方法让RunLoop
跑起来了,而run
方法底层调用的是- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
方法,这个方法会把RunLoop
添加到NSDefaultRunLoopMode
模式下.而Model
中如果没有任何Source0 , Source1 , Timer , Observer
,RunLoop
会立马退出. 所以我们需要往RunLoop
中添加任务,任何任务都可以.
我们需要往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__);
}
RUN> 🏇🏇🏇
2021-05-14 15:05:54.882701+0800 Interview03-线程保活[4172:174678] -[ViewController test] <MJThread: 0x600003c90f00>{number = 7, name = (null)} 2021-05-14 15:05:54.883756+0800 Interview03-线程保活[4172:174678] 🏌🏌🏌🏌🏌🏌🏌🏀🏀🏀🏅🏅🏅
在运行线程就不会执行完任务就挂掉了,而是执行完任务就休眠:
每一次点击屏幕,都是创建了[[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
RUN> 🏇🏇🏇
2021-05-14 15:14:16.185593+0800 Interview03-线程保活[4221:180238] -[ViewController test] <MJThread: 0x6000005ecb40>{number = 7, name = (null)} 2021-05-14 15:14:16.185768+0800 Interview03-线程保活[4221:180238] 🏌🏌🏌🏌🏌🏌🏌🏀🏀🏀🏅🏅🏅 2021-05-14 15:14:17.821116+0800 Interview03-线程保活[4221:180238] -[ViewController test] <MJThread: 0x6000005ecb40>{number = 7, name = (null)} 2021-05-14 15:14:17.821240+0800 Interview03-线程保活[4221:180238] 🏌🏌🏌🏌🏌🏌🏌🏀🏀🏀🏅🏅🏅 2021-05-14 15:14:20.824925+0800 Interview03-线程保活[4221:180238] -[ViewController test] <MJThread: 0x6000005ecb40>{number = 7, name = (null)} 2021-05-14 15:14:20.825090+0800 Interview03-线程保活[4221:180238] 🏌🏌🏌🏌🏌🏌🏌🏀🏀🏀🏅🏅🏅
上面的代码有两个问题:
- 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;
}
运行后,在当前界面返回
RUN> 🏇🏇🏇
2021-05-14 15:21:44.228798+0800 Interview03-线程保活[4480:196517] <MJThread: 0x600002a04080>{number = 9, name = (null)}----begin---- 2021-05-14 15:21:47.517069+0800 Interview03-线程保活[4480:195322] -[ViewController dealloc]
可以发现ViewController销毁了,但是thread还是没被销毁
很奇怪,控制器都释放了,按理说控制器内部的所有东西都应该释放了呀,我们在
ViewController
的dealloc
方法中把thread
置为nil
:2021-05-14 15:25:09.585499+0800 Interview03-线程保活[4520:199781] <MJThread: 0x600003b4ab40>{number = 8, name = (null)}----begin---- 2021-05-14 15:25:13.501110+0800 Interview03-线程保活[4520:199781] -[ViewController test] <MJThread: 0x600003b4ab40>{number = 8, name = (null)} 2021-05-14 15:25:13.501288+0800 Interview03-线程保活[4520:199781] 🏌🏌🏌🏌🏌🏌🏌🏀🏀🏀🏅🏅🏅 2021-05-14 15:25:16.475845+0800 Interview03-线程保活[4520:199617] -[ViewController dealloc]
把
thread
强制置为nil
,thread
还是没有释放.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]); }];
这是因为RunLoop在 [[NSRunLoop currentRunLoop] run]这一行一直阻塞,一直不会打印----end----,这时候任务一直在进行,任务还没有完成线程就不会死,就算在ViewController的dealloc方法里面把thread清空,thread也不会死。
如果我们想要精准的控制线程的生命周期,比如说控制器销毁的时候,线程也销毁,那应该怎么做呢?我们可以像下面这样手动停止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]);
}
run> 🏇🏇🏇
2021-05-14 15:34:46.499408+0800 Interview03-线程保活[4566:205814] <MJThread: 0x600001f1b900>{number = 9, name = (null)}----begin---- 2021-05-14 15:34:55.230839+0800 Interview03-线程保活[4566:205814] -[ViewController test] <MJThread: 0x600001f1b900>{number = 9, name = (null)} 2021-05-14 15:34:55.230956+0800 Interview03-线程保活[4566:205814] 🏌🏌🏌🏌🏌🏌🏌🏀🏀🏀🏅🏅🏅 2021-05-14 15:34:57.846728+0800 Interview03-线程保活[4566:205814] -[ViewController stopThread] <MJThread: 0x600001f1b900>{number = 9, name = (null)} 2021-05-14 15:35:14.203913+0800 Interview03-线程保活[4566:205687] -[ViewController dealloc]
stopRunLoop
虽然执行了,并且ViewController
也已经销毁了,但是thread
仍然没有销毁,这是为什么呢?
线程不会死的原因就是有个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
RUN> 🏇🏇🏇
2021-05-14 15:39:47.441947+0800 Interview03-线程保活[4606:209299] <MJThread: 0x600000f47580>{number = 8, name = (null)}----begin---- 2021-05-14 15:39:48.914864+0800 Interview03-线程保活[4606:209299] -[ViewController test] <MJThread: 0x600000f47580>{number = 8, name = (null)} 2021-05-14 15:39:48.915021+0800 Interview03-线程保活[4606:209299] 🏌🏌🏌🏌🏌🏌🏌🏀🏀🏀🏅🏅🏅 2021-05-14 15:39:50.900749+0800 Interview03-线程保活[4606:209299] -[ViewController stopThread] <MJThread: 0x600000f47580>{number = 8, name = (null)} 2021-05-14 15:39:50.901004+0800 Interview03-线程保活[4606:209299] <MJThread: 0x600000f47580>{number = 8, name = (null)}----end---- 2021-05-14 15:39:59.322333+0800 Interview03-线程保活[4606:209161] -[ViewController dealloc] 2021-05-14 15:39:59.322455+0800 Interview03-线程保活[4606:209161] -[MJThread dealloc]
上面要使用weakself,不然self强引用thread,thread强引用block,block强引用self,产生循环引用(使用weakself之后,就是self强引用thread,thread强引用block,block弱引用self,不会产生循环引用)。
运行代码,进入界面,打印:
2021-05-14 15:39:47.441947+0800 Interview03-线程保活[4606:209299] <MJThread: 0x600000f47580>{number = 8, name = (null)}----begin----
说明线程开始工作了。
点击空白,打印:
2021-05-14 15:42:22.608171+0800 Interview03-线程保活[4606:211084] -[ViewController test] <MJThread: 0x600000f38540>{number = 9, name = (null)} 2021-05-14 15:42:22.610916+0800 Interview03-线程保活[4606:211084] 🏌🏌🏌🏌🏌🏌🏌🏀🏀🏀🏅🏅🏅
说明RunLoop接收到事件,开始处理事件。
点击stop打印:
2021-05-14 15:42:35.860716+0800 Interview03-线程保活[4606:211084] -[ViewController stopThread] <MJThread: 0x600000f38540>{number = 9, name = (null)} 2021-05-14 15:42:35.861011+0800 Interview03-线程保活[4606:211084] <MJThread: 0x600000f38540>{number = 9, name = (null)}----end----
可以看出,执行了CFRunLoopStop,并且线程任务完成,打印了----end----。
点击stop之后再退出当前VC,打印:
2021-05-14 15:44:08.504631+0800 Interview03-线程保活[4606:209161] -[ViewController dealloc] 2021-05-14 15:44:08.504806+0800 Interview03-线程保活[4606:209161] -[MJThread dealloc]
可以发现,当前VC和thread都被销毁了。
上面代码还有一个问题,就是我们每次都要先点击停止再返回当前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
特别备注
本系列文章总结自MJ老师在腾讯课堂iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!