iOS-RunLoop2-线程保活

如果经常要在子线程中做事情,不使用保活,就会一直创建、销毁子线程,这样很耗性能的,所以经常在子线程做事情最好使用线程保活,比如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 *)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] {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,不然退出当前VC,当前VC不会被销毁,为什么这样还没想明白。

不使用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 *)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,产生循环引用。

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

{number = 3, name = (null)}----begin----

说明线程开始工作了。

点击空白,打印:

-[ViewController test] {number = 3, name = (null)}

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

点击stop打印:

-[ViewController stopThread] {number = 3, name = (null)}
{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] {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,这时候又崩了:

iOS-RunLoop2-线程保活_第1张图片
崩了

点击暂停之后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 *)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 

//任务的回调
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 *)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 

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-线程保活)