iOS-RunLoop详解(三):使用RunLoop线程保活方案

iOS-RunLoop详解(三):使用RunLoop线程保活方案

如果经常要在子线程中做事情,不使用保活,就会一直创建、销毁子线程,这样很耗性能,所以经常在子线程做事情最好使用线程保活。

实现线程保活

创建线程类,表示需要经常执行的任务

*********************** MJThread.h **************************
@interface MJThread : NSThread

@end

#import "MJThread.h"

@implementation MJThread

*********************** MJThread.m **************************
- (void)dealloc
{
    NSLog(@"%s", __func__);
}

@end

*********************** ViewController.m **************************
    
@implementation ViewController
    
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // NSThread 频繁创建线程
        MJThread *thread = [[MJThread alloc]initWithTarget:self selector:@selector(test) object:nil];
        [thread start];

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

// 子线程需要执行的任务
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
@end

RUN>

我们每次点击,都会去执行任务:NSThread 频繁创建线程

2021-05-14 14:55:19.054958+0800 Interview03-线程保活[4068:166884] -[ViewController test] {number = 7, name = (null)}
2021-05-14 14:55:19.056474+0800 Interview03-线程保活[4068:166884] -[MJThread dealloc]
2021-05-14 14:55:21.627640+0800 Interview03-线程保活[4068:166923] -[ViewController test] {number = 8, name = (null)}
2021-05-14 14:55:21.627815+0800 Interview03-线程保活[4068:166923] -[MJThread dealloc]

可以看到,任务一执行完,线程就释放了.并且这样频繁的创建线程,很消耗资源

我们可以用RunLoop来延长线程的生命周期,不让线程挂掉

// 子线程需要执行的任务
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    
    [[NSRunLoop currentRunLoop] run];
    
    NSLog(@"%s ----end----", __func__);
}

RUN>

2021-05-14 15:01:54.818875+0800 Interview03-线程保活[4128:171606] -[ViewController test] {number = 7, name = (null)}
2021-05-14 15:01:54.819344+0800 Interview03-线程保活[4128:171606] -[ViewController test] ----end----
2021-05-14 15:01:54.821043+0800 Interview03-线程保活[4128:171606] -[MJThread dealloc]

运行一下,发现线程执行完任务还是会结束线程([MJThread dealloc])

这是因为,我们虽然获取了当前的RunLoop,并且调用run方法让RunLoop跑起来了,而run方法底层调用的是- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;方法,这个方法会把RunLoop添加到NSDefaultRunLoopMode模式下.Model中如果没有任何Source0 , Source1 , Timer , Observer,RunLoop会立马退出. 所以我们需要往RunLoop中添加任务,任何任务都可以.

我们需要往RunLoop中添加任务,任何任务都可以.

// 子线程需要执行的任务
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    NSLog(@"");
    // 往RunLoop里面添加Source\Timer\Observer
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    
    NSLog(@"%s ----end----", __func__);
}

RUN>

2021-05-14 15:05:54.882701+0800 Interview03-线程保活[4172:174678] -[ViewController test] {number = 7, name = (null)}
2021-05-14 15:05:54.883756+0800 Interview03-线程保活[4172:174678] 

在运行线程就不会执行完任务就挂掉了,而是执行完任务就休眠:

