iOS 多线程:RunLoop详细总结

RunLoop

文章目录

  1. RunLoop 简介
    1.1 什么是 RunLoop?
    1.2 RunLoop 和线程
    1.3 默认情况下主线程的 RunLoop 原理
  2. RunLoop 相关类
    2.1 CFRunLoopRef
    2.2 CFRunLoopModeRef
    2.3 CFRunLoopTimerRef
    2.4 CFRunLoopSourceRef
    2.5 CFRunLoopObserverRef
  3. RunLoop 原理
  4. RunLoop 实战应用
    4.1 NSTimer 的使用
    4.2 ImageView 推迟显示
    4.3 后台常驻线程(很常用)
    本文项目连接地址

1. RunLoop 简介

1.1 什么是 RunLoop?

根据字面意思:Run 表示运行,Loop 表示循环,结合在一起就是运行的循环。

RunLoop 实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如触摸事件,UI 刷新事件,定时器事件,selector 事件),从而保持程序的持续运行;而且在没有事件处理的时候,会进入睡眠模式,从而节省 CPU 资源,提高程序性能。

1.2 RunLoop 和线程

RunLoop 和线程息息相关,我们知道线程的作用是用来执行特定的一个或多个任务,但是在默认情况下,线程执行完之后就会退出,就不能再执行任务了。这时我们就需要采用一种方式来让线程能够处理任务,并不退出。所以就有了 RunLoop。

  1. 一条线程对应一个 RunLoop 对象,每条线程都有唯一一个与之对应的 RunLoop 对象。
  2. 我们只能在当前线程中操作当前线程的 RunLoop,而不能去操作其他线程的 RunLoop。
  3. RunLoop 对象在第一次获取 RunLoop 时创建,销毁则是在线程结束的时候。
  4. 主线程的 RunLoop 对象,系统自动帮我们创建好了(原理如下)。而子线程的 RunLoop 对象需要我们主动创建.
1.3 默认情况下主线程的 RunLoop 原理

我们再开启一个 iOS 程序的时候,系统会调用创建项目时自动生成的 main.m 文件。main.m 文件如下所示:

int main(int argc, char * argv[])
{
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

其中UIApplicationMain函数内部帮我们开启了主线程RunLoop,UIApplicationMain内部拥有一个无线循环的代码。上边的代码中开启RunLoop 的过程可以简单的理解为如下代码:

int main(int argc, char * argv[]) {        
    BOOL running = YES;
    do {
        // 执行各种任务,处理各种事件
        // ......
    } while (running);

    return 0;
}

从上边可以看出,程序一直在do-while 循环中执行,所以UIApplicationMain 函数一直没有返回,我们在运行程序之后,程序不会马上退出,会保持持续运行状态。

下图是苹果官方给出的 RunLoop 模型图

RunLoop 模型图.png

从上图可以看出,RunLoop 就是线程中的一个循环,RunLoop 在循环中会不断检测,通过input sources(输入源)和Timer sources(定时源)两种来源等待接受事件;然后对接受到的事件通知线程进行处理,并在没有事件的时候进行休息。

2. RunLoop 相关类

下面我们来了解一下 Core Founction 框架下关于 RunLoop 的5个类,只有弄懂这几个类的含义,我们才能深入了解 RunLoop 运行机制。

  1. CFRunLoopRef:代表 RunLoop 的对象
  2. CFRunLoopModeRef:RunLoop 的运行模式
  3. CFRunLoopSourceRef:就是上图提到的输入源、事件源
  4. CFRunLoopTimerRef:就是上图提到的定时源
  5. CFRunLoopObserverRef:观察者,可以监听 RunLoop 的状态改变

接下来详细讲解这几个类的具体含义和关系

RunLoop 相关类关系图.png

接着介绍这几个类的相互关系
一个 RunLoop 对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef),定时源(CFRunLoopTimerRef),观察者(CFRunLoopObserverRef)。

  • 每次 RunLoop 启动时,只能指定其中一个运行模式CFRunLoopModeRef,这个运行模式CFRunLoopModeRef被称为 CurrentMode。
  • 如果需要切换运行模式CFRunLoopModeRef,只能退出 Loop,再重新指定一个运行模式CFRunLoopModeRef进入。
  • 这样做主要是为了分隔开不同组的输入源CFRunLoopSourceRef,定时源CFRunLoopTimerRef,观察者CFRunLoopObserverRef,让其互不影响。

下面我们来详细讲解这五个类:

2.1 CFRunLoopRef

CFRunLoopRef就是 Core Foundation 框架下 RunLoop 对象类,我们可以通过以下方式来获取 RunLoop 对象:

  • Core Foundation
    • CFRunLoopGetCurrent() 获得当前线程的 RunLoop 对象
    • CFRunLoopGetMain() 获得主线程的 RunLoop 对象
      在 Foundation 框架下获取 RunLoop 对象类的方法如下
  • Foundation
    • [NSRunLoop currentRunLoop] 获得当前线程的RunLoop对象
    • [NSRunLoop mainRunLoop] 获得主线程的RunLoop对象
