iOS开发基础92-线程保活:从 RunLoop 原理到常驻线程封装

iOS 线程保活完全指南:从 RunLoop 原理到常驻线程封装

线程保活是 iOS 开发中一个进阶但实用的技术。当有任务需要随时在子线程执行时,反复创建销毁线程会消耗性能,这时就需要让一个子线程一直存活。本文从线程与 RunLoop 的关系讲起,深入解析线程保活的核心原理,给出完整的 OC 封装实现和 CFRunLoop 底层版本,补充停止线程、自动释放池、内存管理等关键细节,最后给出 Swift 版本对照和常见坑排查。


一、线程保活概述

一句话原理

线程保活就是让一个子线程执行完任务后不退出,一直处于"睡眠-被唤醒-处理任务"的循环中,随时可以接收新任务,避免反复创建销毁线程的开销。

为什么需要线程保活

正常情况下,线程执行完任务后就会被系统回收,生命周期结束:

// 线程执行完 block 后就退出了
NSThread *thread = [[NSThread alloc] initWithBlock:^{
    NSLog(@"执行任务");
}];
[thread start];
// 任务执行完,线程销毁

如果有一个任务随时可能执行(比如定时监控、频繁的后台计算、网络回调处理),每次都创建新线程:

  • 线程创建和销毁有开销(需要分配栈空间、注册到系统)。
  • 频繁创建销毁可能导致性能抖动。
  • 某些任务需要在同一个线程上下文执行(如依赖线程局部存储、NSTimer 在子线程运行)。

线程保活的思路:让线程不退出,用 RunLoop 保持它存活,有任务时唤醒执行,没任务时睡眠(不占 CPU)。

适用场景

场景 说明
频繁的后台任务 任务随时可能触发,且需要在子线程执行
NSTimer 在子线程 Timer 依赖 RunLoop,子线程需要保活才能运行 Timer
监控/轮询线程 持续监控某个状态,间隔执行
串行任务队列 多个任务需要在同一个线程顺序执行
第三方库内部 如 AFNetworking 2.x 的常驻线程处理网络回调

注意:线程保活只适用于非并发场景(一个线程处理任务),如果需要并发处理,用 GCD 队列或 NSOperationQueue 更合适。


二、线程与 RunLoop 的关系

一句话原理

每个线程默认没有 RunLoop,只有当你获取 [NSRunLoop currentRunLoop] 时才会创建;RunLoop 是一个"do-while"循环,有事件就处理,没事件就睡眠,正是它让线程不退出。

核心关系

关系 说明
线程和 RunLoop 一一对应 每个线程有且只有一个 RunLoop(需要时创建)
主线程默认有 RunLoop UIApplicationMain 内部自动开启了主线程的 RunLoop
子线程默认没有 RunLoop 需要手动获取并开启
RunLoop 保活线程 RunLoop 的循环让线程不退出,没事件时睡眠不占 CPU

RunLoop 为什么能保活线程

RunLoop 的本质是一个循环:

do {
    // 1. 通知 Observers:即将进入 RunLoop
    // 2. 通知 Observers:即将处理 Timers
    // 3. 通知 Observers:即将处理 Sources
    // 4. 处理 Source0(非端口事件)
    // 5. 处理 Source1(端口事件)
    // 6. 通知 Observers:即将睡眠
    // 7. 睡眠(等待消息唤醒,不占 CPU)
    // 8. 被唤醒,处理事件
    // 9. 通知 Observers:刚从睡眠中醒来
} while (还有事件 && 没有被停止);

关键点:

  • 没有事件时睡眠:用 mach_msg 监听消息端口,没有消息时线程休眠,不消耗 CPU。
  • 有事件时唤醒:Source/Timer/Observer 事件到达时唤醒线程处理。
  • 循环不退出:只要没有调用 CFRunLoopStop 或没有输入源,循环就一直进行。

主线程之所以能一直运行不退出,就是因为 UIApplicationMain 开启了主线程的 RunLoop。子线程要保活,也需要开启 RunLoop。


三、线程保活的核心原理

一句话原理

开启 RunLoop 前必须至少添加一个 Source/Timer/Observer,否则 RunLoop 启动后会立刻退出(没有事件可等,循环直接结束)。

