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)。
  • 任务(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这些优秀的三方库源码;

objectivec 复制代码
// GCD 实现
dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
    // 子线程:执行耗时操作(如下载图片)
    UIImage *image = [self downloadImageWithURL:url];
    
    dispatch_async(dispatch_get_main_queue(), ^{
        // 主线程:更新 UI
        self.imageView.image = image;
    });
});

// NSOperationQueue 实现
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperationWithBlock:^{
    // 子线程:耗时操作
    UIImage *image = [self downloadImageWithURL:url];
    
    [[NSOperationQueue mainQueue] addOperationWithBlock:^{
        // 主线程:更新 UI
        self.imageView.image = image;
    }];
}];

2. 控制并发数:避免线程爆炸

当需要执行大量任务(如批量上传 100 张图片),若无限制并发,会创建过多线程(CPU 切换开销剧增),导致性能下降。用 dispatch_semaphore 控制最大并发数(如同时只传 5 张):

objectivec 复制代码
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0);
dispatch_semaphore_t semaphore = dispatch_semaphore_create(5); // 最大并发数 5

for (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 的依赖机制实现:

objectivec 复制代码
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 保证线程安全且只执行一次:

objectivec 复制代码
+ (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 完成才能执行,形成死锁:
objectivec 复制代码
// 错误示例:主队列同步执行导致死锁
- (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. 共享资源必须加锁

当多个线程操作同一资源(如全局数组、单例属性),必须通过同步机制(锁、信号量)保证原子操作。例如,多线程向数组添加元素,不加锁会导致数组越界或数据丢失:

objectivec 复制代码
// 错误示例:多线程操作数组,线程不安全
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 未运行而不执行:

objectivec 复制代码
// 错误示例:子线程 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+),简化异步任务写法,避免回调地狱:
objectivec 复制代码
Task {
    // 子线程执行耗时任务
    let rpImage = await downloadImage(url: url)
    // 主线程更新 UI
    await MainActor.run {
        self.imageView.image = rpImage
    }
}
相关推荐
tangweiguo030519873 小时前
SwiftUI布局完全指南:从入门到精通
ios·swift
T1an-17 小时前
最右IOS岗一面
ios
坏小虎9 小时前
Expo 快速创建 Android/iOS 应用开发指南
android·ios·rn·expo
光影少年11 小时前
Android和iOS原生开发的基础知识对RN开发的重要性,RN打包发布时原生端需要做哪些配置?
android·前端·react native·react.js·ios
北京自在科技11 小时前
Find My 修复定位 BUG,AirTag 安全再升级
ios·findmy·airtag
Digitally11 小时前
如何不用 USB 线将 iPhone 照片传到电脑?
ios·电脑·iphone
Sim14801 天前
iPhone将内置本地大模型,手机端AI实现0 token成本时代来临?
人工智能·ios·智能手机·iphone
Digitally1 天前
如何将 iPad 上的照片传输到 U 盘(4 种解决方案)
ios·ipad
报错小能手1 天前
ios开发方向——swift并发进阶核心 @MainActor 与 DispatchQueue.main 解析
开发语言·ios·swift
LcGero1 天前
Cocos Creator 业务与原生通信详解
android·ios·cocos creator·游戏开发·jsb