2.2 CFRunLoopModeRef

系统默认定义了多种运行模式(CFRunLoopModeRef),如下所示

  • kCFRunLoopDefaultMode App 的默认运行模式,通常主线程是在这个运行模式下运行的
  • UITrackingRunLoopMode 跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响)
  • UIInitializationRunLoopMode 在刚启动 App 时首次进入的第一个 Mode,启动完成后就不再使用
  • GSEventReceiveRunLoopMode 接受系统内部事件,通常用不到
  • kCFRunLoopCommonModes 伪模式,不是一种真正的运行模式

其中kCFRunLoopDefaultModeUITrackingRunLoopModekCFRunLoopCommonModes是我们开发中需要用到的模式。

2.3 CFRunLoopTimerRef

CFRunLoopTimerRef是定时源,理解即为基于时间的触发器,可以将其理解为定时器。

下面我们来演示CFRunLoopModeRefCFRunLoopTimerRef结合的使用用法,从而加深理解。

  1. 往视图中添加一个 Text View。
  2. 添加一个定时器,每隔2秒控制台输出打印。
- (void)setupTimer {
    // 定义一个定时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    // 添加到 runloop
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
  1. 然后运行,这个时候如果我们没有任何操作的话,定时器会稳定的每隔2秒调用 run 方法打印。
  2. 但是当我们拖动 Text View 滚动时,发现 run 方法不打印了,即 NSTimer 不工作了。当松开鼠标后,NSTimer 又开始正常工作了。

这是因为:

  • 当我们不做任何操作的时候,RunLoop 处于NSDefaultRunLoopMode下。
  • 当我们拖动 Text View 的时候,RunLoop 就会结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这个模式没有添加 NSTimer,所以我们的 NSTimer 就不工作了。
  • 当我们松开鼠标的时候,RunLoop 就结束UITrackingRunLoopMode,又切换回```NSDefaultRunLoopMode模式,所以 NSTimer 就又开始正常工作了。

你可以将 NSTimer 添加到UITrackingRunLoopMode下,会发现定时器只会在拖动 Text View 的模式下工作,而不做操作的时候定时器不工作。

- (void)setupTimer {
    // 定义一个定时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    // 添加到 runloop
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
}

然道我们就不能在这两种模式都让NSTimer 都能正常工作吗?

当然可以,这就用到了我们之前说的伪模式 (kCFRunLoopCommonModes),这其实不是一种真实的模式,而是一种标记模式,意思就是可以在打上Common Modes 标记的模式下运行。

那么哪些模式被标记上了 Common Modes 呢?

NSDefaultRunLoopModeUITrackingRunLoopMode

所以我们只要将 NSTimer 添加到当前 RunLoop 的kCFRunLoopCommonModes(Foundation 框架下为NSRunLoopCommonModes)下,就可以让 NSTimer 在不做操作和拖动 Text View 两种情况下正常工作了.

- (void)setupTimer {
    // 定义一个定时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    // 添加到 runloop
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

既然说到 NSTimer,我们再讲下NSTimer 中的scheduledTimerWithTimeInterval方法和 RunLoop 的关系。代码如下

NSTimer *timer1 = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

这句代码调用了scheduledTimer 返回的定时器,NSTimer 会自动被加入到RunLoop 的NSDefaultRunLoopMode模式下。这句代码相当于下面两句代码。

// 定义一个定时器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 添加到 runloop
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
2.4 CFRunLoopSourceRef

CFRunLoopSourceRef是事件源,(上图中有提到过),CFRunLoopSourceRef有两种分类方法。

  • 第一种按照官方文档来分类(就像 RunLoop 模型图中的那样)

    • Port-Based Sources(基于端口)
    • Custom Input Source(自定义)
    • Cocoa Perform Selector Sources
  • 第二种按照函数调用栈来分类:

    • Source0:非基于 Port
    • Source1:基于 Port,通过内核和其他线程通信,接收,分发系统事件

这两种分类方式其实没有什么区别,只不过第一种是通过官方理论来分类,第二种是在实际应用中通过调用函数来分类。

下面举个例子来了解一下函数调用栈和 Source

  1. 在视图中添加一个按钮,并且在调用方法中输出语句,在执行过程中打上断点,然后查看对应堆栈信息。

查看堆栈信息

函数调用栈.png

所以点击事件时这样来的:
1.首先程序启动,调用16行的 main 函数,main 函数调用15行的UIApplicationMain函数,然后一直往上调用函数,最终调用到0行的BtnClick 函数,即点函数。

  1. 同时可以看到11行中有 Source0,即说点击事件属于 Sources0函数,点击事件时在 Sources0中处理的。
  2. 至于 Sources1,则是用来接收,分发系统事件,然后再分发到 Sources0中处理的.
2.5 CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,用来监听 RunLoop 的状态改变

CFRunLoopObserverRef可以监听的状态改变有以下几种:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),               // 即将进入Loop:1
    kCFRunLoopBeforeTimers = (1UL << 1),        // 即将处理Timer:2    
    kCFRunLoopBeforeSources = (1UL << 2),       // 即将处理Source:4
    kCFRunLoopBeforeWaiting = (1UL << 5),       // 即将进入休眠:32
    kCFRunLoopAfterWaiting = (1UL << 6),        // 即将从休眠中唤醒:64
    kCFRunLoopExit = (1UL << 7),                // 即将从Loop中退出:128
    kCFRunLoopAllActivities = 0x0FFFFFFFU       // 监听全部状态改变  
};

下面通过代码来监听 RunLoop中的状态改变

// 添加 RunLoop 监听
- (void)addObserver {
    // 创建观察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"监听到RunLoop发生改变---%zd",activity);
    });
    // 添加观察者到当前的 RunLoop 中
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    // 释放 observer, 最后添加完需要释放掉
    CFRelease(observer);
}

运行结果

image.png

可以看到RunLoop 的状态不断的改变,最终变成了状态32,也就是即将进入睡眠状态,说明 RunLoop 之后就会进入睡眠状态

3. RunLoop 原理

了解完5个分类,我们就可以来理解 RunLoop 的运行逻辑了

RunLoop 运行逻辑图.png

接下来说下官方文档给我们的 RunLoop 逻辑

在每次运行开启 RunLoop 的时候,所在线程的 RunLoop 会自动处理之前未处理的事件,并且通知相关的观察者。

具体顺序如下:

  1. 通知观察者 RunLoop 已经启动
  2. 通知观察者即将要开始的定时器
  3. 通知观察者任何即将启动的非基于端口的源
  4. 启动任何准备好的非基于端口的源
  5. 如果基于端口的源准备好并处于等待状态,立即启动,并进入步骤9
  6. 通知观察者线程进入休眠状态
  7. 将线程置于休眠,知道任一下面的事件发生:
    • 某一事件到达基于端口的源
    • 定时器启动
    • RunLoop 设置的时间已经超时
    • RunLoop 被显示唤醒
  8. 通知观察者线程将被唤醒
  9. 处理未处理的事件
    • 如果用户定义的定时器启动,处理定时器事件并重启 RunLoop,进入步骤2
    • 如果输入源启动,传递相应的消息
    • 如果 RunLoop 被显示唤醒而且时间还没超时,重启 RunLoop,进入步骤2
  10. 通知观察者 RunLoop 结束

4. RunLoop 实战应用

光懂原理没啥用,能够实战应用才是王道,下面讲讲 RunLoop 的几种应用

4.1 NSTimer 的使用(详细见2.3CFRunLoopTimerRef)
4.2ImageView 推迟显示

利用performSelector方法为 UIImageView 调用setImage:方法,并利用inModes将其设置为 RunLoop 下NSDefaultRunLoopMode运行模式,代码如下
1.添加图片视图和 text view 视图

- (void)drawImageView {
    _imgView = [[UIImageView alloc] initWithFrame:CGRectMake(50, 50, 200, 200)];
    [self.view addSubview:_imgView];
}
  1. touchsBegan方法中调用赋值图片方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [_imgView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"scenery"] afterDelay:4.0 inModes:@[NSDefaultRunLoopMode]];
}

4.运行程序,然后拖动 UITextView,拖动4秒以上,发现过来4秒后,UIImageView 还没有显示图片,当我们松开的时候,图片显示了。

image.png

这样我们就实现了在拖动完之后,再延时显示 UIImaeView 了。

4.3 后台常驻线程(很常用)

我们再开发应用程序的过程中,如果后台操作特别频繁,经常会在子线程做一些耗时操作(下载文件,后台播放音乐等),我们最好能让这条线程永远常驻内存。

做法如下:
添加一条用于常驻内存的强引用的子线程,在该线程的 RunLoop 下添加一个 Source,开启 RunLoop。

具体操作如下:
1.添加一条强引用的 thread 线程属性。

/** 线程 */
@property(nonatomic, strong)NSThread *thread;

2.创建线程平启动方法

- (void)run1 {
    // 执行任务
    NSLog(@"---run---");
    
    // 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    
    // 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
    NSLog(@"未开启RunLoop");
}

- (void)createThread {
    // 创建线程
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
    // 开启线程
    [self.thread start];
}

运行结果

image.png

这样我们就开启了一条常驻线程,下面我们可以添加其他任务,除了之前创建的时候调用了 run1,还可以在点击的时候调用 run2方法。

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 利用performSelector,在self.thread的线程中调用run2方法执行任务
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void)run2 {
    NSLog(@"---run2---");
}

运行结果

image.png

每当我们点击屏幕的时候,都可以调用---run2---。这样就实现了常驻线程的需求。


本文参考大部分参考iOS多线程:『RunLoop』详尽总结,文章中涉及到的代码也都有实践,经实践完全正确,非常感谢该作者。


iOS多线程详细总结系列文章
iOS GCD之dispatch_semaphore(信号量)
iOS 多线程-GCD 详细总结
iOS 多线程: [NSOperation NSOperationQueue] 详解
iOS 多线程:[pthread,NSThread]详细总结


相关资料参考
深入理解 RunLoop


项目连接地址

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

推荐阅读更多精彩内容