为什么需要 addPort 或 addSource

// 错误写法:RunLoop 启动后立刻退出
NSThread *thread = [[NSThread alloc] initWithBlock:^{
    [[NSRunLoop currentRunLoop] run]; // 没有任何输入源,立刻返回
    NSLog(@"线程退出了"); // 会立刻执行到这里
}];

run 方法内部会检查是否有输入源,如果没有就直接返回,线程退出。所以必须先添加一个输入源:

// 正确写法:先添加 port,再开启 RunLoop
NSThread *thread = [[NSThread alloc] initWithBlock:^{
    // 添加一个 port 作为输入源,保持 RunLoop 不退出
    [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"线程退出了"); // 只有 RunLoop 被停止才会到这里
}];

三种保活方式对比

方式 说明 推荐度
addPort: 添加一个空端口,最简单,OC 层面 ⭐⭐⭐⭐ 推荐
CFRunLoopSourceCreate 创建自定义 Source,C 层面,更底层 ⭐⭐⭐⭐ 推荐
addTimer:forMode: 添加一个重复的 Timer(如间隔很大) ⭐⭐ 不推荐(浪费资源)

四、线程保活的完整实现

步骤一:创建子线程并开启 RunLoop

@interface PermenantThread ()
@property (nonatomic, strong) NSThread *innerThread;
@property (nonatomic, assign) BOOL isStopped;
@end

@implementation PermenantThread

