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
    }
}
相关推荐
AirDroid_cn3 小时前
在 iOS 18 中,控制中心怎样添加应用快捷方式?
macos·ios·cocoa
RollingPin3 小时前
iOS八股文之 内存管理
ios·内存管理·内存泄漏·ios面试·arc·runloop·引用计数
2501_915106329 小时前
iOS 26 APP 性能测试实战攻略:多工具组合辅助方案
android·macos·ios·小程序·uni-app·cocoa·iphone
开开心心loky9 小时前
[iOS] KVC 学习
学习·ios·objective-c·cocoa
00后程序员张18 小时前
iOS混淆与IPA文件加固全流程实战 防止苹果应用被反编译的工程级方案
android·ios·小程序·https·uni-app·iphone·webview
胖虎118 小时前
iOS 推送证书 P8 介绍及生成流程
ios·个推·p8证书·极光推送·ios推送
白熊18819 小时前
【图像大模型】ms-swift 深度解析:一站式多模态大模型微调与部署框架的全流程使用指南
开发语言·ios·swift
2501_9151063219 小时前
iOS 应用加固与苹果软件混淆指南,如何防止 IPA 被反编译与二次打包?
android·ios·小程序·https·uni-app·iphone·webview
用户347475478332820 小时前
把SwiftUI View 转为图片
ios·swiftui