runloop

2019-12-24  本文已影响0人  古月行云

一、Runloop 简介

1. 简介

2. 基本作用

3. API

OSX / iOS 系统中,有2套API来访问和使用 RunLoop

NSRunLoopCFRunLoopRef 都代表着 RunLoop 对象。

4. 存在价值

main 函数中的 RunLoop (主运行循环):第14行代码的 UIApplicationMain 函数内部就启动了一个 RunLoop。所以 UIApplicationMain 函数一直没有返回,保持了程序的持续运行。这个默认启动的 RunLoop 是跟主线程相关联的。

image.png

二、Runloop 解析

1. Runloop 运行模式

一种 Runloop 运行模式就是一个要监控的 Input 和 Timer 事件源的集合或者是一个要通知的 Runloop 观察者的集合。每次运 行Runloop,都要指定一个运行模式(显示地或者隐式地)。在 Runloop 的运行期间,只有和当前运行模式相关的源才能被监控和允许发送事件。相似的,只有和当前运行模式相关的观察者才会被通知 Runloop 的行为。和其他模式相关的源会保留新的事件直到 Runloop 运行在了合适的模式才会分发。

在我们的代码中,我们可以通过字符串来标识模式。Cocoa和Core Foundation定义了一个默认模式和几个普通的有用的模式,这些模式都是用字符串来标识的。我们可以用一个字符串当做名字来自定义一个模式,虽然我们自定义模式的名字是随意的,但是模式的内容不是随意的,在我们自己创建的要用的模式中至少要添加一个 Input 源、 Timer 源或者 Runloop 观察者。

在 Runloop 的特殊阶段我们可是使用运行模式来过滤我们不想要的源的事件,大多数的情况下,Runloop 都运行在系统提供的默认模式下,然而 Model Panel 可能运行在“模式”模式,当运行在这个模式期间,只有和这个模式相关的事件源才会发送事件到我们的线程。对于第二线程来说,我们通常使用自定义模式来阻止低优先级的事件源在其他关键处理的时间内发送事件。

注意:运行模式不是根据事件类型划分的,而是根据事件源划分的。我们不能通过模式来匹配鼠标按下事件或者键盘事件,但是我们可以用运行模式来监听一组不同的Port、暂时挂起Timers或者改变当前被监控的事件源和Runloop观察者。

下面列举了一些Cocoa和Core Foundation定义的标准模式:

2. Runloop 处理逻辑

Runloop接收来自两种源的事件:

  1. 输入源(Input sources):传递异步消息,通常来自于其他线程或者程序。
  2. 定时源(Timer sources):传递同步消息,在设定好的时间或者循环间断地发生的事件。

这两种事件源都是使用应用指定的事件处理方法来处理到达的事件。

下面的图显示了Runloop和事件源的概念结构。 Input sources异步的分发事件到响应的处理器,然后引起runUntilDate:(由线程相关的Runloop对象调用)方法退出。 Timer sources同步分发事件到相应的处理器但是不会引起Runloop退出。

1.png 2.png 3.png

备注:

3. Input Sources

Input Sources 异步地分发事件到线程。大概有两种类型的 Input Sources,Port-based类型的输入源监控着应用的Mach端口,自定义的输入源监控着自定义的事件源。NSRunloop不关心输入源的类型。两种输入源唯一的不同是输入源的触发方式,Port-based输入源是由系统内核触发的,而自定义的输入源要我们自己触发。创建输入源的时候我们就给给输入源添加指定的模式。下面是一些输入源:

4. Timer Sources

Timer Sources 同步地在将来的一个确定的时间分发事件到我们的线程。Timers 可以让线程通知自己去处理一些事情。Timers 不是一个实时的机制,当 Timers 触发的时候 NSrunloop 刚好正在执行处理函数,Timer s会等待 NSRunloop 调用自己的处理函数。

Timers 可以创建一次性的和重复性的事件,当创建重复性的事件的时候,Timers 只会根据规划好的触发时间来重新规划触发时间,而不是根据确切的触发时间。而且由于延迟触发丢失了几次触发的话,Timers 只会补充一次触发。

5. NSRunloop 观察者

不像是事件源一样在事件触发的时候执行处理函数。NSRunloop 观察者是在 NSRunloop 几个执行的特定的点触发。NSRunloop 可以观察的几个事件是:

创建观察者的方法是 CFRunLoopObserverRef,我们可以通 过Core Foundation 方法添加到指定的 NSRunloop。观察者也可以创建一次性的和重复性的。一次性的观察者触发之后就会从 NSRunloo p中删除。

三、RunLoop 相关类

Core Foundation 中关于 RunLoop 的5个类

1. CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的运行模式:一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个 Source/Timer/Observer
每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode 如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入 这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

5.png

系统默认注册了5个Mode:(前两个跟最后一个常用)

2. CFRunLoopSourceRef 事件源(输入源)

按照官方文档的分类:

按照函数调用栈的分类

函数调用栈


6.png

3. CFRunLoopTimerRef

CFRunLoopTimerRef 是基于时间的触发器,基本上说的就是 NSTimer (CADisplayLink 也是加到 RunLoop),它受 RunLoop 的 Mode 影响。
GCD的定时器不受 RunLoop 的 Mode 影响。

4. CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变 可以监听的时间点有以下几个


8.png

使用

 - (void)observer {
     // 创建observer
     CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
         NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
     });
     // 添加观察者:监听RunLoop的状态
     CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
     // 释放Observer
     CFRelease(observer);
 }
特别注意
 /*
     CF的内存管理(Core Foundation)
     1.凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
     * 比如CFRunLoopObserverCreate
     2.release函数:CFRelease(对象);
  */
复制代码

四、runloop应用

1. NSTimer (最常见RunLoop使用)

场景还原:拖拽时模式由 NSDefaultRunLoopMode 进入 UITrackingRunLoopMode ,NSTimer 不再响应图片停止轮播,将计时器改成 NSRunLoopCommonModes 模式下两种模式都可运行。

 - (void)timer {
     NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
     // 定时器只运行在NSDefaultRunLoopMode下,一旦RunLoop进入其他模式,这个定时器就不会工作
     // [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
     // 定时器只运行在UITrackingRunLoopMode下,一旦RunLoop进入其他模式,这个定时器就不会工作
     // [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
     // 定时器会跑在标记为common modes的模式下
     // 标记为common modes的模式:UITrackingRunLoopMode和NSDefaultRunLoopMode兼容
     [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
 }
 - (void)timer2 {
     // 调用了scheduledTimer返回的定时器,已经自动被添加到当前runLoop中,而且是NSDefaultRunLoopMode
     NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
     // 修改模式
     [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
 }
复制代码

2. ImageView

需求:当用户在拖拽时(UI交互时)不显示图片,拖拽完成时显示图片

3. 常驻线程 (重要)

应用场景: 经常在后台进行耗时操作,如:监控联网状态,扫描沙盒等 不希望线程处理完事件就销毁,保持常驻状态

4. 自动释放池

在休眠前(kCFRunLoopBeforeWaiting)进行释放,处理事件前创建释放池,中间创建的对象会放入释放池。
特别注意:在启动 RunLoop 之前建议用 @autoreleasepool {...} 包裹。
意义:创建一个大释放池,释放 {} 期间创建的临时对象,一般好的框架的作者都会这么做。

7.png
- (void)execute {
     @autoreleasepool {
         NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
         [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
         [[NSRunLoop currentRunLoop] run];
}
复制代码

5. 补充: GCD定时器

一般的NSTimer定时器因为受到RunLoop,会存在时间不准时的情况。 上文有提到GCD不受RunLoop影响,下面简单的说一下它的使用

 /** 定时器(这里不用带*,因为 dispatch_source_t 就是个类,内部已经包含了*) */
 @property (nonatomic, strong) dispatch_source_t timer;
 int count = 0;
 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
     // 获得队列
     // dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
     dispatch_queue_t queue = dispatch_get_main_queue();
     // 创建一个定时器(dispatch_source_t本质还是个OC对象)
     self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
     // 设置定时器的各种属性(几时开始任务,每隔多长时间执行一次)
     // GCD的时间参数,一般是纳秒 NSEC_PER_SEC(1秒 == 10的9次方纳秒)
     // 何时开始执行第一个任务
     // dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC) 比当前时间晚3秒
     dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
     uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
     dispatch_source_set_timer(self.timer, start, interval, 0);
     // 设置回调
     dispatch_source_set_event_handler(self.timer, ^{
         NSLog(@"------------%@", [NSThread currentThread]);
         count++;
 //        if (count == 4) {
 //            // 取消定时器
 //            dispatch_cancel(self.timer);
 //            self.timer = nil;
 //        }
     });
     // 启动定时器
     dispatch_resume(self.timer);
 }
复制代码

五、runloop 与线程

每条线程都有唯一的一个与之对应的 RunLoop 对象;
主线程的 RunLoop 已经自动创建好了,子线程的RunLoop需要主动创建;
RunLoop在第一次获取时创建,在线程结束时销毁;

六、RunLoop 面试题

  1. 什么是RunLoop?

    • 其实它内部就是do-while循环,在这个循环内部不断的处理各种任务(比如Source、Timer、Observer)。
    • 一个线程对应一个RunLoop,主线程的RunLoop默认已经启动,子线程的RunLoop需要手动启动(调用run方法) 。
    • RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Soure、Timer、Observer,那么就直接退出RunLoop。
  2. 在开发中如何使用RunLoop?什么应用场景?

    • 开启一个常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)
    • 在子线程中开启一个定时器
    • 在子线程中进行一些长期监控
    • 可以控制定时器在特定模式下执行
    • 可以让某些事件(行为、任务)在特定模式下执行
    • 可以添加 Observer 监听 RunLoop 的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)
  3. 在异步线程中下载很多图片。如果失败了,该如何处理?请结合runloop来谈谈解决方案?
    答:(提示:在异步线程中启动一个runloop重新发送网络图片)
    (1)重新下载图片
    (2)利用 runloop 的输入源回到主线程刷新 UIImageView。

转自:https://juejin.im/post/5cb745dde51d456e6479b463

上一篇下一篇

猜你喜欢

热点阅读