- (instancetype)init {
    if (self = [super init]) {
        _isStopped = NO;
        __weak typeof(self) weakSelf = self;
        
        self.innerThread = [[NSThread alloc] initWithBlock:^{
            // 1. 添加输入源(port),保持 RunLoop 不退出
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            
            // 2. 循环运行 RunLoop,直到被停止
            while (!weakSelf.isStopped) {
                // runMode:beforeDate: 在没有任务时睡眠,有任务时处理后返回
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
            
            NSLog(@"线程退出");
        }];
        
        [self.innerThread start];
    }
    return self;
}

为什么用 while + runMode:beforeDate: 而不是直接 run?

  • run 方法无法主动停止(它会一直运行,直到 RunLoop 被手动停止或所有输入源移除)。
  • runMode:beforeDate: 处理完一个事件后就会返回,我们可以在 while 循环中检查 isStopped 标志,决定是否继续。
  • 这是最灵活的控制方式。

步骤二:在保活线程上执行任务

- (void)executeTask:(void (^)(void))task {
    if (!task || self.isStopped) return;
    
    // 在 innerThread 上执行任务,wait 设为 NO(不阻塞当前线程)
    [self performSelector:@selector(executeTaskInternal:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}

- (void)executeTaskInternal:(void (^)(void))task {
    task();
}

performSelector:onThread:withObject:waitUntilDone: 会唤醒目标线程的 RunLoop,执行指定方法。这是线程间通信的标准方式。

步骤三:停止线程

- (void)stop {
    if (self.isStopped) return;
    
    // 在 innerThread 上执行停止操作
    [self performSelector:@selector(stopInternal) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}

- (void)stopInternal {
    self.isStopped = YES;
    
    // 停止 RunLoop(关键!否则 RunLoop 可能还在睡眠,while 循环无法检查 isStopped)
    CFRunLoopStop(CFRunLoopGetCurrent());
}

关键点:设置 isStopped = YES 后必须调用 CFRunLoopStop(),因为 RunLoop 可能正在睡眠中,不会立刻检查 while 条件。CFRunLoopStop 会唤醒 RunLoop 并让 runMode:beforeDate: 返回,然后 while 检查 isStopped 为 YES,退出循环,线程结束。

步骤四:dealloc 时自动停止

- (void)dealloc {
    [self stop];
    NSLog(@"PermenantThread dealloc");
}

五、完整封装类

// PermenantThread.h
#import <Foundation/Foundation.h>

typedef void (^PermenantThreadTask)(void);

@interface PermenantThread : NSObject

/** 开启线程(init 时自动开启,此方法可手动重启) */
- (void)run;

/** 在保活线程上执行任务 */
- (void)executeTask:(PermenantThreadTask)task;

/** 停止线程(对象销毁时自动调用) */
- (void)stop;

@end
// PermenantThread.m
#import "PermenantThread.h"
#import <CoreFoundation/CoreFoundation.h>

@interface PermenantThread ()
@property (nonatomic, strong) NSThread *innerThread;
@property (nonatomic, assign) BOOL isStopped;
@end

@implementation PermenantThread

- (instancetype)init {
    if (self = [super init]) {
        [self run];
    }
    return self;
}

#pragma mark - 公开方法

- (void)run {
    if (self.innerThread && !self.isStopped) return; // 已经在运行
    
    self.isStopped = NO;
    __weak typeof(self) weakSelf = self;
    
    self.innerThread = [[NSThread alloc] initWithBlock:^{
        @autoreleasepool { // 子线程必须手动管理 autoreleasepool
            // 添加输入源
            [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
            
            // 循环运行
            while (!weakSelf.isStopped) {
                @autoreleasepool { // 每次循环一个 autoreleasepool,及时释放临时对象
                    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
                }
            }
        }
        NSLog(@"保活线程退出");
    }];
    
    self.innerThread.name = @"com.example.permenantThread";
    [self.innerThread start];
}

- (void)executeTask:(PermenantThreadTask)task {
    if (!task || self.isStopped || !self.innerThread) return;
    
    [self performSelector:@selector(executeTaskInternal:) 
                 onThread:self.innerThread 
               withObject:task 
            waitUntilDone:NO];
}

- (void)stop {
    if (self.isStopped || !self.innerThread) return;
    
    // 在 innerThread 上执行停止,waitUntilDone:YES 确保停止完成
    [self performSelector:@selector(stopInternal) 
                 onThread:self.innerThread 
               withObject:nil 
            waitUntilDone:YES];
    
    self.innerThread = nil;
}

#pragma mark - 内部方法

- (void)executeTaskInternal:(PermenantThreadTask)task {
    if (task) task();
}

- (void)stopInternal {
    self.isStopped = YES;
    // 唤醒并停止 RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
}

#pragma mark - 生命周期

- (void)dealloc {
    [self stop];
    NSLog(@"PermenantThread dealloc");
}

@end

使用示例

@interface ViewController ()
@property (nonatomic, strong) PermenantThread *thread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 创建保活线程(init 时自动开启)
    self.thread = [[PermenantThread alloc] init];
    
    // 在子线程执行任务
    [self.thread executeTask:^{
        NSLog(@"执行任务 - %@", [NSThread currentThread]);
        // 做耗时操作...
    }];
}

- (void)dealloc {
    // 可选:stop 会在 dealloc 时自动调用
    // [self.thread stop];
}

@end

六、CFRunLoop 底层版本

如果不想用 addPort:,可以用 CoreFoundation 的 CFRunLoopSource 来保活:

self.innerThread = [[NSThread alloc] initWithBlock:^{
    @autoreleasepool {
        // 1. 创建 Source 上下文(结构体必须初始化)
        CFRunLoopSourceContext context = {0};
        
        // 2. 创建 Source
        CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
        
        // 3. 添加到当前 RunLoop
        CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
        
        // 4. Source 已经添加到 RunLoop,可以释放本地引用(RunLoop 会持有)
        CFRelease(source);
        
        // 5. 启动 RunLoop
        // 参数:mode、超时时间(1.0e10 约等于无穷大)、返回后是否停止
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
        
        // RunLoop 停止后到这里
        NSLog(@"线程退出");
    }
}];

CFRunLoopRunInMode 的第三个参数 returnAfterSourceHandled 设为 false,表示处理完事件后继续运行,不返回。设为 true 则处理完一个事件就返回,类似 runMode:beforeDate:。

停止 CFRunLoop 版本的线程

- (void)stopInternal {
    self.isStopped = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
}

七、实际应用场景

场景一:子线程运行 NSTimer

NSTimer 依赖 RunLoop,子线程默认没有 RunLoop,Timer 不会执行。线程保活后可以在子线程运行 Timer:

[self.thread executeTask:^{
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 
                                                      target:self 
                                                    selector:@selector(timerFire:) 
                                                    userInfo:nil 
                                                     repeats:YES];
    // 因为线程有 RunLoop,timer 会正常运行
}];

场景二:AFNetworking 2.x 的常驻线程

AFNetworking 2.x 用一个常驻线程处理所有网络请求的回调,保证回调在同一个线程上下文:

// AFNetworking 内部的实现思路
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

场景三:监控/轮询线程

持续监控某个状态(如文件变化、socket 连接),间隔执行检查任务:

[self.thread executeTask:^{
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:5.0 
                                                      repeats:YES 
                                                        block:^(NSTimer * _Nonnull timer) {
        [self checkStatus]; // 每 5 秒检查一次
    }];
}];

八、常见问题与坑

Q1:子线程需要 autoreleasepool 吗?

需要。主线程的 RunLoop 会自动创建和销毁 autoreleasepool,但子线程需要手动管理:

// 正确写法
NSThread *thread = [[NSThread alloc] initWithBlock:^{
    @autoreleasepool { // 外层 autoreleasepool
        // ... RunLoop 代码
        while (!self.isStopped) {
            @autoreleasepool { // 每次循环一个 autoreleasepool,及时释放
                [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
            }
        }
    }
}];

不写 autoreleasepool,子线程中产生的 autorelease 对象会泄漏,内存持续增长。

Q2:为什么 stop 后线程没有立刻退出?

可能原因:

  1. 没有调用 CFRunLoopStop:只设置 isStopped = YES,RunLoop 还在睡眠,不会检查 while 条件。必须调用 CFRunLoopStop() 唤醒。
  2. 有任务正在执行:RunLoop 正在处理任务,等任务处理完才会检查退出条件。
  3. stop 调用线程不对:CFRunLoopStop 必须在目标线程上调用(用 performSelector:onThread: 切过去)。

Q3:保活线程会占用 CPU 吗?

不会。RunLoop 在没有事件时会进入睡眠状态(mach_msg 等待消息),不消耗 CPU。只有被事件唤醒处理任务时才占用 CPU。这是线程保活的核心优势——常驻但不耗电。

Q4:保活线程和 GCD 队列怎么选?

对比 保活线程 GCD 串行队列
并发 单线程,非并发 单线程,非并发
任务执行 performSelector 投递 dispatch_async 投递
Timer 支持(线程有 RunLoop) 不支持(GCD 队列没有 RunLoop)
线程生命周期 手动管理 系统管理
适用场景 需要 RunLoop/Timer、线程上下文 大多数后台任务

大多数场景用 GCD 串行队列更简单。只有需要 RunLoop(如 NSTimer、Source 事件)或需要固定线程上下文时,才用线程保活。

Q5:performSelector:onThread: 有什么注意事项?

  • 目标线程必须有 RunLoop,否则方法不会执行。
  • waitUntilDone:YES 会阻塞当前线程,等目标线程执行完才返回。如果在目标线程上调用 waitUntilDone:YES 会死锁。
  • withObject: 只支持对象类型,基本类型需要包装成 NSNumber。
  • 方法签名必须是 - (void)method:(id)obj 的形式。

Q6:保活线程的内存泄漏问题?

常见泄漏原因:

  1. block 强引用 self:executeTask 的 block 中引用了 self,而 self 强引用 thread,thread 的 block 又强引用 task,形成循环引用。用 __weak 解决。
  2. 线程没有停止:dealloc 时没有调用 stop,线程一直存活,持有 self,导致 self 无法释放。确保 dealloc 时调用 stop。
  3. autoreleasepool 缺失:子线程没有 autoreleasepool,临时对象无法释放。

Q7:可以在保活线程中更新 UI 吗?

绝对不要。UI 操作必须在主线程执行。保活线程中执行完任务后,如果需要更新 UI,切回主线程:

[self.thread executeTask:^{
    // 子线程做耗时操作
    UIImage *image = [self loadImage];
    
    dispatch_async(dispatch_get_main_queue(), ^{
        self.imageView.image = image; // 回主线程更新 UI
    });
}];

Q8:RunLoop 的 mode 有什么用?

RunLoop 有多个 mode,每个 mode 有独立的 Source/Timer/Observer。RunLoop 一次只能运行在一个 mode 下:

mode 说明
NSDefaultRunLoopMode 默认模式,大多数情况用这个
UITrackingRunLoopMode 滚动时的模式,ScrollView 滚动时 RunLoop 切换到这个 mode
NSRunLoopCommonModes 通用模式,包含 Default 和 Tracking 等

线程保活通常用 NSDefaultRunLoopMode 即可。如果 Timer 需要在滚动时也运行,用 NSRunLoopCommonModes。

Q9:多个保活线程有必要吗?

一般不需要。一个保活线程处理所有串行任务即可。如果需要并发,用 GCD 并发队列或 NSOperationQueue,不要创建多个保活线程(每个线程都有栈空间开销,默认 512KB)。

Q10:Swift 中怎么实现线程保活?

Swift 中 performSelector:onThread: 是 OC 运行时方法,Swift 中可以用,但更推荐用 GCD 的 DispatchSourceTimer 或 Swift Concurrency 的 Actor。如果确实需要保活线程,思路和 OC 一样,用 Thread + RunLoop。


九、Swift 版本对照

import Foundation

class PermenantThread: NSObject {
    private var innerThread: Thread?
    private var isStopped = false
    
    override init() {
        super.init()
        run()
    }
    
    func run() {
        if innerThread != nil && !isStopped { return }
        
        isStopped = false
        weak var weakSelf = self
        
        innerThread = Thread {
            autoreleasepool {
                // 添加输入源
                RunLoop.current.add(Port(), forMode: .default)
                
                while !(weakSelf?.isStopped ?? true) {
                    autoreleasepool {
                        RunLoop.current.run(mode: .default, before: .distantFuture)
                    }
                }
            }
            print("保活线程退出")
        }
        innerThread?.name = "com.example.permenantThread"
        innerThread?.start()
    }
    
    func executeTask(_ task: @escaping () -> Void) {
        guard let thread = innerThread, !isStopped else { return }
        
        // Swift 中用 perform 切换线程
        perform(#selector(executeTaskInternal(_:)), on: thread, with: task, waitUntilDone: false)
    }
    
    @objc private func executeTaskInternal(_ task: () -> Void) {
        task()
    }
    
    func stop() {
        guard let thread = innerThread, !isStopped else { return }
        
        perform(#selector(stopInternal), on: thread, with: nil, waitUntilDone: true)
        innerThread = nil
    }
    
    @objc private func stopInternal() {
        isStopped = true
        CFRunLoopStop(CFRunLoopGetCurrent())
    }
    
    deinit {
        stop()
        print("PermenantThread deinit")
    }
}

// 使用
let thread = PermenantThread()
thread.executeTask {
    print("执行任务 - \(Thread.current)")
}
// thread.stop()

Swift 中更推荐的替代方案:

  • DispatchSourceTimer:GCD 定时器,不需要保活线程,系统自动管理。
  • Actor(Swift 5.5+):编译器保证线程安全的串行执行,比手动线程保活更安全。

十、总结

  • 线程保活的本质:用 RunLoop 让子线程不退出,有任务时唤醒执行,没任务时睡眠(不占 CPU)。
  • 核心原理:RunLoop 必须至少有一个输入源(Port/Source/Timer)才能运行,否则启动后立刻退出。
  • 实现步骤:创建子线程 → 添加 Port/Source → while 循环调用 runMode:beforeDate: → performSelector:onThread: 投递任务 → CFRunLoopStop 停止。
  • 关键细节:
    • 子线程必须手动管理 @autoreleasepool。
    • 停止线程必须调用 CFRunLoopStop() 唤醒 RunLoop。
    • dealloc 时自动 stop,避免内存泄漏。
    • UI 操作必须回主线程。
  • 适用场景:需要 RunLoop/Timer、固定线程上下文、频繁后台任务;大多数场景用 GCD 更简单。
  • 常见坑:忘记 autoreleasepool、stop 不调用 CFRunLoopStop、block 循环引用、在保活线程更新 UI、用全局队列做 barrier。

posted @ 2018-09-04 16:08  Mr.陳  阅读(1603)  评论(0)    收藏  举报