如果經常要在子線程中做事情,不使用保活,就會一直創建、銷毀子線程,這樣很耗性能的,所以經常在子線程做事情最好使用線程保活,比如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地址:線程保活