iOS八股文之 多线程
一、iOS的多线程是啥
- iOS 多线程是提升 App 性能、避免主线程阻塞的核心技术;
- 其本质是通过并发执行任务,充分利用 CPU 资源(现代设备多为多核),同时保证 UI 响应流畅(主线程(UI 线程)负责处理 UI 绘制、用户交互(点击、滑动等))。
- 所以不仅要掌握工具用法,还要理解 “线程调度”“资源竞争”“任务协作” 等底层逻辑,方便更好的判断如何使用及避免踩坑。
二、iOS的几种多线程技术方案
主要有 4 种多线程方案,从底层到高层封装程度递增:
技术方案 | 简介 | 特点 | 适用场景 |
---|---|---|---|
pthread | 跨平台 C 语言线程库(POSIX Thread) | 底层、无封装,需手动管理生命周期 | 几乎不用(除非兼容其他平台) |
NSThread | OC 封装的线程类 | 轻量,可直接操作线程对象 | 简单场景(如临时子线程,需手动控制) |
GCD(Grand Central Dispatch) | 系统级调度框架(C 语言) | 自动管理线程生命周期,高效利用 CPU 核心 | 绝大多数场景(简单任务、并发控制) |
NSOperationQueue | 基于 GCD 的 OC 封装(面向对象) | 支持任务依赖、优先级、取消 / 暂停,更灵活 | 复杂任务流(如多任务有依赖关系、需监听状态) |
但实际开发中以 GCD 和 NSOperationQueue 为主:
1. GCD(建议用)
GCD 是苹果推荐的多线程方案,核心是 “队列 + 任务”,通过队列管理任务,系统自动调度线程执行任务。
- 队列(Dispatch Queue):存储任务的 “容器”,按 “FIFO(先进先出)” 原则执行任务,分两种类型:
- 串行队列(Serial Queue):每次只执行一个任务,任务按顺序执行(同一时间只有一个线程工作)。
- 自定义串行队列:dispatch_queue_create(“com.example.serial”, DISPATCH_QUEUE_SERIAL);
- 主队列(Main Queue):特殊的串行队列,运行在主线程,dispatch_get_main_queue()。
- 并行队列(Concurrent Queue):可同时执行多个任务(系统会自动创建多个线程并行处理),任务启动顺序按 FIFO,但执行完成顺序不确定。
- 全局并行队列:系统提供,无需手动创建,dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0)(QOS 控制优先级);
- 自定义并行队列:dispatch_queue_create(“com.example.concurrent”, DISPATCH_QUEUE_CONCURRENT)。
- 串行队列(Serial Queue):每次只执行一个任务,任务按顺序执行(同一时间只有一个线程工作)。
- 任务(Block):需要执行的代码块,通过 “同步 / 异步” 方式提交到队列:
- 同步执行(dispatch_sync):提交任务后,当前线程会等待任务执行完毕才继续往下走(会阻塞当前线程)。
- 异步执行(dispatch_async):提交任务后,当前线程立即继续往下走,不等待任务执行(不会阻塞当前线程)。
队列 + 任务的组合效果:
队列类型 | 同步执行(sync) | 异步执行(async) |
---|---|---|
串行队列 | 不创建新线程,当前线程顺序执行 | 创建 1 个子线程,任务顺序执行 |
并行队列 | 不创建新线程,当前线程顺序执行 | 创建多个子线程(数量由系统决定),任务并行执行 |
主队列 | 死锁(主线程等待自身执行任务) | 不创建新线程,主线程顺序执行(等同同步,无意义) |
2. NSOperationQueue
NSOperationQueue 是对 GCD 的面向对象封装,通过 NSOperation(任务)和 NSOperationQueue(队列)实现多线程,核心优势是任务可管理:
- NSOperation:单个任务,可通过子类(如 NSBlockOperation)实现,支持:
- addDependency:/removeDependency::设置任务依赖(如任务 B 必须在任务 A 完成后执行);
- setQueuePriority::设置任务优先级(仅同队列内有效);
- cancel:取消任务(仅未执行的任务可取消);
- isFinished/isCancelled:监听任务状态。
- NSOperationQueue:管理任务的队列,核心属性:
- maxConcurrentOperationCount:最大并发数(默认 NSOperationQueueDefaultMaxConcurrentOperationCount,由系统决定;设为 1 则变为串行队列);
- addOperation::添加任务到队列;
- cancelAllOperations:取消所有未执行的任务。
三、线程安全
- 当多个线程同时操作同一资源(如全局变量、共享数据)时,可能导致数据错乱(如银行转账中 “多线程同时扣减余额” 导致金额错误),这就是 “线程不安全”。
- 保证线程安全的核心是 “同步”:控制多个线程对共享资源的访问顺序,确保同一时间只有一个线程操作资源。
- 常见同步机制:
- @synchronized:OC 提供的简易锁,语法 @synchronized(锁对象) { … }(锁对象需唯一,如 self),底层是递归锁。
- NSLock:OC 锁对象,lock 加锁,unlock 解锁,非递归(同一线程多次 lock 会死锁)。
- dispatch_semaphore:GCD 信号量,通过 semaphore_wait(等待信号,信号量 -1)和 semaphore_signal(发送信号,信号量 +1)控制并发数(信号量为 0 时阻塞)。
- pthread_mutex:底层 C 语言互斥锁,功能强大,支持多种类型(如递归锁、条件锁)。
四、多线程的一些使用
1. 最常见场景:耗时操作放子线程执行,避免阻塞主线程更新 UI
这是最高频的场景(如网络请求、图片加载),以下为简单示例,实际优秀代码建议阅读AFNetworking及SDImage这些优秀的三方库源码;
// GCD 实现
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{// 子线程:执行耗时操作(如下载图片)UIImage *image = [self downloadImageWithURL:url];dispatch_async(dispatch_get_main_queue(), ^{// 主线程:更新 UIself.imageView.image = image;});
});// NSOperationQueue 实现
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{// 子线程:耗时操作UIImage *image = [self downloadImageWithURL:url];[[NSOperationQueue mainQueue] addOperationWithBlock:^{// 主线程:更新 UIself.imageView.image = image;}];
}];
2. 控制并发数:避免线程爆炸
当需要执行大量任务(如批量上传 100 张图片),若无限制并发,会创建过多线程(CPU 切换开销剧增),导致性能下降。用 dispatch_semaphore 控制最大并发数(如同时只传 5 张):
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(5); // 最大并发数 5for (int i = 0; i < 100; i++) {dispatch_async(concurrentQueue, ^{dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); // 信号量 -1,若为 0 则等待// 执行上传任务(耗时操作)[self uploadImage:imageList[i]];dispatch_semaphore_signal(semaphore); // 信号量 +1,唤醒等待的线程});
}
3. 任务依赖:按顺序执行关联任务
如 “先下载配置文件 → 解析配置 → 再下载图片”,用 NSOperationQueue 的依赖机制实现:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];// 任务1:下载配置
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{[self downloadConfig];
}];// 任务2:解析配置(依赖任务1)
NSBlockOperation *op2 = [NSBlockOperation blockOperationWithBlock:^{[self parseConfig];
}];
[op2 addDependency:op1]; // op2 依赖 op1// 任务3:下载图片(依赖任务2)
NSBlockOperation *op3 = [NSBlockOperation blockOperationWithBlock:^{[self downloadImages];
}];
[op3 addDependency:op2]; // op3 依赖 op2// 添加所有任务到队列
[queue addOperations:@[op1, op2, op3] waitUntilFinished:NO];
4. 一次性代码:确保某段代码只执行一次
这个典型例子是单例初始化,用 GCD 的 dispatch_once 保证线程安全且只执行一次:
+ (instancetype)shareInstance {static RPManager *manager = nil;static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{manager = [[self alloc] init];});return manager;
}
五、多线程踩坑收集
1. 疏忽导致在子线程操作 UI
UIKit 框架(如 UIView、UIControl)的方法仅在主线程调用才安全,子线程操作 UI 可能导致:
界面不刷新(UI 绘制依赖主线程 RunLoop);
崩溃(如 EXC_BAD_ACCESS,因 UI 组件的内部状态被多线程篡改)。
解决:所有 UI 操作必须通过 dispatch_async(dispatch_get_main_queue(), ^{…}) 切换到主线程。
2. 避免死锁场景
死锁是 “两个或多个线程互相等待对方释放资源” 导致的无限阻塞,最常见场景:
- 主队列同步执行:主线程正在执行任务 A,此时用 dispatch_sync 向主队列提交任务 B,主线程会等待任务 B 完成,但任务 B 需等任务 A 完成才能执行,形成死锁:
// 错误示例:主队列同步执行导致死锁
- (void)viewDidLoad {[super viewDidLoad];// 主线程正在执行 viewDidLoad(任务 A)dispatch_sync(dispatch_get_main_queue(), ^{// 任务 B 提交到主队列,需等待任务 A 完成,但任务 A 正在等任务 B... 死锁NSLog(@"死锁了");});
}
- 锁的嵌套使用不当:线程 1 持有锁 A 并等待锁 B,线程 2 持有锁 B 并等待锁 A,导致互相阻塞。
所以,避免在主队列用 dispatch_sync;锁的使用遵循 “同一顺序”(如所有线程都先加锁 A 再加锁 B)。
3. 共享资源必须加锁
当多个线程操作同一资源(如全局数组、单例属性),必须通过同步机制(锁、信号量)保证原子操作。例如,多线程向数组添加元素,不加锁会导致数组越界或数据丢失:
// 错误示例:多线程操作数组,线程不安全
NSMutableArray *array = [NSMutableArray array];
for (int i = 0; i < 1000; i++) {dispatch_async(dispatch_get_global_queue(0, 0), ^{[array addObject:@(i)]; // 多线程同时操作,可能崩溃});
}// 正确示例:加锁保证线程安全
NSMutableArray *array = [NSMutableArray array];
dispatch_semaphore_t semaphore = dispatch_semaphore_create(1); // 信号量 1(互斥锁)
for (int i = 0; i < 1000; i++) {dispatch_async(dispatch_get_global_queue(0, 0), ^{dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);[array addObject:@(i)]; // 同一时间只有一个线程操作dispatch_semaphore_signal(semaphore);});
}
4. 子线程中使用 RunLoop 未启动导致任务不执行
子线程的 RunLoop 默认不启动,若在子线程中使用 performSelector:withObject:afterDelay:(依赖 RunLoop),任务会因 RunLoop 未运行而不执行:
// 错误示例:子线程 performSelector 不执行
dispatch_async(dispatch_get_global_queue(0, 0), ^{[self performSelector:@selector(doTask) withObject:nil afterDelay:1.0];// 子线程 RunLoop 未启动,任务不会执行
});
//解决:手动启动子线程的 RunLoop(需添加事件源,如 NSPort):
dispatch_async(dispatch_get_global_queue(0, 0), ^{[self performSelector:@selector(doTask) withObject:nil afterDelay:1.0];// 添加事件源并启动 RunLoop[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];[[NSRunLoop currentRunLoop] run];
});
5. 全局队列的优先级滥用导致卡顿
全局并行队列有不同 QOS(服务质量)等级(如 QOS_CLASS_USER_INTERACTIVE 最高,QOS_CLASS_BACKGROUND 最低),若随意使用高优先级队列,会抢占主线程或关键任务的 CPU 资源,导致卡顿;所以非紧急任务别用高优先级队列
紧急 UI 相关任务:QOS_CLASS_USER_INTERACTIVE;
普通任务:QOS_CLASS_DEFAULT;
后台任务(如日志上传):QOS_CLASS_BACKGROUND。
六、多线程的一些其他补充
- Xcode 断点调试:在断点设置中勾选 “Thread Specific”,只在特定线程触发断点;
- 查看线程调用栈:Debug → Debug Workflow → Always Show Disassembly,或用 bt 命令在控制台打印当前线程调用栈;
- Instruments 监控:用 “Thread States” 模板查看线程状态(运行 / 阻塞),分析线程是否被过度创建或长期阻塞。
- Swift 多线程本质与 OC 一致,但语法更简洁( ̄▽ ̄)~*;
GCD:DispatchQueue.global().async { … },DispatchQueue.main.async { … };
异步 / 同步关键字:async/await(Swift 5.5+),简化异步任务写法,避免回调地狱:
Task {// 子线程执行耗时任务let rpImage = await downloadImage(url: url)// 主线程更新 UIawait MainActor.run {self.imageView.image = rpImage}
}