iOS技术专题iOS开发技术分享iOS Developer

runloop深入探究,手动开启和关闭一个runloop

2017-03-30  本文已影响844人  喵子G

提到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。

Demo地址:https://github.com/Joker-388/JKRThreadCloseDemo

获取授权

上一篇下一篇

猜你喜欢

热点阅读