iOS 开发高级进阶 第三周 多线程 Runloop
iOS 多线程以及 RunLoop 学习总结
基础知识
什么是进程?
- 进程是指在系统中正在运行的一个应用程序
- 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内
什么是线程?
- 1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程)
- 一个进程(程序)的所有任务都在线程中执行
多线程的原理:
- 同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)
- 多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)
- 如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
什么是主线程?
- 一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”
- 主线程主要用于显示\刷新UI界面和处理UI事件(比如点击事件、滚动事件、拖拽事件等)
- 使用主线程的时候需要注意:别将比较耗时的操作放到主线程中;耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验
多线程的方案:
多线程
在 iOS 主流的3种多线程方案分别是:
- NSThread
- 比其他两个轻量级
- 需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销
- NSOperation & NSOperationQueue
- 不需要关心线程管理、数据同步的事情,可以把精力放在自己需要执行的操作上
- NSOperation 是一个抽象类,使用它必须用它的子类,可以实现它或者使用它定义好的两个子类:NSInvocationOperation 和 NSBlockOperation
- 创建 NSOperation 子类的对象,把对象添加到 NSOperationQueue 队列里执行
- GCD(Grand Central Dispatch)
- 是苹果为多核的并行运算提出的解决方案,所以会自动合理地利用更多的 CPU 内核,最重要的是它会自动管理线程的生命周期(创建线程、调度任务、销毁线程),完全不需要我们管理。
NSThread
一个NSThread对象就代表一条线程
NSThread 的创建启动等方法
创建、启动线程
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil];
[thread start];
线程一启动,就会在线程thread中执行self的run方法-
主线程相关用法
+ (NSThread *)mainThread; // 获得主线程
- (BOOL)isMainThread; // 是否为主线程
+ (BOOL)isMainThread; // 是否为主线程
获得当前线程
NSThread *current = [NSThread currentThread];
-
线程的名字
- (void)setName:(NSString *)n;
- (NSString *)name;
创建线程后自动启动线程
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
隐式创建并启动线程
[self performSelectorInBackground:@selector(run) withObject:nil];
控制线程状态
- 启动线程
- (void)start; //进入就绪状态 -> 运行状态。当线程任务执行完毕,自动进入死亡状态
- 阻塞(暂停)线程
+ (void)sleepUntilDate:(NSDate*) date;
+ (void)sleepForTimeInterval:(NSTimeInterVal)ti; // 进入阻塞状态
- 强制停止线程
+ (void) exit; // 进入死亡状态
注意: 一旦线程停止(死亡)了,就不能在此开启任务。
当多个线程同时访问一块资源时,很容易引发数据错乱和数据安全的问题,解决的办法是采用互斥锁,@synchronized(锁对象) { // 需要锁定的代码 }
。互斥锁能有效防止因多线程抢夺资源造成的数据安全问题,但是需要消耗大量的 CPU 资源。另外在定义属性时有nonatomic和atomic两种选择。
- atomic:原子属性,为setter方法加锁(默认就是atomic),线程安全,需要消耗大量的资源
- nonatomic:非原子属性,不会为setter方法加锁,适合内存小的移动设备
线程间通:一个线程传递数据给另一个线程,在一个线程中执行完特定任务后,转到另一个线程继续执行任务。
线程间通信常用的方法:
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
GCD
GCD 的使用有两个步骤:
- 定制任务,确定要做的事
- 将任务添加到队列中
- GCD 会自动将对列中的任务取出,放到对应的线程中执行
- 任务的取出遵循对列的FIFO原则,先进先出
执行任务
GCD 中有2个用来执行任务的函数
- 用同步的方式执行任务
dispatch_sync(dispatch_queue_t queue, dispatch_block_t block);
- 用异步的方式执行任务
dispatch_async(dispatch_queue_t queue, dispatch_block_t block);
同步执行和异步执行的区别:
- 同步:只能在当前线程中执行任务,不具备开启新线程的能力
- 异步:可以在新的线程中执行任务,具备开启新线程的能力
对列的类型
GCD 的对列可以分为2大类型
- 并发队列(Concurrent Dispatch Queue)
- 可以让多个任务并发同时执行(自动开启多个线程同时执行)
- 并发功能只有在异步(dispatch_async)函数下才有效
- 串行队列(Serial Dispatch Queue)
- 让任务一个接一个的执行,一个任务执行完毕,在执行另一个
同步、异步、并行队列、串行队列的理解:
- 同步操作会阻塞当前线程并等待 Block 中的任务执行完毕,然后当前线程才会继续执行下去
- 异步操作,当前线程会直接往下执行,它不会阻塞当前线程。
队列 | 同步执行 | 异步执行 |
---|---|---|
串行队列 | 当前线程,一个一个执行 | 其他线程,一个一个执行 |
并行队列 | 当前线程,一个一个执行 | 开很多线程,一起执行 |
并发队列
GCD默认已经提供了全局的并发队列,供整个应用使用,不需要手动创建
使用dispatch_get_global_queue
函数获得全局的并发队列
dispatch_queue_t dispatch_get_global_queue(
dispatch_queue_priority_t priority, // 队列的优先级
unsigned long flags); // 此参数暂时无用,用0即可
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); // 获得全局并发队列
串行队列
GCD中获得串行有2种途径
- 使用
dispatch_queue_create
函数创建串行队列
dispatch_queue_t
dispatch_queue_create(const char *label, // 队列名称
dispatch_queue_attr_t attr); // 队列属性,一般用NULL即可
dispatch_queue_t queue = dispatch_queue_create("cn.itcast.queue", NULL); // 创建
dispatch_release(queue); // 非ARC需要释放手动创建的队列
- 使用主队列(跟主线程相关联的队列)
- 主队列是GCD自带的一种特殊的串行队列
- 放在主队列中的任务,都会放到主线程中执行
- 使用
dispatch_get_main_queue()
获得主队列
dispatch_queue_t queue = dispatch_get_main_queue();
各种队列的执行效果:
线程间通信示例:
从子线程回到主线程
dispatch_async(
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行耗时的异步操作...
dispatch_async(dispatch_get_main_queue(), ^{
// 回到主线程,执行UI刷新操作
});
});
延时执行
- 调用NSObject的方法
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
// 2秒后再调用self的run方法
- 使用GCD函数
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
// 2秒后异步执行这里的代码...
});
一次性代码
使用dispatch_once函数能保证某段代码在程序运行过程中只被执行1次
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
// 只执行1次的代码(这里面默认是线程安全的)
});
队列组
队列组可以分别异步执行2个耗时的操作,等2个异步操作都执行完毕后,再回到主线程执行操作。
dispatch_group_t group = dispatch_group_create();
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行1个耗时的异步操作
});
dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// 执行1个耗时的异步操作
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
// 等前面的异步操作都执行完毕后,回到主线程...
});
NSOperation
NSOperation 和 NSOperationQueue 实现多线程的步骤:
- 先将需要执行的操作封装到一个 NSOperation 对象中
- 然后将NSOperation 对象添加到 NSOperationQueue 中
- 系统会自动将 NSOperationQueue 中的 NSoperation 取出来
- 将取出来的 NSOperation 封装的操作放到一条新线程中执行
NSOperation 的子类
NSOperation是个抽象类,并不具备封装操作的能力,必须使用它的子类
- NSInvocationOperation
- NSBlockOperation
- 自定义子类继承NSOperation,实现内部相应的方法
NSInvocationOperation
创建NSInvocationOperation对象
- (id)initWithTarget:(id)target selector:(SEL)sel object:(id)arg;
调用start方法开始执行操作
- (void)start;
一旦执行操作,就会调用target的sel方法
注意:
- 默认情况下,调用了start方法后并不会开一条新线程去执行操作,而是在当前线程同步执行操作
- 只有将NSOperation放到一个NSOperationQueue中,才会异步执行操作
NSBlockOperation
创建NSBlockOperation对象
+ (id)blockOperationWithBlock:(void (^)(void))block;
通过
addExecutionBlock:
方法添加更多的操作
- (void)addExecutionBlock:(void (^)(void))block;
注意:只要NSBlockOperation封装的操作数 > 1,就会异步执行操作
NSOperationQueue
NSOperationQueue的作用
- NSOperation可以调用start方法来执行任务,但默认是同步执行的
- 将NSOperation添加到NSOperationQueue(操作队列)中,系统会自动异步执行NSOperation中的操作
添加操作到NSOperationQueue中
- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;
最大并发数的相关方法
- (NSInteger)maxConcurrentOperationCount;
- (void)setMaxConcurrentOperationCount:(NSInteger)cnt;
队列的取消、暂停和恢复
取消队列的所有操作
- (void)cancelAllOperations;
提示:也可以调用NSOperation的- (void)cancel
方法取消单个操作暂停和恢复队列
- (void)setSuspended:(BOOL)b; // YES代表暂停队列,NO代表恢复队列
- (BOOL)isSuspended;
操作依赖:
NSOperation之间可以设置依赖来保证执行顺序
比如一定要让操作A执行完后,才能执行操作B,可以这么写
[operationB addDependency:operationA]; // 操作B依赖于操作A
可以监听一个操作的执行完毕
- (void (^)(void))completionBlock;
- (void)setCompletionBlock:(void (^)(void))block;
自定义NSOperation需要重写- (void)main
方法,在里面实现想执行的任务.
RunLoop
- 每条线程都有唯一的一个与之对应的RunLoop对象
- 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建
- RunLoop在第一次获取时创建,在线程结束时销毁
获得 RunLoop 对象
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
Core Foundation中关于RunLoop的5个类
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
CFRunLoopModeRef代表RunLoop的运行模式
- 一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer
- 每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
- 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
- 这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
系统默认注册了5个mode
- kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
- UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
- kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
CFRunLoopSourceRef是事件源(输入源)
- Source0:非基于Port的
- Source1:基于Port的
CFRunLoopTimerRef是基于时间的触发器,基本上说的就是NSTimer
CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
可以监听的时间点有以下几个
RunLoop的处理流程