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 后线程没有立刻退出?
可能原因:
- 没有调用 CFRunLoopStop:只设置
isStopped = YES,RunLoop 还在睡眠,不会检查 while 条件。必须调用CFRunLoopStop()唤醒。 - 有任务正在执行:RunLoop 正在处理任务,等任务处理完才会检查退出条件。
- 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:保活线程的内存泄漏问题?
常见泄漏原因:
- block 强引用 self:
executeTask的 block 中引用了 self,而 self 强引用 thread,thread 的 block 又强引用 task,形成循环引用。用__weak解决。 - 线程没有停止:dealloc 时没有调用 stop,线程一直存活,持有 self,导致 self 无法释放。确保 dealloc 时调用 stop。
- 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。

浙公网安备 33010602011771号