什么是runloop?
不知道大家有没有想过这个问题,一个应用开始运行以后放在那里,如果不对它进行任何操作,这个应用就像静止了一样,不会自发的有任何动作发生,但是如果我们点击界面上的一个按钮,这个时候就会有对应的按钮响应事件发生。给我们的感觉就像应用一直处于随时待命的状态,在没人操作的时候它一直在休息,在让它干活的时候,它就能立刻响应。其实,这就是run loop的功劳。
RunLoop是一个接收处理异步消息事件的循环,一个循环中:等待事件发生,然后将这个事件送到能处理它的地方。
底层来看:runloop 底层本质就是一个被优化的do…while循环(有事干就循环没事干就休息)。
最牛的地方在“休息”和“唤醒”这个功能。do…white循环堵塞线程,一直执行;而runloop如果没有任务不会堵塞线程,就直接休眠了,下次有任务再唤起,接着干。
那么休息和唤醒是依靠thread 来实现和管理,下图可以看出他们的关系
Runloop 和 线程有什么关系?
线程和runloop 是一一对应:
- CFMutableDictionaryRef 里边保存 k(线程指针)- v(runloop)
- runloop 是由线程创建的
Runloop优点:
Runloop 工作流程:
CFRunLoop对象可以检测某个task或者dispatch的输入事件,当检测到有输入源事件,CFRunLoop将会将其加入到线程中进行处理(这就体现了runloop和thread的关系)。比方说用户输入事件、网络连接事件、周期性或者延时事件、异步的回调等。
Runloop检测事件的类型
可以分为输入源(input source)和定时源(timer source)2种类型(一般情况开发人员不习惯这么分类,习惯下边的分类)。
Run loop接收输入事件来自两种不同的来源:输入源(input source)和定时源(timer source)。两种源都使用程序的某一特定的处理例程来处理到达的事件。图-1显示了run loop的概念结构以及各种源。
需要说明的是,当你创建输入源,你需要将其分配给run loop中的一个或多个模式(什么是模式,下文将会讲到)。模式只会在特定事件影响监听的源。大多数情况下,run loop运行在默认模式下,但是你也可以使其运行在自定义模式。若某一源在当前模式下不被监听,那么任何其生成的消息只在run loop运行在其关联的模式下才会被传递。
1.输入源:
传递异步事件,通常消息来自于其他线程或程序。输入源传递异步消息给相应的处理例程,并调用runUntilDate:方法来退出(在线程里面相关的NSRunLoop对象调用)。
1.1.基于端口的输入源:
基于端口的输入源由内核自动发送。
Cocoa和Core Foundation内置支持使用端口相关的对象和函数来创建的基于端口的源。例如,在Cocoa里面你从来不需要直接创建输源。你只要简单的创建端口对象,并使用NSPort的方法把该端口添加到run loop。端口对象会自己处理创建和配置输入源。
在Core Foundation,你必须人工创建端口和它的run loop源。我们可以使用端口相关的函数(CFMachPortRef,CFMessagePortRef,CFSocketRef)来创建合适的对象。
1.2.自定义输入源:
自定义的输入源需要人工从其他线程发送。
为了创建自定义输入源,必须使用Core Foundation里面的CFRunLoopSourceRef类型相关的函数来创建。你可以使用回调函数来配置自定义输入源。Core Fundation会在配置源的不同地方调用回调函数,处理输入事件,在源从run loop移除的时候清理它。
除了定义在事件到达时自定义输入源的行为,你也必须定义消息传递机制。源的这部分运行在单独的线程里面,并负责在数据等待处理的时候传递数据给源并通知它处理数据。消息传递机制的定义取决于你,但最好不要过于复杂。
1.3.Cocoa上的Selector源
除了基于端口的源,Cocoa定义了自定义输入源,允许你在任何线程执行selector方法。和基于端口的源一样,执行selector请求会在目标线程上序列化,减缓许多在线程上允许多个方法容易引起的同步问题。不像基于端口的源,一个selector执行完后会自动从run loop里面移除。
当在其他线程上面执行selector时,目标线程须有一个活动的run loop。对于你创建的线程,这意味着线程在你显式的启动run loop之前是不会执行selector方法的,而是一直处于休眠状态。
NSObject类提供了类似如下的selector方法:
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)argwaitUntilDone:(BOOL)wait modes:(NSArray *)array;
2.定时源:
定时源在预设的时间点同步方式传递消息,这些消息都会发生在特定时间或者重复的时间间隔。定时源则直接传递消息给处理例程,不会立即退出run loop。
需要注意的是,尽管定时器可以产生基于时间的通知,但它并不是实时机制。和输入源一样,定时器也和你的run loop的特定模式相关。如果定时器所在的模式当前未被run loop监视,那么定时器将不会开始直到run loop运行在相应的模式下。类似的,如果定时器在run loop处理某一事件期间开始,定时器会一直等待直到下次run loop开始相应的处理程序。如果run loop不再运行,那定时器也将永远不启动。
这种是开发常用的分类,分为3种,分别是CFRunLoopSource、CFRunLoopTimer、CFRunLoopObserver。可以通过CFRunLoopAddSource, CFRunLoopAddTimer或者CFRunLoopAddObserver添加相应的事件类型。
要让一个RunLoop跑起来还需要run loop modes,每一个source, timer和observer添加到RunLoop中时必须要与一个模式(CFRunLoopMode)相关联才可以运行。
model 的执行逻辑
model 常见应用
NSDefaultRunLoopMode:默认model,空闲时间处理
NSEventTrackingRunLoopMode:拖动model(主线程)
NSDefaultRunLoopCommonMode:NSDefaultRunLoopMode+ NSEventTrackingRunLoopMode 二者合一
解决NSTimer在ScrollView滑动时不work
[runLoop addTimer :timer forMode : NSDefaultRunLoopCommonMode ]; // 将Timer注册到NSDefaultRunLoopCommonMode,ScrollView滑动主线程runloop处在NSEventTrackingRunLoopMode模式下,默认的NSDefaultRunLoopMode不会触发time回调
如果当前mode里边没有time和souce,那么当前runloop,不会运行
runloop三大罗汉:
time
定时输入observer
观察者
可以观察的时间点有:
1)Entry:即将进入Loop
2)BeforeTimer:即将处理Timer
3)BeforeSource:即将处理Source
4)BeforeWaiting:即将进入休眠
5)AfterWaiting:刚从休眠中唤醒
6)Exit:即将退出Loopsource
• Source1 :基于mach_Port的,来自系统内核或者其他进程或线程的事件,可以主动唤醒休眠中的RunLoop(iOS里进程间通信开发过程中我们一般不主动使用)。mach_port大家就理解成进程间相互发送消息的一种机制就好, 比如屏幕点击, 网络数据的传输都会触发sourse1。
• Source0 :非基于Port的 处理事件,什么叫非基于Port的呢?就是说你这个消息不是其他进程或者内核直接发送给你的。一般是APP内部的事件, 比如hitTest:withEvent的处理, performSelectors的事件.
简单举个例子:一个APP在前台静止着,此时,用户用手指点击了一下APP界面,那么过程就是下面这样的:
我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会被IOKit先包装成Event,通过mach_Port传给正在活跃的APP , Event先告诉source1(mach_port),source1唤醒RunLoop, 然后将事件Event分发给source0,然后由source0来处理。
runloop 调用场景
time 是依赖runloop
子线程的runloop 默认是不开启的(在子线程里边写time,time 不会执行)
那么可以通过控制子线程来控制runloop,进而控制子线程里边的time。
苹果用RunLoop实现的功能
App启动后 ,系统默认
注册了5个Mode:
1)NS Default RunLoop Mode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
2)UI Tracking RunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
3)UI Initialization RunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
4)GS EventReceive RunLoopMode: 接受系统事件的内部 Mode,通常用不到。
5)NSRunLoopCommonModes : 这是一个占位的 Mode,没有实际作用。
注册了两个 Observer ( AutoreleasePool 相关操作)
回调Callout:都是 _wrapRunLoopWithAutoreleasePoolHandler()
第一个 Observer 监视的事件是
Entry (即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。
(其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。)
第二个 Observer 监听NSRunLoop运行状态:
BeforeWaiting (准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;
Exit (即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。
(其 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。)
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
也可以看看这篇博客,写的知识点比较详细:
https://www.jianshu.com/p/b80a8d4484e6
https://www.cnblogs.com/ioshe/p/5489112.html
https://www.jianshu.com/p/f33c0e5ad0e2