基于Runloop的线程保活、销毁

525 阅读4分钟

Runloop的启动和退出方式

三种启动RunLoop的方式:

- (void)run;  

- (void)runUntilDate:(NSDate *)limitDate;

- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

这三种方式无论通过哪一种方式启动runloop,如果没有一个输入源或者timer附加于runloop上,runloop就会立刻退出。

(1) 第一种方式,runloop会一直运行下去,在此期间会处理来自输入源的数据,并且会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法;

(2) 第二种方式,可以设置超时时间,在超时时间到达之前,runloop会一直运行,在此期间runloop会处理来自输入源的数据,并且也会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法;

(3) 第三种方式,runloop会运行一次,超时时间到达或者第一个input source被处理,则runloop就会退出。

前两种启动方式会重复调用runMode:beforeDate:方法。

退出RunLoop的方式

1, 第一种启动方式的退出方法

文档说,如果想退出runloop,不应该使用第一种启动方式来启动runloop。

如果runloop没有input sources或者附加的timer,runloop就会退出。 虽然这样可以将runloop退出,但是苹果并不建议我们这么做,因为系统内部有可能会在当前线程的runloop中添加一些输入源,所以通过手动移除input source或者timer这种方式,并不能保证runloop一定会退出。

2,第二种启动方式runUntilDate:

可以通过设置超时时间来退出runloop。

3, 第三种启动方式runMode:beforeDate:

通过这种方式启动,runloop会运行一次,当超时时间到达或者第一个输入源被处理,runloop就会退出。

------ 如果我们想控制runloop的退出时机,而不是在处理完一个输入源事件之后就退出,那么就要重复调用runMode:beforeDate:,

具体可以参考苹果文档给出的方案,如下:

**

 NSRunLoop *myLoop  = [NSRunLoop currentRunLoop];
 myPort = (NSMachPort *)[NSMachPort port];
 [myLoop addPort:_port forMode:NSDefaultRunLoopMode];

BOOL isLoopRunning = YES; // global

while (isLoopRunning && [myLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);

**

//关闭runloop的地方
- (void)quitLoop
 {
    isLoopRunning = NO;
    CFRunLoopStop(CFRunLoopGetCurrent());
}

4,总结:

  • 如果不想退出runloop可以使用第一种方式启动runloop;
  • 使用第二种方式启动runloop,可以通过设置超时时间来退出;
  • 使用第三种方式启动runloop,可以通过设置超时时间或者使用CFRunLoopStop方法来退出。

AF的经典案列

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}
    
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

在iOS开发过程中,有时一些花费时间比较长的操作阻塞主线程,导致界面卡顿,那么我们就会创建一个子线程,然后把这些花费时间比较长的操作放在子线程中来处理。可是当子线程中的任务执行完毕后,子线程就会被销毁掉。

首先我们要明确几个概念:

  • 线程中的任务执行完毕后,线程就会被销毁掉。

  • 获取RunLoop只能使用 [NSRunLoop currentRunLoop] 或 [NSRunLoop mainRunLoop];

  • 即使RunLoop开始运行,如果RunLoop 中的 modes 为空,那么RunLoop会直接退出。

  • 自己创建的Thread中的任务是在kCFRunLoopDefaultMode这个mode中执行的。

  • 线程添加了runloop,并运行起来,实际上是添加了一个 do-while 循环,这样这个线程的程序一直卡在这个 do-while循环上,这样相当于线程的任务一直没有执行完,所以线程一直不会销毁。

  • 一旦我们添加了一个runloop,并run了,我们如果要销毁这个线程,就必须停止runloop,至于这个停止的方式,我们接下去往下看。

[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
  • 这行代码的目的是添加一个端口监听这个端口的事件,
[runLoop run];
  • runloop开始跑起来,但是要注意,这种runloop,只有一种方式能终止
[NSRunLoop currentRunLoop]removePort:<(nonnull NSPort *)> forMode:<nonnull NSRunLoopMode)>

只有从runloop中移除我们之前添加的端口,这样runloop没有任何事件,所以直接退出。

再次回到 AF2.x 的这行源码上,因为他用的是run,而且并没有记录下自己添加的NSMachPort,所以显然,他就没打算退出这个runloop,这是一个常驻线程