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开启成功。多次点击按钮,在子线程中执行任务,日志如下:

点击了按钮
任务在子线程中执行:<TestThread: 0x60000179f9c0>{number = 6, name = 常驻线程}
点击了按钮
任务在子线程中执行:<TestThread: 0x60000179f9c0>{number = 6, name = 常驻线程}
点击了按钮
任务在子线程中执行:<TestThread: 0x60000179f9c0>{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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,826评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,968评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,234评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,562评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,611评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,482评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,271评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,166评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,608评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,814评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,926评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,644评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,249评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,866评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,991评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,063评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,871评论 2 354

推荐阅读更多精彩内容