GeekBand - iOS 多线程和RunLoop 总结

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

各种队列的执行效果:


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

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


runloop1

RunLoop的处理流程

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 162,408评论 4 371
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,690评论 2 307
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 112,036评论 0 255
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,726评论 0 221
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,123评论 3 296
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 41,037评论 1 225
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,178评论 2 318
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,964评论 0 213
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,703评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,863评论 2 254
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,333评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,658评论 3 263
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,374评论 3 244
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,195评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,988评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,167评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,970评论 2 279

推荐阅读更多精彩内容