runloop深入探究,手动开启和关闭一个runloop
提到iOS中开启一个常驻线程,网上最常见的就是在子线程中开启一个runloop,然后给runloop添加一个port或者timer。但是都只提到了如果让线程不常驻,那么这个线程该如何关闭掉呢?
我尝试了很多办法,这里就不写测试步骤了,具体测试如下:在导航控制器的二级页面创建线程,重写控制器的dealloc方法,然后在dealloc方法中再判断线程的状态。
网上常见的常驻线程方法和问题:
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithBlock:^{
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self performSelector:@selector(testMethod) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)testMethod {
NSLog(@"%@", [NSThread currentThread]);
}
- (void)dealloc {
NSLog(@"Thread isExecuting: %zd", [self.thread isExecuting]);
NSLog(@"Thread isFinished: %zd", [self.thread isFinished]);
NSLog(@"Thread isCancelled: %zd", [self.thread isCancelled]);
NSLog(@"dealloc");
}
推出控制器后log输出如下:
2017-03-30 17:34:38.541 JKRThreadDemo[21891:1853638] 线程内执行方法: <NSThread: 0x600000265680>{number = 3, name = (null)}
2017-03-30 17:34:40.069 JKRThreadDemo[21891:1853638] 线程内执行方法: <NSThread: 0x600000265680>{number = 3, name = (null)}
2017-03-30 17:34:40.261 JKRThreadDemo[21891:1853638] 线程内执行方法: <NSThread: 0x600000265680>{number = 3, name = (null)}
2017-03-30 17:34:40.421 JKRThreadDemo[21891:1853638] 线程内执行方法: <NSThread: 0x600000265680>{number = 3, name = (null)}
2017-03-30 17:34:44.318 JKRThreadDemo[21891:1852681] Thread isExecuting: 1
2017-03-30 17:34:44.318 JKRThreadDemo[21891:1852681] Thread isFinished: 0
2017-03-30 17:34:44.318 JKRThreadDemo[21891:1852681] Thread isCancelled: 0
2017-03-30 17:34:44.318 JKRThreadDemo[21891:1852681] dealloc
问题:推出控制器后线程没有关闭
子线程为什么没有关闭呢?究其原因就是因为子线程中runloop还在跑,这里就需要做一个操作,去关闭一个runloop。
要关闭一个runloop,前提就是要移除它的source,经过我的测试,NSRunloop对象的port和timer移除很麻烦,首先不能和self对象有关联,不然会造成无法移除和线程不关闭并持有当前对象,导致当前对象也不能够关闭的诸多问题。并且NSPort对象在添加在runloop的port中后,引用计数会+3,然后remove后引用计数-2,莫名其妙多出来一个,导致内存也有泄漏的问题。
这里将我测试后可行的两种解决方案贴出来:
NSRunloop下子线程常驻和关闭方案
@property (nonatomic, strong) NSThread *thread;
...
static BOOL stop;
static BOOL doMethod;
- (void)viewDidLoad {
[super viewDidLoad];
stop = NO;
self.thread = [[NSThread alloc] initWithBlock:^{
@autoreleasepool {
NSLog(@"开启了一个子线程:%@", [NSThread currentThread]);
[NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"Time action : %@", [NSThread currentThread]);
// 如果开关关闭就停止runloop
if (stop) {
NSLog(@"移除runloop的source");
[timer invalidate];
} else if (doMethod) {
[self testMethod];
}
}];
[[NSRunLoop currentRunLoop] run];
NSLog(@"Runloop finish");
}
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
// 在自定义的常住线程中处理一个操作
doMethod = YES;
}
- (void)testMethod {
NSLog(@"在自定义的子线程中异步处理了一个耗时操作 : %@", [NSThread currentThread]);
sleep(3.0);
// 处理完操作后关闭常住线程
NSLog(@"处理完耗时操作后关闭常住线程");
stop = YES;
}
- (void)dealloc {
NSLog(@"Thread isExecuting: %zd", [self.thread isExecuting]);
NSLog(@"Thread isFinished: %zd", [self.thread isFinished]);
NSLog(@"Thread isCancelled: %zd", [self.thread isCancelled]);
NSLog(@"dealloc");
}
输出如下:
2017-03-30 17:52:21.017 JKRThreadDemo[22084:1873866] 开启了一个子线程:<NSThread: 0x6000002673c0>{number = 3, name = (null)}
2017-03-30 17:52:22.018 JKRThreadDemo[22084:1873866] Time action : <NSThread: 0x6000002673c0>{number = 3, name = (null)}
2017-03-30 17:52:22.018 JKRThreadDemo[22084:1873866] 在自定义的子线程中异步处理了一个耗时操作 : <NSThread: 0x6000002673c0>{number = 3, name = (null)}
2017-03-30 17:52:25.022 JKRThreadDemo[22084:1873866] 处理完耗时操作后关闭常住线程
2017-03-30 17:52:26.020 JKRThreadDemo[22084:1873866] Time action : <NSThread: 0x6000002673c0>{number = 3, name = (null)}
2017-03-30 17:52:26.020 JKRThreadDemo[22084:1873866] 移除runloop的source
2017-03-30 17:52:26.022 JKRThreadDemo[22084:1873866] Runloop finish
2017-03-30 17:52:29.177 JKRThreadDemo[22084:1873590] Thread isExecuting: 0
2017-03-30 17:52:29.178 JKRThreadDemo[22084:1873590] Thread isFinished: 1
2017-03-30 17:52:29.178 JKRThreadDemo[22084:1873590] Thread isCancelled: 0
2017-03-30 17:52:29.178 JKRThreadDemo[22084:1873590] dealloc
不用NSPort的原因:
1,由于NSPort的引用计数莫名其妙+1的情况,如果要防止内存泄漏还要在切换ARC环境-1
2,经过测试,使用NSRunloop持久化的线程无法使用
[self performSelector:@selector(testMethod) onThread:self.thread withObject:nil waitUntilDone:YES];
方法来在该线程中执行一个方法,如果这样的话,会造成runloop无法关闭的问题,具体原因不清楚。
CFRunLoopRef下子线程常驻和关闭方案
@property (nonatomic, strong) NSThread *thread;
...
static BOOL stop;
static BOOL doingMethod;
- (void)viewDidLoad {
[super viewDidLoad];
stop = NO;
doingMethod = NO;
// 创建一个常驻线程
self.thread = [[NSThread alloc] initWithBlock:^{
CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
// 给runloop添加一个自定义source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
// 给runloop添加一个状态监听者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopBeforeWaiting:
NSLog(@"即将进入睡眠");
// 当runloop进入空闲时,即方法执行完毕后,判断runloop的开关,如果关闭就执行关闭操作
{
if (stop) {
NSLog(@"关闭runloop");
// 移除runloop的source
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);
// 没有source的runloop是可以通过stop方法关闭的
CFRunLoopStop(CFRunLoopGetCurrent());
}
}
break;
case kCFRunLoopExit:
NSLog(@"退出");
break;
default:
break;
}
});
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
CFRunLoopRun();
CFRelease(observer);
NSLog(@"Runloop finish");
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
if (!doingMethod) {
doingMethod = YES;
// 在该线程中异步执行一个方法
[self performSelector:@selector(testMethod) onThread:self.thread withObject:nil waitUntilDone:YES];
}
}
- (void)testMethod {
NSLog(@"在自定义的子线程中异步处理了一个耗时操作 : %@", [NSThread currentThread]);
sleep(3.0);
// 处理完操作后关闭常住线程
NSLog(@"处理完耗时操作后关闭常住线程");
stop = YES;
}
- (void)dealloc {
NSLog(@"Thread isExecuting: %zd", [self.thread isExecuting]);
NSLog(@"Thread isFinished: %zd", [self.thread isFinished]);
NSLog(@"Thread isCancelled: %zd", [self.thread isCancelled]);
NSLog(@"dealloc");
}
输出如下:
2017-03-30 18:04:11.016 JKRThreadDemo[22220:1888064] 即将进入睡眠
2017-03-30 18:04:12.262 JKRThreadDemo[22220:1888064] 在自定义的子线程中异步处理了一个耗时操作 : <NSThread: 0x600000266800>{number = 3, name = (null)}
2017-03-30 18:04:15.264 JKRThreadDemo[22220:1888064] 处理完耗时操作后关闭常住线程
2017-03-30 18:04:15.265 JKRThreadDemo[22220:1888064] 即将进入睡眠
2017-03-30 18:04:15.265 JKRThreadDemo[22220:1888064] 关闭runloop
2017-03-30 18:04:15.265 JKRThreadDemo[22220:1888064] 退出
2017-03-30 18:04:15.266 JKRThreadDemo[22220:1888064] Runloop finish
2017-03-30 18:04:27.744 JKRThreadDemo[22220:1887888] Thread isExecuting: 0
2017-03-30 18:04:27.744 JKRThreadDemo[22220:1887888] Thread isFinished: 1
2017-03-30 18:04:27.744 JKRThreadDemo[22220:1887888] Thread isCancelled: 0
2017-03-30 18:04:27.744 JKRThreadDemo[22220:1887888] dealloc
这种方案的解决思路:由于CFRunloop下不像NSRunloop有一个层未知的封装,所以可以很清晰的执行一套业务逻辑。
1,开启一个子线程
2,为子线程添加一个runloop
3,为runloop添加一个source
4,为runloop添加一个observer
5,外部设置一个runloop开关
当runloop进入空闲状态时,判断开关状态,如果开关为关闭状态,则移除runloop的source,没有了source的runloop是可以通过CFRunLoopStop方法关闭的。runloop关闭后,runloop的run方法后的的log就有了输出。并且在控制器推出的时候,可以看到线程的状态为finished。