GeekBand - iOS 多线程和RunLoop 总结

iOS 开发高级进阶 第三周 多线程 Runloop

iOS 多线程以及 RunLoop 学习总结

基础知识

什么是进程?

  • 进程是指在系统中正在运行的一个应用程序
  • 每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内

什么是线程?

  • 1个进程要想执行任务,必须得有线程(每1个进程至少要有1条线程)
  • 一个进程(程序)的所有任务都在线程中执行

多线程的原理:

  • 同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)
  • 多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)
  • 如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象

什么是主线程?

  • 一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”
  • 主线程主要用于显示\刷新UI界面和处理UI事件(比如点击事件、滚动事件、拖拽事件等)
  • 使用主线程的时候需要注意:别将比较耗时的操作放到主线程中;耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验

多线程的方案:

GeekBand - iOS 多线程和RunLoop 总结_第1张图片

多线程

在 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();

各种队列的执行效果:


GeekBand - iOS 多线程和RunLoop 总结_第2张图片
GCD
线程间通信示例:

从子线程回到主线程

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
GeekBand - iOS 多线程和RunLoop 总结_第3张图片
RunLoop

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的状态改变

可以监听的时间点有以下几个


GeekBand - iOS 多线程和RunLoop 总结_第4张图片
runloop1

RunLoop的处理流程

GeekBand - iOS 多线程和RunLoop 总结_第5张图片
GeekBand - iOS 多线程和RunLoop 总结_第6张图片

你可能感兴趣的:(GeekBand - iOS 多线程和RunLoop 总结)