iOS Runloop

1、Runloop 简介

1. 简介

  • RunLoop就是让线程随时处理事件但不退出的机制
  • 每个线程都有一个RunLoop
  • RunLoop 实际上就是一个对象,这个对象管理了其须要处理的事件(好比button的点击、各类手势的的事件、定时器、tableView的代理方法)和消息,是iOS里的一种事件处理机制。
  • 线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(好比传入 quit 的消息),函数返回。

2. 基本做用

  • 保持程序的持续运行(好比主运行循环)
  • 处理App中的各类事件(好比触摸事件、定时器事件、Selector事件)
  • 节省CPU资源,提升程序性能:该作事时作事,该休息时休息

3. API

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

  • CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,全部这些 API 都是线程安全的。
  • NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,可是这些 API 不是线程安全的。因此要了解 RunLoop 内部结构,须要多研究 CFRunLoopRef 层面的API(Core Foundation 层面)

NSRunLoopCFRunLoopRef 都表明着 RunLoop 对象。git

4. 存在价值

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

image.jpeg

2、Runloop 解析

1. Runloop 运行模式

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

在咱们的代码中,咱们能够经过字符串来标识模式。Cocoa和Core Foundation定义了一个默认模式和几个普通的有用的模式,这些模式都是用字符串来标识的。咱们能够用一个字符串当作名字来自定义一个模式,虽然咱们自定义模式的名字是随意的,可是模式的内容不是随意的,在咱们本身建立的要用的模式中至少要添加一个 Input 源、 Timer 源或者 Runloop 观察者。面试

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

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

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

  • NSDefaultRunLoopMode:默认的运行模式,用于大部分操做,除了NSConnection对象事件。
  • NSConnectionReplyMode:用来监控NSConnection对象的回复的,不多可以用到。
  • NSModalPanelRunLoopMode:用于标明和Mode Panel相关的事件。
  • NSEventTrackingRunLoopMode:用于跟踪触摸事件触发的模式(例如UIScrollView上下滚动)。
  • NSRunLoopCommonModes:是一个模式集合,当绑定一个事件源到这个模式集合的时候就至关于绑定到了集合内的每个模式。Cocoa 应用默认包含 Default、Panel、Event Tracking 模式,Core Foundation 只包含 Default 模式,咱们能够经过 CFRunLoopAddCommonMode 添加模式。

2. Runloop 处理逻辑

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

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

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

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

RunLoop处理逻辑1-官方.png

RunLoop处理逻辑2-官方.png

RunLoop处理逻辑3-网友整理.png

备注:

  • 输入源:每个须要Runloop处理事件的对象都有一个输入源(InputSource),而且把这个输入源添加到Runloop里,每产生一个事件(好比用户作了一个手势、点了一个button、滑动了一下tableview、定时器到时)就把这个事件放到对应的输入源。Runloop运行时循环检查每个输入源是否有事件须要处理,若是有事件要处理Runloop就就调用这个事件的处理方法(经过addTargetxxx指定的方法或者是代理的方法)。若是Runloop里全部的输入源都没有事件要处理,Runloop会休眠。若是Runloop里一个输入源都没有(对象销毁前会把它以前添加的那个输入源取消),Runloop(runUntilDate:这个方法)就退出来了。
  • 除了处理输入源的事件,Runloop也会生成Runloop行为的通知。注册Runloop的观察者能够收到这些消息,而后在线程内用他们作一些额外的处理。咱们只能使用Core Foundation接口来注册线程的Runloop观察者

3. Input Sources

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

  • Port-Based Sources
    Cocoa 和 Core Foundation 提供了类和接口用来建立 Port-Based 源,Cocoa 只要建立 NSPort 对象,并添加到 NSRunloop 中就能够啦,NSPort负责输入源的建立和配置。Core Foundation 须要手动的常见 port 和输入源。

  • Custom Input Sources
    咱们要用到CFRunLoopSourceRef函数建立输入源,并定义几个回调函数用于配置输入源、处理事件和删除输入源。事件的触发机制要咱们本身定义。

  • Cocoa Perform Selector Sources
    Cocoa定义了能够在任何线程上执行方法的事件源,在想要执行的线程上执行方法是顺序执行的,避免了多个方法在线程上执行的同步问题。Perform Selector Sources在方法执行完以后就会本身从NSRunloop中删除。
    Perform Selector Sources要求目标线程的NSRunloop必须是运行的,主线程默认是运行的。NSRunloop在一次迭代过程当中会处理全部的Perform Selector调用,而不是一次迭代处理一个Perform Selector调用。NSObject中定义的Perform Selector方法以下

    • performSelectorOnMainThread:withObject:waitUntilDone:
    • performSelectorOnMainThread:withObject:waitUntilDone:modes:
    • performSelector:onThread:withObject:waitUntilDone:
    • performSelector:onThread:withObject:waitUntilDone:modes:
    • performSelector:withObject:afterDelay:
    • performSelector:withObject:afterDelay:inModes:
    • cancelPreviousPerformRequestsWithTarget:
    • cancelPreviousPerformRequestsWithTarget:selector:object:

    延迟执行是在NSRunloop的下一次迭代中过了指定的延迟事件才执行。取消操做是针对延迟执行方法的。

