如果经常要在子线程中做事情,不使用保活,就会一直创建、销毁子线程,这样很耗性能的,所以经常在子线程做事情最好使用线程保活,比如AFN2.X就使用RunLoop实现了线程保活。
一. 实现线程保活
为了监控线程生命周期我们自定义MJThread继承于NSThread,重写其dealloc方法,实现如下代码:
#import "ViewController.h"
#import "MJThread.h"
@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//self和thread会造成循环引用
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];
//waitUntilDone:YES 等到子线程任务执行完再执行下面NSLog
//NO 不用等到子线程执行完再执行下面NSLog(下面NSLog在主线程,test在子线程,同时执行)
NSLog(@"123");
}
// 子线程需要执行的任务
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
// 没打印dealloc,也没打印----end----
// -[ViewController test] <MJThread: 0x600000a8fec0>{number = 3, name = (null)}
}
// 这个方法的目的:线程保活
- (void)run {
NSLog(@"%s %@", __func__, [NSThread currentThread]);
// 往RunLoop里面添加Source\Timer\Observer,Port相关的是Source1事件
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//添加了一个Source1,但是这个Source1也没啥事,所以线程在这里就休眠了,不会往下走,不会打印----end----
//如果不添加Source\Timer\Observer,RunLoop没有任何事件处理RunLoop就会立马退出,打印----end----
[[NSRunLoop currentRunLoop] run];
NSLog(@"%s ----end----", __func__);
}
@end
上面代码,如果只写 [[NSRunLoop currentRunLoop] run],不添加Port,RunLoop没有任何事件处理,那么RunLoop就会立马退出,会打印----end----,如果添加Port,并且[[NSRunLoop currentRunLoop] run],如下:
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
这样NSRunLoop里面有事件(虽然不用处理什么),就不会退出了。线程在[[NSRunLoop currentRunLoop] run]这一行就休眠了,不会往下执行打印----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;
}
运行后,在当前界面返回,打印:
-[ViewController dealloc]
可以发现ViewController销毁了,但是thread还是没被销毁,为什么呢?
这是因为RunLoop在 [[NSRunLoop currentRunLoop] run]这一行一直阻塞,一直不会打印----end----,这时候任务一直在进行,任务还没有完成线程就不会死,就算在ViewController的dealloc方法里面把thread清空,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;
//如果使用如下方式创建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];
//要使用weakself,不然self强引用thread,thread强引用block,block强引用self,产生循环引用。
while (!weakSelf.isStoped) {
//beforeDat:过期时间,传入distantFuture遥远的未来,就是永远不会过期
[[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]);
}
//点击停止按钮
- (IBAction)stop {
// 在子线程调用CFRunLoopStop
[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清空也不行,因为任务还没结束,线程就不会死。
//self.thread = nil;
}
@end
注意:上面要使用weakself,不然self强引用thread,thread强引用block,block强引用self,产生循环引用(使用weakself之后,就是self强引用thread,thread强引用block,block弱引用self,不会产生循环引用)。
运行代码,进入界面,打印:
<MJThread: 0x60000233af00>{number = 3, name = (null)}----begin----
说明线程开始工作了。
点击空白,打印:
-[ViewController test] <MJThread: 0x60000233af00>{number = 3, name = (null)}
说明RunLoop接收到事件,开始处理事件。
点击stop打印:
-[ViewController stopThread] <MJThread: 0x6000015f2500>{number = 3, name = (null)}
<MJThread: 0x6000015f2500>{number = 3, name = (null)}----end----
可以看出,执行了CFRunLoopStop,并且线程任务完成,打印了----end----。
点击stop之后再退出当前VC,打印:
-[ViewController dealloc]
-[MJThread dealloc]
可以发现,当前VC和thread都被销毁了。
上面代码还有一个问题,就是我们每次都要先点击停止再返回当前VC,这样很麻烦,可能你会说可以把[self stop]方法写在ViewController的dealloc方法里面,试了下,发现报错坏内存访问:
那到底是谁坏了呢?稍微想一下也知道是self控制器坏了。
其实原因就是[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO]的最后一个参数,当传入NO的时候,代表不等子线程(self.thread)里面的东西执行完,主线程的dealloc方法会接着往下走,往下走ViewController的dealloc方法就执行完了,self就不在了。
这时候子线程(self.thread)继续做事,先拿到self对象和stopThread消息,然后在子线程给self对象发送topThread消息(内部就是通过子线程的RunLoop循环),这时候self都不在了,拿不到了,所以在子线程的RunLoop循环里会报错坏内存访问。
现在你应该明白为什么会在RunLoop那行代码报坏内存访问错误了吧!
解决办法也很简单,dealloc方法里面调用[self stop],并且将上面NO改成YES。
运行代码,直接返回当前VC,打印:
-[ViewController dealloc]
-[ViewController stopThread] <MJThread: 0x600000dda7c0>{number = 3, name = (null)}
可以发现控制器被销毁了,CFRunLoopStop也调用了,但是线程还没死,又活了,这就奇怪了。
其实那个RunLoop的确停掉了,但是停掉之后,他会再次来到while循环判断条件:
while (!weakSelf.isStoped) {
//beforeDat:过期时间,传入distantFuture遥远的未来,就是永远不会过期
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
这时候当前控制器已经被销毁,weakSelf指针已经被清空,这时候!nil获取的就是YES,所以会再次进入循环体启动RunLoop,RunLoop又跑起来了,线程又有事情干了,所以线程不会销毁。
解决办法:
while (weakSelf && !weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
再次运行项目,点击暂停,返回当前VC,这时候又崩了:
点击暂停之后RunLoop肯定停掉了,RunLoop停掉后,这时候的线程就不能用了,但是这时候thread还没销毁(还没调用dealloc),因为thread还被self引用着,这时候访问一个不能用的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]);
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (!self.thread) return;
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 子线程需要执行的任务
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
- (IBAction)stop {
if (!self.thread) return;
// 在子线程调用stop(waitUntilDone设置为YES,代表子线程的代码执行完毕后,这个方法才会往下走)
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
// 用于停止子线程的RunLoop
- (void)stopThread
{
// 设置标记为YES
self.stopped = YES;
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
// 清空线程
self.thread = nil;
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self stop];
}
@end
二. 线程保活的封装
上面的代码虽然实现了线程保活并且也没啥bug,但是用起来比较麻烦,下面就封装一个可控制线程生命周期的类。
MJPermenantThread.h文件
#import <Foundation/Foundation.h>
//任务的回调
typedef void (^MJPermenantThreadTask)(void);
@interface MJPermenantThread : NSObject
/**
开启线程
*/
//- (void)run;
/**
在当前子线程执行一个任务
*/
- (void)executeTask:(MJPermenantThreadTask)task;
/**
结束线程
*/
- (void)stop;
@end
MJPermenantThread.m文件
#import "MJPermenantThread.h"
/** MJThread **/
@interface MJThread : NSThread
@end
@implementation MJThread
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
/** MJPermenantThread **/
@interface MJPermenantThread()
@property (strong, nonatomic) MJThread *innerThread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;
@end
@implementation MJPermenantThread
#pragma mark - public methods
- (instancetype)init
{
if (self = [super init]) {
self.stopped = NO;
__weak typeof(self) weakSelf = self;
self.innerThread = [[MJThread alloc] initWithBlock:^{
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}];
//自动开始线程
[self.innerThread start];
}
return self;
}
//- (void)run
//{
// if (!self.innerThread) return;
//
// [self.innerThread start];
//}
- (void)executeTask:(MJPermenantThreadTask)task
{
if (!self.innerThread || !task) return;
[self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}
- (void)stop
{
if (!self.innerThread) return;
[self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}
//当前对象死了,让当前对象里面的线程也死
- (void)dealloc
{
NSLog(@"%s", __func__);
[self stop];
}
#pragma mark - private methods
- (void)__stop
{
self.stopped = YES;
CFRunLoopStop(CFRunLoopGetCurrent());
self.innerThread = nil;
}
- (void)__executeTask:(MJPermenantThreadTask)task
{
task();
}
@end
在ViewController里面执行以下代码:
#import "ViewController.h"
#import "MJPermenantThread.h"
@interface ViewController ()
@property (strong, nonatomic) MJPermenantThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[MJPermenantThread alloc] init];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.thread executeTask:^{
NSLog(@"执行任务 - %@", [NSThread currentThread]);
}];
}
- (IBAction)stop {
[self.thread stop];
}
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
点击跳转,跳转到另外一个界面,在另外一个界面会自动开启子线程,返回界面,发现界面和子线程会自动销毁。
小问题:
保住线程的命为什么要用RunLoop,用强指针不就好了么?
准确来讲,使用RunLoop是为了让线程保持激活状态,虽然用强指针指着它,可以保住线程的命,线程不会调用dealloc,这时候线程还在内存中,但是线程的任务一旦执行完毕,生命周期就结束,无法再使用,已经是个废物了。所以用强指针保住命没什么意义,只能用RunLoop让线程一直有事可做,一直保持激活状态。
三. 线程保活的封装(C语言)
MJPermenantThread.h文件
#import <Foundation/Foundation.h>
typedef void (^MJPermenantThreadTask)(void);
@interface MJPermenantThread : NSObject
/**
开启线程
*/
//- (void)run;
/**
在当前子线程执行一个任务
*/
- (void)executeTask:(MJPermenantThreadTask)task;
/**
结束线程
*/
- (void)stop;
@end
MJPermenantThread.m文件
#import "MJPermenantThread.h"
/** MJThread **/
@interface MJThread : NSThread
@end
@implementation MJThread
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
/** MJPermenantThread **/
@interface MJPermenantThread()
@property (strong, nonatomic) MJThread *innerThread;
@end
@implementation MJPermenantThread
#pragma mark - public methods
- (instancetype)init
{
if (self = [super init]) {
self.innerThread = [[MJThread alloc] initWithBlock:^{
NSLog(@"begin----");
// 创建上下文(要初始化一下结构体,否则结构体里面有可能是垃圾数据)
CFRunLoopSourceContext context = {0};
// 创建source
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
// 往Runloop中添加source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
// 销毁source
CFRelease(source);
// 启动
//参数:模式,过时时间(1.0e10一个很大的值),是否执行完source后就会退出当前loop
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
//如果使用的是C语言的方式就可以通过最后一个参数让执行完source之后不退出当前Loop,所以就可以不用stopped属性了
// while (weakSelf && !weakSelf.isStopped) {
// // 第3个参数:returnAfterSourceHandled,设置为true,代表执行完source后就会退出当前loop
// CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
// }
NSLog(@"end----");
}];
[self.innerThread start];
}
return self;
}
//- (void)run
//{
// if (!self.innerThread) return;
//
// [self.innerThread start];
//}
- (void)executeTask:(MJPermenantThreadTask)task
{
if (!self.innerThread || !task) return;
[self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}
- (void)stop
{
if (!self.innerThread) return;
[self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self stop];
}
#pragma mark - private methods
- (void)__stop
{
CFRunLoopStop(CFRunLoopGetCurrent());
self.innerThread = nil;
}
- (void)__executeTask:(MJPermenantThreadTask)task
{
task();
}
@end
C语言方式和OC方式达到的效果都是一样的,但是C语言方式控制的更精准,可以控制执行完source后不退出当前loop,这样就不用写while循环了。
Demo地址:线程保活