每一次点击屏幕,都是创建了[[MJThread alloc]initWithTarget:self selector:@selector(test) object:nil];经过[[NSRunLoop currentRunLoop] addPort[[NSRunLoop currentRunLoop] run]; 线程进入休眠了,但是我们现在没办法唤醒线程和执行线程任务,程序继续修改。

上面的代码thread是一个局部变量,每次执行任务都会重新创建,所以我们把线程设置成成员属性。

*********************** ViewController.m **************************
@interface ViewController ()
@property (strong, nonatomic) MJThread *thread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.thread = [[MJThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

//触碰屏幕事件
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 子线程需要执行的任务
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    NSLog(@"");
    
}

// 这个方法的目的:线程保活
- (void)run {
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    NSLog(@"---------- start -----------");
    // 往RunLoop里面添加Source\Timer\Observer
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    
    NSLog(@"%s ----end----", __func__);
}
@end

RUN>

2021-05-14 15:14:16.185593+0800 Interview03-线程保活[4221:180238] -[ViewController test] {number = 7, name = (null)}
2021-05-14 15:14:16.185768+0800 Interview03-线程保活[4221:180238] 
2021-05-14 15:14:17.821116+0800 Interview03-线程保活[4221:180238] -[ViewController test] {number = 7, name = (null)}
2021-05-14 15:14:17.821240+0800 Interview03-线程保活[4221:180238] 
2021-05-14 15:14:20.824925+0800 Interview03-线程保活[4221:180238] -[ViewController test] {number = 7, name = (null)}
2021-05-14 15:14:20.825090+0800 Interview03-线程保活[4221:180238] 

上面的代码有两个问题:

  1. self和thread会造成循环引用,都不会释放
  2. 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; 
}

运行后,在当前界面返回

RUN>

2021-05-14 15:21:44.228798+0800 Interview03-线程保活[4480:196517] {number = 9, name = (null)}----begin----
2021-05-14 15:21:47.517069+0800 Interview03-线程保活[4480:195322] -[ViewController dealloc]

可以发现ViewController销毁了,但是thread还是没被销毁

很奇怪,控制器都释放了,按理说控制器内部的所有东西都应该释放了呀,我们在ViewControllerdealloc方法中把thread置为nil:

2021-05-14 15:25:09.585499+0800 Interview03-线程保活[4520:199781] {number = 8, name = (null)}----begin----
2021-05-14 15:25:13.501110+0800 Interview03-线程保活[4520:199781] -[ViewController test] {number = 8, name = (null)}
2021-05-14 15:25:13.501288+0800 Interview03-线程保活[4520:199781] 
2021-05-14 15:25:16.475845+0800 Interview03-线程保活[4520:199617] -[ViewController dealloc]

thread强制置为nil,thread还是没有释放.

 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]);
    }];

这是因为RunLoop在 [[NSRunLoop currentRunLoop] run]这一行一直阻塞,一直不会打印----end----,这时候任务一直在进行,任务还没有完成线程就不会死,就算在ViewController的dealloc方法里面把thread清空,thread也不会死。

如果我们想要精准的控制线程的生命周期,比如说控制器销毁的时候,线程也销毁,那应该怎么做呢?我们可以像下面这样手动停止RunLoop:

