OC线程保活

引入:

  什么是线程保活呢?为什么要保活?有什么用呢?上来就是灵魂三连,当然这也是这篇文章的主题;还包括在实现线程保活过程中遇到的问题的记录。

简介:

  线程:分主线程和其它线程(也叫子线程)。主线程中进行ui刷新和事件的处理等操作,且主线程存在RunLoop是一直存在线程中的,且在操作完成后不会退出RunLoop,而是处在闲置状态;而其它线程中的RunLoop是在第一次获取的时候创建并存在的,且当该线程的操作完成后,线程及其中的运行循环会自动销毁。
   线程的创建和销毁很耗性能,所以经常在子线程做事情最好使用线程保活,例如AFNetworking 2.X版本就使用RunLoop进行线程保活的。
  那么怎样才能让其它线程一直存活呢?
其实很简单,让该线程中RunLoop一直运行,不退出,该线程就会一直存活在程序中。
  那么另一个问题来了怎样保持RunLoop一直运行不退出呢?
RunLoop里面添加Source\Timer\ObserverPort相关的是Source事件,RunLoop运行起来。
  知易行难,在子线程保活的过程中会遇到各种小问题,下面是我在线程保活和释放过程中遇到的问题。

实现过程及遇到的问题:

1.子线程的正常创建和销毁:

  用NSThreadinitWithTarget: selector: object:或者initWithBlock:方法创建线程,并start。当线程中的操作完成后,线程和RunLoop会自动销毁。

/// 自定义线程,重写线程的dealloc辅助打印释放线程的信息
@implementation ZBYThread

- (void)dealloc {
    NSLog(@"%s", __func__);
}

@end

/// 创建线程
- (void)threadNoRun {
    ZBYThread * thread = [[ZBYThread alloc] initWithTarget:self selector:@selector(run2) object:nil];
    [thread start];
}

/// run方法:运行结束后线程被释放
- (void)run {
   @autoreleasepool {   // 防止大次循环内存暴涨
       for (int i = 0; i < 100; i++) {
          NSLog(@"--子线程操作%ld",(long)i);
       }
   }
    NSLog(@"%s ----end----", __func__);
}

上述代码运行的结果是任务打印任务结束后,直接输出end并释放线程。如下:

-[ZBYThreadAliveViewController run] ----end----
-[ZBYThread dealloc]
2.只添加Port等事件,不运行RunLoop,情况和1相同:

代码:

- (void)threadNoRun {
    ZBYThread * thread = [[ZBYThread alloc] initWithTarget:self selector:@selector(run2) object:nil];
    [thread start];
}

- (void)run2 {
    // addPort可以添加
    [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
//    [[NSRunLoop currentRunLoop] addTimer:<#(nonnull NSTimer *)#> forMode:<#(nonnull NSRunLoopMode)#>]
   @autoreleasepool {   // 防止大次循环内存暴涨
       for (int i = 0; i < 100; i++) {
          NSLog(@"--子线程操作%ld",(long)i);
       }
   }
    NSLog(@"--end--%@",[NSThread currentThread]);
}

打印结果:

--end--<ZBYThread: 0x6000005595c0>{number = 9, name = (null)}
-[ZBYThread dealloc]
3.添加Port事件并运行RunLooprun方法,RunLoop一直运行,不会退出,导致线程不会销毁:

我们看一下run方法的官方注视:

     this method effectively begins an infinite loop that processes data from the run loop’s input sources and timers.  
     // 这个方法有效地开始了一个无限循环,处理来自运行循环的输入源和计时器的数据。
     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]]);
     where shouldKeepRunning is set to NO somewhere else in the program.

它告诉我们,RunLooprun方法开启一个无限循环,导致RunLoop及其所在的线程无法退出和销毁。
如果需要对RunLoop进行退出操作,请使用runMode方法。
代码:

- (void)threadRun {
    ZBYThread * thread = [[ZBYThread alloc] initWithTarget:self selector:@selector(run3) object:nil];
    [thread start];
    dispatch_after(1.5, dispatch_get_main_queue(), ^{   // 延迟在子线程中添加操作
        [self performSelector:@selector(doSomethingInAliveThread) onThread:thread withObject:nil waitUntilDone:NO];
    });
    dispatch_after(2.5, dispatch_get_main_queue(), ^{   // 退出线程,无法正常退出,因为NSRunLoop的run方法
        [self performSelector:@selector(quitRunLoop) onThread:thread withObject:nil waitUntilDone:NO];
    });
}

