iOS线程保活

一.什么是线程保活

如图1所以,任务执行完成后,线程会退出。线程的创建和销毁比较耗性能,如果需要在一条线程中频繁的执行任务,就需要保证线程在执行完任务后不退出。在ios中使用RunLoop 机制来保证线程能在有任务时执行任务,没有任务时进入休眠状态。
iOS中的主线程中RunLoop是主动开启的,所以ios的主线程不会退出,子线程的RunLoop不存在,需要手动添加。所以如果在子线程没有添加RunLoop,在执行完任务后,线程就退出,无法再次执行任务。如果需要在子线程中多次执行任务,就需要在子线程添加RunLoop,并且启动RunLoop。

线程生命周期

1.png

二.子线程保活实现

自定义TestThread,重写dealloc 方法,用于监控多线程的生命周期

//自定义 Thread,重写 dealloc方法
@interface TestThread : NSThread

@end

@implementation TestThread

- (void)dealloc{
    NSLog(@"TestThread 释放");
}

@end

在控制器中创建子线程,并在子线程中执行任务

@interface TestController ()
@property (nonatomic, strong) TestThread *thread;
@end

@implementation TestController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor blueColor];
    self.title = @"线程保活控制器";
    
    self.thread = [[TestThread alloc] initWithTarget:self selector:@selector(threadRun) object:nil];
    self.thread.name = @"常驻线程";
    [self.thread start];
    
    
    UIButton *btn = [[UIButton alloc] init];
    [self.view addSubview:btn];
    btn.frame = CGRectMake(200, 200, 50, 50);
    btn.backgroundColor = [UIColor redColor];
    [btn addTarget:self action:@selector(clickBtn) forControlEvents:UIControlEventTouchUpInside];
}

- (void)threadRun{
    
    NSLog(@"子线程NSRunLoop 开启");
    
    //开启子线程的NSRunLoop,如果RunLoop为空,会创建一个RunLoop
    // 如果RunLoop Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出,需要添加添加port
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    // 启动NSRunLoop
    [[NSRunLoop currentRunLoop] run];
    
}

- (void)clickBtn{
    NSLog(@"点击了按钮");
    [self performSelector:@selector(excuteTask) onThread:self.thread withObject:nil waitUntilDone:NO];
}


- (void)excuteTask{
    
    NSLog(@"任务在子线程中执行:%@",[NSThread currentThread]);
}


- (void)dealloc{
    NSLog(@"TestController 释放");
}

当进入 TestController试,就会打印如下日志

Runloop保活[4059:89028] 子线程NSRunLoop 开启

说明self.thread子线程的 RunLoop开启成功。多次点击按钮,在子线程中执行任务,日志如下:

点击了按钮
任务在子线程中执行:{number = 6, name = 常驻线程}
点击了按钮
任务在子线程中执行:{number = 6, name = 常驻线程}
点击了按钮
任务在子线程中执行:{number = 6, name = 常驻线程}

通过上面的日志看到, self.thread在执行完任务后没有退出,而是等待下次任务来临,继续执行任务。

问题: 当我们点击返回按钮时,TestController没有销毁,thread也没有销毁。出现了内存泄露。

子线程的创建方式

- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(nullable id)argument
2.png

在苹果文档中提到,用这种方式创建子线程,target 和 argument 都会被线程持有,直到线程退出。
而子线程开启了RunLoop,是无限循环,不会退出。线程不会退出,所以 target(这里是TestController)也不会销毁。TestController不销毁,self.thread也不会销毁。所以就造成了内存泄露。

线程退出方案:
  • 调用cancel方法
- (void)stopThread{
    NSLog(@"停止线程");
    [self.thread cancel];
}
3.png

可以看到cancel只是一个标志位,并不是退出线程.所以无法停掉子线程

  • 调用exit方法
- (void)stopThread{
    NSLog(@"停止线程");
    [NSThread exit];
}
4.png

文档中说了应该避免调用这个方法,调用此方法,子线程没有机会去清空资源。

  • 停掉子线程的RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());

发现子线程还是可以执行任务,说明RunLoop没有被成功停掉。
我们开启子线程RunLoop的方法如下:

    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];

run 方法的官方文档如下

Discussion

If no input sources or timers are attached to the run loop, this method exits immediately; otherwise, 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.
Manually removing all known input sources and timers from the run loop is not a guarantee that the run loop will exit. macOS can install and remove additional input sources as needed to process requests targeted at the receiver’s thread. Those sources could therefore prevent the run loop from exiting.
If you want the run loop to terminate, you shouldn't use this method. Instead, use one of the other run methods and also check other arbitrary conditions of your own, in a loop. A simple example would be:

BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
  • 如果没有sources或者timers 添加到run loop中,会立即退出,否则会无限loop。

  • 如果要停止loop,不应该使用run方法启动RunLoop,使用[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]代替,使用标志位来控制是否需要开启RunLoop。

使用如下方案,可以停止RunLoop. 线程和控制器也会释放

- (void)threadRun{

    NSLog(@"子线程NSRunLoop 开启");
    
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];

    while (!self.isStoped)

    {
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }

    NSLog(@"子线程--end");

}

- (void)stopClick{
    NSLog(@"点击了停止");
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
    NSLog(@"点击了停止---end");
}

- (void)stopThread{
    NSLog(@"停止线程");
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    // 清空线程
   self.thread = nil;
}

在停止子线程RunLoop中如果有耗时操作,会有crash
- (void)stopClick{

    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];

}
// 用于停止子线程的RunLoop
- (void)stopThread
{
    // 设置标记为YES
    self.stopped = YES;
    
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    
    //如果在访问self之前,有耗时操作(此处NSlog) self.thread会出现坏内存访问
    NSLog(@"stopThread:%@",[NSThread currentThread]);

    // 清空线程
    /*
      1.点击返回按钮时。会执行控制器的dealloc方法
      2.在dealloc方法中会调用stopClick
      3. 在stopClick方法中,waitUntilDone是NO,异步执行 stopThread方法,
        此时如果stopThread比较耗时,控制器self已经销毁,如果再在stopThread中访问self,会出现坏内存访问
     
     用同步执行能保证在控制器释放之前,清空线程
     [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES]
      
     */
    self.thread = nil;
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    
    [self stopClick];
}

最终方案:

- (void)stopClick{

   //  [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
    // 在子线程调用stop(waitUntilDone设置为YES,代表子线程的代码执行完毕后,这个方法才会往下走)
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];

}
Block方式创建线程
    self.stopped = NO;
    self.thread = [[TestThread 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];
c语言方式创建runloop

使用这种方式,不需要stop标记位

 
          self.thread = [[TestThread alloc] initWithBlock:^{
            
            CFRunLoopSourceContext context = {0};
            
            // 创建source
            CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            
            // 往Runloop中添加source
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            
            // 销毁source
            CFRelease(source);
            //  returnAfterSourceHandled 设置为true,代表执行完source后就会退出当前loop
            // 启动
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);

        }];
        
        [self.thread start];

参考资料

https://mp.weixin.qq.com/s/2sko43kjNyavod0r52y68Q
https://zhuanlan.zhihu.com/p/142549302

你可能感兴趣的:(iOS线程保活)