- (IBAction)stop {
    // 在子线程调用stop
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 用于停止子线程的RunLoop
- (void)stopThread
{
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

run>

2021-05-14 15:34:46.499408+0800 Interview03-线程保活[4566:205814] {number = 9, name = (null)}----begin----
2021-05-14 15:34:55.230839+0800 Interview03-线程保活[4566:205814] -[ViewController test] {number = 9, name = (null)}
2021-05-14 15:34:55.230956+0800 Interview03-线程保活[4566:205814] 
2021-05-14 15:34:57.846728+0800 Interview03-线程保活[4566:205814] -[ViewController stopThread] {number = 9, name = (null)}
2021-05-14 15:35:14.203913+0800 Interview03-线程保活[4566:205687] -[ViewController dealloc]

stopRunLoop 虽然执行了,并且ViewController 也已经销毁了,但是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;
    self.thread = [[MJThread alloc] initWithBlock:^{
        NSLog(@"%@----begin----", [NSThread currentThread]);

        // 往RunLoop里面添加Source\Timer\Observer
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        while (!weakSelf.isStoped) {
            [[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 *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 子线程需要执行的任务
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    NSLog(@"");
}

- (IBAction)stop {
    // 在子线程调用stop
    [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清空,thread也不会销毁,因为任务还没结束,线程就不会死。
    self.thread = nil;
    
//    [self stop];
}

@end

RUN>

2021-05-14 15:39:47.441947+0800 Interview03-线程保活[4606:209299] {number = 8, name = (null)}----begin----
2021-05-14 15:39:48.914864+0800 Interview03-线程保活[4606:209299] -[ViewController test] {number = 8, name = (null)}
2021-05-14 15:39:48.915021+0800 Interview03-线程保活[4606:209299] 
2021-05-14 15:39:50.900749+0800 Interview03-线程保活[4606:209299] -[ViewController stopThread] {number = 8, name = (null)}
2021-05-14 15:39:50.901004+0800 Interview03-线程保活[4606:209299] {number = 8, name = (null)}----end----
2021-05-14 15:39:59.322333+0800 Interview03-线程保活[4606:209161] -[ViewController dealloc]
2021-05-14 15:39:59.322455+0800 Interview03-线程保活[4606:209161] -[MJThread dealloc]

上面要使用weakself,不然self强引用thread,thread强引用block,block强引用self,产生循环引用(使用weakself之后,就是self强引用thread,thread强引用block,block弱引用self,不会产生循环引用)。

运行代码,进入界面,打印:

2021-05-14 15:39:47.441947+0800 Interview03-线程保活[4606:209299] {number = 8, name = (null)}----begin----

说明线程开始工作了。

点击空白,打印:

2021-05-14 15:42:22.608171+0800 Interview03-线程保活[4606:211084] -[ViewController test] {number = 9, name = (null)}
2021-05-14 15:42:22.610916+0800 Interview03-线程保活[4606:211084] 

说明RunLoop接收到事件,开始处理事件。

点击stop打印:

2021-05-14 15:42:35.860716+0800 Interview03-线程保活[4606:211084] -[ViewController stopThread] {number = 9, name = (null)}
2021-05-14 15:42:35.861011+0800 Interview03-线程保活[4606:211084] {number = 9, name = (null)}----end----

可以看出,执行了CFRunLoopStop,并且线程任务完成,打印了----end----。

点击stop之后再退出当前VC,打印:

2021-05-14 15:44:08.504631+0800 Interview03-线程保活[4606:209161] -[ViewController dealloc]
2021-05-14 15:44:08.504806+0800 Interview03-线程保活[4606:209161] -[MJThread dealloc]

可以发现,当前VC和thread都被销毁了。

上面代码还有一个问题,就是我们每次都要先点击停止再返回当前VC,这样很麻烦,可能你会说可以把[self stop]方法写在ViewController的dealloc方法里面,试了下,发现报错坏内存访问:

image-20210514155504725

这是为什么呢?这就是我们上面讲的waitUntilDone造成的.我们在[self performSelector:@selector(stopRunLoop) onThread:self.thread withObject:nil waitUntilDone:NO];中把waitUntilDone设置为NO,就表示在子线程执行的stopRunLoop函数和在主线程执行的- (IBAction)stop函数是同时执行的.一旦- (IBAction)stop函数先执行完,那么ViewControllerdealloc函数也会立马执行完毕,ViewController就会释放.这时候再去执行stopRunLoop就会报坏内存访问,因为ViewController已经释放了.为什么会崩溃到[[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];这一行呢?

现在你应该明白为什么会在RunLoop那行代码报坏内存访问错误了吧!

解决办法也很简单,dealloc方法里面调用[self stop],并且将上面NO改成YES。

- (IBAction)stop {
    // 在子线程调用stop
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)dealloc
{
    NSLog(@"%s", __func__);
    
   [self stop];
}

运行代码,直接返回当前VC,打印:

2021-05-14 15:59:38.055083+0800 Interview03-线程保活[4787:225673] -[ViewController dealloc]
2021-05-14 15:59:38.055307+0800 Interview03-线程保活[4787:225794] -[ViewController stopThread] {number = 8, name = (null)}

我们点击返回退出控制器后ViewController释放了,但是没有看到线程释放

其实那个RunLoop的确停掉了,但是停掉之后,他会再次来到while循环判断条件:

我们在while循环中打一个断点:

image-20210514160136836

这时候当前控制器已经被销毁,weakSelf指针已经被清空,这时候!nil获取的就是YES,所以会再次进入循环体启动RunLoop,RunLoop又跑起来了,线程又有事情干了,所以线程不会销毁。

解决办法:

while (weakSelf && !weakSelf.isStoped) {
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}

再次运行项目,返回当前VC

2021-05-14 16:03:29.471773+0800 Interview03-线程保活[4863:229852] {number = 9, name = (null)}----end----
2021-05-14 16:03:29.472196+0800 Interview03-线程保活[4863:229852] -[MJThread dealloc]

再次运行项目,点击暂停,返回当前VC,这时候又崩了

image-20210514160513785

点击暂停之后RunLoop肯定停掉了,RunLoop停掉后,这时候的线程就不能用了,runloop停止掉后它的任务就执行完了,线程的生命周期已经结束了,这时候它已经不能再执行任务了.但是这时候thread还没销毁(还没调用dealloc),因为thread还被self引用着,我们点击返回按钮,又让子线程去执行stopRunLoop任务就会报错,这时候访问一个不能用的thread就会报坏内存访问错误。

解决办法也很简单,暂停RunLoop后把thread指针置为nil,并且如果发现子线程为nil就不在子线程执行任务了。

image-20210514160945789

最后的完整代码

#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]);

        // 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 *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 子线程需要执行的任务
- (void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    NSLog(@"");
}

- (IBAction)stop {
    // 在子线程调用stop
    if (!self.thread) return;
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

// 用于停止子线程的RunLoop
- (void)stopThread
{
    // 设置标记为NO
    self.stopped = YES;
    
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.thread = nil;
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

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

@end
特别备注

本系列文章总结自MJ老师在腾讯课堂iOS底层原理班(下)/OC对象/关联对象/多线程/内存管理/性能优化,相关图片素材均取自课程中的课件。如有侵权,请联系我删除,谢谢!

你可能感兴趣的:(iOS-RunLoop详解(三):使用RunLoop线程保活方案)