美文网首页iOS基础篇RunLoop
iOS-RunLoop2-线程保活

iOS-RunLoop2-线程保活

作者: Imkata | 来源:发表于2019-12-13 09:01 被阅读0次

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

    一. 实现线程保活

    为了监控线程生命周期我们自定义MJThread继承于NSThread,重写其dealloc方法,实现如下代码:

    #import "ViewController.h"
    #import "MJThread.h"
    
    @interface ViewController ()
    @property (strong, nonatomic) MJThread *thread;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        //self和thread会造成循环引用
        self.thread = [[MJThread alloc] initWithTarget:self selector:@selector(run) object:nil];
        [self.thread start];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
        //waitUntilDone:YES 等到子线程任务执行完再执行下面NSLog
        //NO 不用等到子线程执行完再执行下面NSLog(下面NSLog在主线程,test在子线程,同时执行)
        NSLog(@"123");
    }
    
    // 子线程需要执行的任务
    - (void)test
    {
        NSLog(@"%s %@", __func__, [NSThread currentThread]);
        // 没打印dealloc,也没打印----end----
        // -[ViewController test] <MJThread: 0x600000a8fec0>{number = 3, name = (null)}
    }
    
    // 这个方法的目的:线程保活
    - (void)run {
        NSLog(@"%s %@", __func__, [NSThread currentThread]);
        
        // 往RunLoop里面添加Source\Timer\Observer,Port相关的是Source1事件
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        
        //添加了一个Source1,但是这个Source1也没啥事,所以线程在这里就休眠了,不会往下走,不会打印----end----
        //如果不添加Source\Timer\Observer,RunLoop没有任何事件处理RunLoop就会立马退出,打印----end----
        [[NSRunLoop currentRunLoop] run];
        
        NSLog(@"%s ----end----", __func__);
    }
    
    @end
    

    上面代码,如果只写 [[NSRunLoop currentRunLoop] run],不添加Port,RunLoop没有任何事件处理,那么RunLoop就会立马退出,会打印----end----,如果添加Port,并且[[NSRunLoop currentRunLoop] run],如下:

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

    这样NSRunLoop里面有事件(虽然不用处理什么),就不会退出了。线程在[[NSRunLoop currentRunLoop] run]这一行就休眠了,不会往下执行打印----end----了,如果有其他事情,线程会再次被唤醒,处理事情。

    上面的代码有两个问题:

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

    运行后,在当前界面返回,打印:

    -[ViewController dealloc]
    

    可以发现ViewController销毁了,但是thread还是没被销毁,为什么呢?

    这是因为RunLoop在 [[NSRunLoop currentRunLoop] run]这一行一直阻塞,一直不会打印----end----,这时候任务一直在进行,任务还没有完成线程就不会死,就算在ViewController的dealloc方法里面把thread清空,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;
        
        //如果使用如下方式创建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];
            //要使用weakself,不然self强引用thread,thread强引用block,block强引用self,产生循环引用。
            while (!weakSelf.isStoped) {
                //beforeDat:过期时间,传入distantFuture遥远的未来,就是永远不会过期
                [[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<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
    }
    
    // 子线程需要执行的任务
    - (void)test
    {
        NSLog(@"%s %@", __func__, [NSThread currentThread]);
    }
    
    //点击停止按钮
    - (IBAction)stop {
        // 在子线程调用CFRunLoopStop
        [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清空也不行,因为任务还没结束,线程就不会死。
        //self.thread = nil;
    }
    @end
    

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

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

    <MJThread: 0x60000233af00>{number = 3, name = (null)}----begin----
    

    说明线程开始工作了。

    点击空白,打印:

    -[ViewController test] <MJThread: 0x60000233af00>{number = 3, name = (null)}
    

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

    点击stop打印:

    -[ViewController stopThread] <MJThread: 0x6000015f2500>{number = 3, name = (null)}
    <MJThread: 0x6000015f2500>{number = 3, name = (null)}----end----
    

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

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

    -[ViewController dealloc]
    -[MJThread dealloc]
    

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

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

    坏内存访问

    那到底是谁坏了呢?稍微想一下也知道是self控制器坏了。
    其实原因就是[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:NO]的最后一个参数,当传入NO的时候,代表不等子线程(self.thread)里面的东西执行完,主线程的dealloc方法会接着往下走,往下走ViewController的dealloc方法就执行完了,self就不在了。
    这时候子线程(self.thread)继续做事,先拿到self对象和stopThread消息,然后在子线程给self对象发送topThread消息(内部就是通过子线程的RunLoop循环),这时候self都不在了,拿不到了,所以在子线程的RunLoop循环里会报错坏内存访问。

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

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

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

    -[ViewController dealloc]
    -[ViewController stopThread] <MJThread: 0x600000dda7c0>{number = 3, name = (null)}
    

    可以发现控制器被销毁了,CFRunLoopStop也调用了,但是线程还没死,又活了,这就奇怪了。

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

    while (!weakSelf.isStoped) {
        //beforeDat:过期时间,传入distantFuture遥远的未来,就是永远不会过期
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    }
    

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

    解决办法:

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

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

    崩了

    点击暂停之后RunLoop肯定停掉了,RunLoop停掉后,这时候的线程就不能用了,但是这时候thread还没销毁(还没调用dealloc),因为thread还被self引用着,这时候访问一个不能用的thread就会报坏内存访问错误。

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

    代码如下:

    #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]);
        }];
        [self.thread start];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        if (!self.thread) return;
        [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
    }
    
    // 子线程需要执行的任务
    - (void)test
    {
        NSLog(@"%s %@", __func__, [NSThread currentThread]);
    }
    
    - (IBAction)stop {
        if (!self.thread) return;
        
        // 在子线程调用stop(waitUntilDone设置为YES,代表子线程的代码执行完毕后,这个方法才会往下走)
        [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
    }
    
    // 用于停止子线程的RunLoop
    - (void)stopThread
    {
        // 设置标记为YES
        self.stopped = YES;
        
        // 停止RunLoop
        CFRunLoopStop(CFRunLoopGetCurrent());
        NSLog(@"%s %@", __func__, [NSThread currentThread]);
        
        // 清空线程
        self.thread = nil;
    }
    
    - (void)dealloc
    {
        NSLog(@"%s", __func__);
        
        [self stop];
    }
    @end
    

    二. 线程保活的封装

    上面的代码虽然实现了线程保活并且也没啥bug,但是用起来比较麻烦,下面就封装一个可控制线程生命周期的类。

    MJPermenantThread.h文件

    #import <Foundation/Foundation.h>
    
    //任务的回调
    typedef void (^MJPermenantThreadTask)(void);
    
    @interface MJPermenantThread : NSObject
    
    /**
     开启线程
     */
    //- (void)run;
    
    /**
     在当前子线程执行一个任务
     */
    - (void)executeTask:(MJPermenantThreadTask)task;
    
    /**
     结束线程
     */
    - (void)stop;
    
    @end
    

    MJPermenantThread.m文件

    #import "MJPermenantThread.h"
    
    /** MJThread **/
    @interface MJThread : NSThread
    @end
    @implementation MJThread
    - (void)dealloc
    {
        NSLog(@"%s", __func__);
    }
    @end
    
    /** MJPermenantThread **/
    @interface MJPermenantThread()
    @property (strong, nonatomic) MJThread *innerThread;
    @property (assign, nonatomic, getter=isStopped) BOOL stopped;
    @end
    
    @implementation MJPermenantThread
    #pragma mark - public methods
    - (instancetype)init
    {
        if (self = [super init]) {
            self.stopped = NO;
            
            __weak typeof(self) weakSelf = self;
            
            self.innerThread = [[MJThread alloc] initWithBlock:^{
                [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
                
                while (weakSelf && !weakSelf.isStopped) {
                    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
                }
            }];
            
            //自动开始线程
            [self.innerThread start];
        }
        return self;
    }
    
    //- (void)run
    //{
    //    if (!self.innerThread) return;
    //
    //    [self.innerThread start];
    //}
    
    - (void)executeTask:(MJPermenantThreadTask)task
    {
        if (!self.innerThread || !task) return;
        
        [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
    }
    
    - (void)stop
    {
        if (!self.innerThread) return;
        
        [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
    }
    
    //当前对象死了,让当前对象里面的线程也死
    - (void)dealloc
    {
        NSLog(@"%s", __func__);
        
        [self stop];
    }
    
    #pragma mark - private methods
    - (void)__stop
    {
        self.stopped = YES;
        CFRunLoopStop(CFRunLoopGetCurrent());
        self.innerThread = nil;
    }
    
    - (void)__executeTask:(MJPermenantThreadTask)task
    {
        task();
    }
    
    @end
    

    在ViewController里面执行以下代码:

    #import "ViewController.h"
    #import "MJPermenantThread.h"
    
    @interface ViewController ()
    @property (strong, nonatomic) MJPermenantThread *thread;
    @end
    
    @implementation ViewController
    
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        self.thread = [[MJPermenantThread alloc] init];
    }
    
    - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
    {
        [self.thread executeTask:^{
            NSLog(@"执行任务 - %@", [NSThread currentThread]);
        }];
    }
    
    - (IBAction)stop {
        [self.thread stop];
    }
    
    - (void)dealloc
    {
        NSLog(@"%s", __func__);
    }
    
    @end
    

    点击跳转,跳转到另外一个界面,在另外一个界面会自动开启子线程,返回界面,发现界面和子线程会自动销毁。

    小问题:

    保住线程的命为什么要用RunLoop,用强指针不就好了么?

    准确来讲,使用RunLoop是为了让线程保持激活状态,虽然用强指针指着它,可以保住线程的命,线程不会调用dealloc,这时候线程还在内存中,但是线程的任务一旦执行完毕,生命周期就结束,无法再使用,已经是个废物了。所以用强指针保住命没什么意义,只能用RunLoop让线程一直有事可做,一直保持激活状态。

    三. 线程保活的封装(C语言)

    MJPermenantThread.h文件

    #import <Foundation/Foundation.h>
    
    typedef void (^MJPermenantThreadTask)(void);
    
    @interface MJPermenantThread : NSObject
    
    /**
     开启线程
     */
    //- (void)run;
    
    /**
     在当前子线程执行一个任务
     */
    - (void)executeTask:(MJPermenantThreadTask)task;
    
    /**
     结束线程
     */
    - (void)stop;
    
    @end
    

    MJPermenantThread.m文件

    #import "MJPermenantThread.h"
    
    /** MJThread **/
    @interface MJThread : NSThread
    @end
    @implementation MJThread
    - (void)dealloc
    {
        NSLog(@"%s", __func__);
    }
    @end
    
    /** MJPermenantThread **/
    @interface MJPermenantThread()
    @property (strong, nonatomic) MJThread *innerThread;
    @end
    
    @implementation MJPermenantThread
    #pragma mark - public methods
    - (instancetype)init
    {
        if (self = [super init]) {
            self.innerThread = [[MJThread alloc] initWithBlock:^{
                NSLog(@"begin----");
                
                // 创建上下文(要初始化一下结构体,否则结构体里面有可能是垃圾数据)
                CFRunLoopSourceContext context = {0};
                
                // 创建source
                CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
                
                // 往Runloop中添加source
                CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
                
                // 销毁source
                CFRelease(source);
                
                // 启动
                //参数:模式,过时时间(1.0e10一个很大的值),是否执行完source后就会退出当前loop
                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
                
                //如果使用的是C语言的方式就可以通过最后一个参数让执行完source之后不退出当前Loop,所以就可以不用stopped属性了
    //            while (weakSelf && !weakSelf.isStopped) {
    //                // 第3个参数:returnAfterSourceHandled,设置为true,代表执行完source后就会退出当前loop
    //                CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
    //            }
                
                NSLog(@"end----");
            }];
            
            [self.innerThread start];
        }
        return self;
    }
    
    //- (void)run
    //{
    //    if (!self.innerThread) return;
    //
    //    [self.innerThread start];
    //}
    
    - (void)executeTask:(MJPermenantThreadTask)task
    {
        if (!self.innerThread || !task) return;
        
        [self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
    }
    
    - (void)stop
    {
        if (!self.innerThread) return;
        
        [self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
    }
    
    - (void)dealloc
    {
        NSLog(@"%s", __func__);
        
        [self stop];
    }
    
    #pragma mark - private methods
    - (void)__stop
    {
        CFRunLoopStop(CFRunLoopGetCurrent());
        self.innerThread = nil;
    }
    
    - (void)__executeTask:(MJPermenantThreadTask)task
    {
        task();
    }
    
    @end
    

    C语言方式和OC方式达到的效果都是一样的,但是C语言方式控制的更精准,可以控制执行完source后不退出当前loop,这样就不用写while循环了。

    Demo地址:线程保活

    相关文章

      网友评论

        本文标题:iOS-RunLoop2-线程保活

        本文链接:https://www.haomeiwen.com/subject/hnnxnctx.html