4. Timer Sources

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

Timers 能够建立一次性的和重复性的事件,当建立重复性的事件的时候,Timers 只会根据规划好的触发时间来从新规划触发时间,而不是根据确切的触发时间。并且因为延迟触发丢失了几回触发的话,Timers 只会补充一次触发。

5. NSRunloop 观察者

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

  • 进入 NSRunloop
  • NSRunloop 将要处理 Timer 事件
  • NSRunloop 将要处理 Input 事件
  • NSRunloop 将要进入睡眠
  • NSRunloop 被唤醒,可是是在处理事件以前
  • 退出 NSRunloop

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

3、RunLoop 相关类

Core Foundation 中关于 RunLoop 的5个类

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef 注:RunLoop 若是没有这些东西会直接退出

1. CFRunLoopModeRef

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

1465700097876160.jpg

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

  • kCFRunLoopDefaultMode:App的默认Mode,一般主线程是在这个Mode下运行
  • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其余 Mode 影响
  • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就再也不使用
  • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,一般用不到
  • kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode

2. CFRunLoopSourceRef 事件源(输入源)

按照官方文档的分类:

  • Port-Based Sources (基于端口,跟其余线程交互,经过内核发布的消息)
  • Custom Input Sources (自定义)
  • Cocoa Perform Selector Sources (performSelector...方法)

按照函数调用栈的分类

  • Source0:非基于Port的,event事件,只含有回调,须要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,而后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop。
  • Source1:基于Port的,包含了一个 mach_port 和一个回调,被用于经过内核和其余线程相互发送消息,能主动唤醒 RunLoop 的线程。

函数调用栈

函数调用栈.png

3. CFRunLoopTimerRef

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

4. CFRunLoopObserverRef

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

可监听状态.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(对象);
  */
复制代码

4、runloop应用

  • NSTimer
  • PerformSelector
  • ImageView显示
  • 须要让线程执行周期性的工做(常驻线程)
  • 自动释放池
  • 须要使用 Port 或者自定义 Input Source 与其余线程进行通信
  • NSURLConnection 在子线程中发起异步请求

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交互时)不显示图片,拖拽完成时显示图片

  • 方法1 监听UIScrollerView滚动 (经过UIScrollViewDelegate监听,此处再也不举例)
  • 方法2 RunLoop 设置运行模式
    // 只在NSDefaultRunLoopMode模式下显示图片
    // inModes:设置运行模式
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
    复制代码

3. 常驻线程 (重要)

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

  • 第一种(推荐)
    开启
    - (void)run {
       //addPort:添加端口(就是source)  forMode:设置模式
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
       //启动RunLoop
         [[NSRunLoop currentRunLoop] run];
      /*
       //另外两种启动方式
         [NSDate distantFuture]:遥远的将来  这种写法跟上面的run是一个意思
         [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
         不设置模式
         [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
       */
     }
    复制代码
    退出-退出当前线程
    [NSThread exit];
    复制代码
  • 第二种(奇葩法)
    优势:退出RunLoop比较方便-定义个标记 while(flag){...}
    - (void)run {
         while (1) {
             [[NSRunLoop currentRunLoop] run];
         }
     }
    复制代码

4. 自动释放池

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

屏幕快照 2018-10-04 14.40.44.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);
 }
复制代码

5、runloop 与线程

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

  • 获取RunLoop对象

    // 工做线程 须要程序员手工写代码让runloop运行起来
    [NSRunLoop currentLoop]runUntilDate:]
    // Foundation
    [NSRunLoop currentRunLoop]; // 得到当前线程的RunLoop对象
    [NSRunLoop mainRunLoop]; // 得到主线程的RunLoop对象
    // Core Foundation
    CFRunLoopGetCurrent(); // 得到当前线程的RunLoop对象
    CFRunLoopGetMain(); // 得到主线程的RunLoop对象
    复制代码
  • 线程安全性
    基于 Cocoa 的接口不是线程安全的,基于 Core Foundation 的接口是线程安全的。

6、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。

相关连接

苹果官方文档
CFRunLoop官方文档
NSRunLoop官方文档
CFRunLoopRef
NSRunloop的使用

相关文章
相关标签/搜索