- (void)run3 {
    @autoreleasepool {   // 防止大次循环内存暴涨
        for (int i = 0; i < 100; i++) {
            NSLog(@"--子线程操作%ld",(long)i);
        }
        // addPort可以添加
        [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
        // NSRunLoop的run方法是无法停止的,它专门用于开启一个永不销毁的线程(NSRunLoop)
        [[NSRunLoop currentRunLoop] run];
        NSLog(@"--end--%@",[NSThread currentThread]);    // 当RunLoop停止时,会执行该代码
    }
}

- (void)doSomethingInAliveThread {
    // 任务可以正常执行,说明线程一直是活着的
    NSLog(@"%s", __func__);
}

打印:

任务打印省略
-[ZBYThreadAliveViewController doSomethingInAliveThread]

最后没有线程释放的打印,说明线程一直存活。

4.添加Port事件并运行runMode方法,RunLoop一直运行,当标记改变RunLoop退出,线程销毁:

需要注意的点:

  1. 需要用标记,打破while循环;
  2. 如果用属性强引用引用线程,注意引用循环;
  3. 当外部强引用线程时,注意造成的循环引用;或者强引用线程的对象提前释放造成标记永远为NO,无法打破while,造成RunLoop无法退出;
  4. 当线程退出RunLoop后,无法重新启用。
    代码:
- (void)threadRunModelStrong {
    ZBYThread * thread = [[ZBYThread alloc] initWithTarget:self selector:@selector(runForStrong) object:nil];
    [thread start];
    // 属性强引用线程,容易造成循环引用
    self.thread = thread;
    dispatch_after(1.5, dispatch_get_main_queue(), ^{   // 延迟在子线程中添加操作
        [self performSelector:@selector(doSomethingInAliveThread) onThread:thread withObject:nil waitUntilDone:NO];
    });
    dispatch_after(2.5, dispatch_get_main_queue(), ^{   // 退出线程,可以正常退出,因为NSRunLoop的runModel方法
        [self performSelector:@selector(quitRunLoop2) onThread:thread withObject:nil waitUntilDone:NO];
    });
}

- (void)runForStrong {
    __weak typeof(self) weakSelf = self;
    self.isStoped = NO;
    @autoreleasepool {
        for (int i = 0; i < 100; i++) {
            NSLog(@"----子线程任务 %ld",(long)i);
        }
        NSLog(@"%@----子线程任务结束",[NSThread currentThread]);
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        // 往RunLoop里面添加Source\Timer\Observer,Port相关的事件
        // 添加了一个Source1,但是这个Source1也没啥事,所以线程在这里就休眠了,不会往下走,----end----一直不会打印
        [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
    
        // weakSelf判断是否存在,不存在会默认为NO,无法打破while循环
        // 要使用weakself,不然self强引用thread,thread强引用block,block强引用self,产生循环引用。
        while (weakSelf && !weakSelf.isStoped) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        NSLog(@"%s ----end----", __func__); // 当RunLoop停止时,会执行该代码
    }
}

/// 需要在quitRunLoop中,进行如下设置
- (void)quitRunLoop2 {
    self.isStoped = YES;
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    [self.thread cancel];
    // 解决循环引用问题
    self.thread = nil;
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

打印:

省略操作打印
-[ZBYThreadAliveViewController doSomethingInAliveThread]
-[ZBYThreadAliveViewController quitRunLoop2] <ZBYThread: 0x600000559240>{number = 12, name = (null)}
----end----
-[ZBYThread dealloc]

最后成功释放了线程。

6.完整封装见类:ZBYThreadTool两种封装
  1. RunLoop运行Port保活线程
    需要添加标记,打破while循环,退出RunLoop
  2. RunLoop运行source保活线程
    更偏向底层c语言,控制的更精准;可以控制执行完source后不退出当前loop,这样不用写while循环添加标记了。
    封装代码:
// .h代码
#import "ZBYThread.h"

/// 任务的回调
typedef void (^ZBYThreadToolTask)(void);

@interface ZBYThreadAliveTool : NSObject

@property (nonatomic, strong, readonly) ZBYThread * innerThread;

@property (nonatomic, assign, getter = isStopped) BOOL stopped;

/// 保活线程
+ (instancetype)threadToolForPort;

/// 保活线程
+ (instancetype)threadToolForC;

/// 在当前子线程添加一个执行任务
/// @param task 执行任务
- (void)addExecuteTask:(ZBYThreadToolTask)task;


/// 结束并销毁线程
- (void)stop;

@end

// .m代码

@interface ZBYThreadAliveTool ()

@property (strong, nonatomic, readwrite) ZBYThread * innerThread;

@end

@implementation ZBYThreadAliveTool

/// RunLoop运行Port保活线程
+ (instancetype)threadToolForPort {
    ZBYThreadAliveTool * tool = [ZBYThreadAliveTool new];
    tool.stopped = NO;
    __weak typeof(tool) weakTool = tool;
    tool.innerThread = [[ZBYThread alloc] initWithBlock:^{
        NSLog(@"begin----");
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
        while (weakTool && !weakTool.isStopped) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        NSLog(@"end----");
    }];
    // 自动开始线程
    [tool.innerThread start];
    return tool;
}

/// RunLoop运行source保活线程
/// C语言方式和OC方式达到的效果都是一样的,但是C语言方式控制的更精准,可以控制执行完source后不退出当前loop,
/// 这样就不用写while循环了。
+ (instancetype)threadToolForC {
    ZBYThreadAliveTool * tool = [ZBYThreadAliveTool new];
    tool.stopped = NO;
//    __weak typeof(tool) weakTool = tool;
    tool.innerThread = [[ZBYThread 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----");
    }];
    // 自动开始线程
    [tool.innerThread start];
    return tool;
}

- (void)addExecuteTask:(ZBYThreadToolTask)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 {
    [self stop];
}

// MARK: - private methods

- (void)_stop {
    self.stopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
    self.innerThread = nil;
}

- (void)_executeTask:(ZBYThreadToolTask)task {
    task();
}

@end

调用代码:

-(void)threadAliveTool {
    ZBYThreadAliveTool * tool = [ZBYThreadAliveTool threadToolForPort];
//    ZBYThreadAliveTool * tool = [ZBYThreadAliveTool threadToolForC];
    [tool addExecuteTask:^{
        NSLog(@"--添加操作--%@",[NSThread currentThread]);
    }];
    dispatch_after(1.0, dispatch_get_main_queue(), ^{
        [tool stop];
    });
}

打印:

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

推荐阅读更多精彩内容