iOS 中RunLoop 是一个事件循环对象
runloop 跑一圈,只能执行一个事件。
一般一个线程执行任务完成后线程就会退出。如果需要在这个线程里多次执行任务,可能就会不停的创建,销毁线程,这样CPU消耗很大。如果可以让线程不退出并且能随时处理事件:
do {
//接受消息->等待->处理
} while(message != quit)
线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。
当启动一个iOS APP时主线程对应的RunLoop被主动开启。如果不杀掉APP则APP一直运行,就是因为RunLoop保证主线程不会被销毁。保证线程不退出。
RunLoop在循环过程中监听事件,当前线程有任务时,唤醒线程执行任务,任务执行完成以后,使当前线程进入休眠状态。当然这里的休眠不同于自己写的死循环(while(1);),它在休眠时几乎不会占用系统资源,是由操作系统内核负责实现的。
CFRunLoopRef & NSRunLoop
CoreFoundation 框架为 CFRunLoopRef 对象,它提供了纯 C 函数的 API,是线程安全的;
Foundation 框架中用 NSRunLoop 对象,是基于 CFRunLoopRef 的封装,提供面向对象的 API,不是线程安全的。要避免在其他线程上调用当前线程的RunLoop
RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。只能在一个线程的内部获取其 RunLoop(主线程除外)。
CFRunLoopModeRef
1. 一个Runloop对应一条线程,一一对应,以线程作为key, runloop 作为value 存在在一个全局字典中。
2. 一个runloop里面可以有多个CFRunLoopMode (模式),每一个mode又可以包含多个 source/timer/observer。
3. 每次调用 RunLoop 时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。
注意:如果需要切换 Mode,只能退出 RunLoop,再重新指定一个 Mode 进入。是为了分隔开不同mode下的 Source/Timer/Observer,让其互不影响
4. 一个 RunLoop 在某个 mode 下运行时,不会接收和处理其他 mode 的事件
5. 一个modeitem(Source/Timer/Observer) 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。
6. 如果runloop 指定运行的 mode 中一个 item 都没有,RunLoop 会直接退出,不进入循环。
kCFRunLoopCommonModes
1. CFRunLoop里面有一个 伪mode 叫做 kCFRunLoopCommonModes,它不是一个真正的 mode,而是若干个 mode 的集合(NSSet)。
注意:Run Loop不能在运行在 NSRunLoopCommonModes 模式,它不是一个具体的模式。
2. 一个modeitem(Source/Timer/Observer) 只要加入CommonModes 里面,就相当于添加到了CommonModes 里面所有的 mode 中。
注意:在iOS系统中主线程CommonMode默认包含了 NSDefaultRunLoopMode 和 UITrackingRunLoopMode。子线程CommonMode中只包含NSDefaultRunLoopMode。
3. 一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。
// 添加mode 将自定义Model添加到CFRunLoopCommonModes
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
注意:当 RunLoop 的model 发生变化时,RunLoop 都会自动将 commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里,(把CommonModes中所有的modelItem 分别加进 CommonModes 集合中的所有model下)。即使RunLoop切换model,modelitem 可以实现在多种model 下执行,不受model 变化的影响。
4. 添加事件源的时候添加到 NSRunLoopCommonModes,只要Run Loop运行在NSRunLoopCommonModes 中任何一个模式,这个事件源都可以被触发
5. 只能通过 mode name 来操作runLoop内部的 mode,当传入一个新的 mode name 但 RunLoop 内部没有对应 mode 时,RunLoop会自动创建对应的 CFRunLoopModeRef。
6. 对于 RunLoop 其内部的 mode 只能增加不能删除。
日常使用场景:
程序应用大部分情况下是处于NSDefaultRunLoopMode状态,只有当scrollView滑动时,主线程RunLoop 会自动切换为UITrackingRunLoopMode状态。
不同的mode 影响到设置的监听者(比如Timer或CADisplayLink)是否会被回调。比如在主线程中,设置Timer为NSDefaultRunLoopMode属性,当应用在滑动时,Timer的方法是不会被回调的,因为滑动过程中,RunLoop会切换为UITrackingRunLoopMode状态,而它只是监听了NSDefaultRunLoopMode状态。只有mainRunLoop才会在滑动时,切换为UITrackingRunLoopMode,子线程中的RunLoop是不会的。
在主线程中设置Timer或CADisplayLink,我们通常都会设置为NSRunLoopCommonModes属性,表示在NSDefaultRunLoopMode和UITrackingRunLoopMode状态下都会进行监听,避免滑动时,无法回调。
mode的切换,目前无法得知系统是如何进行切换的。从源码中发现的事实,切换的时候,会加锁保证多线程安全,并且有三处切换:
sleep 之前,runloop 可能一觉醒来,发现 mode 已经物是人非。
doMainQueue 之前,执行完 GCD main queue 中的任务后,mode 也能会发生变化。
在 CFRunLoopRunSpecific 函数,也就是 runloop exit 之后。
RunLoop退出的条件
1. app退出,线程关闭,被外部调用强制停止CFRunLoopStop()
2. 设置最大时间到期;kCFRunLoopRunTimedOut
3. modeItem为空;kCFRunLoopRunFinished
4. 参数说处理完事件就返回 kCFRunLoopRunHandledSource,启动Runloop时设置参数为一次性执行
CFRunLoopSourceRef
source 是事件产生的地方(输入源),分为三类,输入源向线程发送异步消息
1. Port-Based Sources: source1 ,基于Port,通过内核和其他线程通信,则是用来接收、分发系统事件,然后再分发到Sources0中处理的
2. Custom Input Sources: 自定义source0 相关 ,可处理UIKit 的 UIEvent 事件
3. Cocoa Perform Selector Sources: 与PerformSEL方法相关
源码中 source分为:source0和source1,区别在于它们是怎么被标记 (signal) 的。
source0 是 app 内部的消息机制,只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
source1 是基于端口 的,包含了一个 mach_port 和一个回调(函数指针),用于通过内核和其他线程相互发送消息。监听程序相应的端口,能主动唤醒 RunLoop 的线程.
Run loop处理的输入事件有两种不同的来源:输入源(input source)和定时源(timer source)。输入源传递异步消息,通常来自于其他线程或者程序。定时源则传递同步消息,在特定时间或者一定的时间间隔发生。
PerformSelecter:执行完后会自动清除出run loop。
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,方法会失效。
使用场景:事件响应
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。Source1 会触发回调,并调用 _UIApplicationHandleEventQueue( ) 进行应用内部的分发。
_UIApplicationHandleEventQueue() 会把IOHIDEvent处理并包装成UIEvent进行处理或分发,其中包括识别UIGesture/处理屏幕旋转/发送给UIWindow等。通常事件比如UIButton点击、touchesBegin/Move/End/Cancel事件都是在这个回调中完成的。
手势识别
_UIApplicationHandleEventQueue() 识别了一个手势时,首先调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。当UIGestureRecognizer 变化(创建/销毁/状态改变)时,回调都会进行处理。
定时器:CFRunLoopTimerRef--系统内“定时闹钟”
定时源在预设的时间点同步方式传递消息。定时器是线程通知自己做某事的一种方法。定时器是基于时间的通知,并不实时机制。
例如,搜索控件可以使用定时器,当用户连续输入的时间超过一定时间时,就开始一次搜索。这样,用户就可以有足够的时间来输入想要搜索的关键字。
NSTimer 和 performSEL 方法实际上是对CFRunloopTimerRef的封装;
GCD的timer:dispatch_source_t 类型,不用runloop和mode,性能消耗更小。
dispatch_source_set_timer(dispatch_source_t source, // 定时器对象
dispatch_time_t start, // 定时器开始执行的时间
uint64_t interval, // 定时器的间隔时间
uint64_t leeway // 定时器的精度
);
1. 如果定时器所在的模式 不是 runloop的currentmodel,那么定时器将不会执行,直到 runloop运行在定时器所在的mode 模式下。
2. 如果定时器在runloop处理某一事件期间开始,定时器会一直等待直到下次runloop开始相应的处理程序,如果runloop不运行了,那么定时器也永远不启动。
3. 如果timer 添加的线程未开启runloop , timer 就不会被执行。
在子线程中将NSTimer以默认方式加到该线程的runloop中,启动子线程
4. 注意内存泄漏,循环引用的问题。
一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。如果某个时间点被错过了,例如执行了一个很长的任务,对应时间点的回调会被跳过去,不会延后执行。就比如等公交,如果 10:10 时玩手机错过了,那只能等 10:20 的了。
注意:RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
CADisplayLink:和屏幕刷新率一致的定时器,一秒刷新60次。
和 NSTimer 不一样,其内部实际是操作了一个 Source。
CADisplayLink以特定模式注册到runloop后,每当屏幕显示内容刷新结束的时候,runloop就会向CADisplayLink指定的target发送一次指定的selector消息, CADisplayLink类对应的selector就会被调用一次。所以通常情况下,按照iOS设备屏幕的刷新率60次/秒
CADisplayLink 在正常情况下会在屏幕每次刷新结束后调用,精确度相当高。如果CPU过于繁忙,无法保证屏幕60次/秒的刷新率,就会导致跳过若干次调用回调方法的机会,跳过次数取决CPU的忙碌程度。
CADisplayLink 使用场合相对专一, 适合做界面的不停重绘,比如视频播放的时候需要不停地获取下一帧用于界面渲染。
定时器定时工作注意:定时器会基于安排好的时间而非实际时间,自动的开始。
注意:比如安排好了: 5,10,15,20 ...分别执行一次任务,如果定时器延迟到 10分的时候才开始任务,定时器在5-10这个时间段中也只会启动一次,之后按照预设的时间点正常运行15,20....。
NSTimer 和 CADisplayLink的区别 :
1. CADisplayLink - 简介 - 简书 2. 三个定时器之间的区别⏲
Runloop本质:mach port 和 mach_msg()
IOKit 层是为设备驱动提供了一个面向对象(C++)的一个框架。
在 Mach 中,进程、线程和虚拟内存都被称为”对象”。 Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。”消息”是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。
1条 Mach 消息实际上就是一个二进制数据包 (BLOB),其头部定义了当前端口 local_port 和目标端口 remote_port。(Source1是基础端口,它就是依靠系统发送消息到指定的Port来触发的)。
1. 通过 msg 函数 决定Runloop是否休眠等待:调用mach_msg( ) 函数接收消息,如果没有 port 消息过来,内核会将线程置于等待状态。
2. 一个是通过判断退出条件来决定Runloop是否循环。
在空闲前指定用于唤醒的 mach port 端口,空闲时被 mach_msg() 函数阻塞着并监听唤醒端口, mach_msg() 又会调用 mach_msg_trap() 函数从用户态切换到内核态,系统内核就将这个线程挂起,一直停留在 mac_msg_trap 状态。直到另一个线程向内核发送这个端口的 msg ,trap 状态被唤醒。 RunLoop 继续工作。(当有消息返回时,当前线程被阻塞,系统内核开辟一条新的线程去返回这个消息)
程序休眠时,RunLoop停留在 _CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy) 这个函数内部就是调用了mach_msg 让程序处于休眠状态。
runloop & NSThread
1. runloop 和 pthread_t (也就是线程)是一一对应的, 对应关系是保存在一个全局的 dictionary 中,key是pthread_t , value是相对应的RunLoop 。
2. 在子线程中调用 :NSRunLoop*runloop= [NSRunLoop currentRunLoop];
原理: 获取RunLoop对象的时候,会先看一下全局字典里有没有存子线程相对应的RunLoop,如果有直接返回,没有会创建一个,并将与之对应的子线程存入字典中。
3. RunLoop是一个懒加载机制,子线程的runloop默认不创建。 如果不主动获取,那子线程内的RunLoop一直不会存在。
4. 当线程结束销毁的时候,也销毁相应的 runloop。
5. 启动一个APP时主线程对应的RunLoop 会被主动开启
关于GCD 和 RunLoop 交互
RunLoop 底层会用到 GCD 的东西,GCD 的某些 API 也用到了 RunLoop。runloop启动时设置的最大超时时间实际上是GCD的dispatch_source_t类型。
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
Runloop即将进入休眠,会重绘一次界面
<CFRunLoopObserver> {
activities=0xa0,
callout=_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv
}
准备进入睡眠和即将退出 loop 两个时间点,会调用函数更新 UI 界面。 UI的绘制是拿到所有之后,在统一绘制的。
当在操作 UI 时,某个需要变化的 UIView/CALayer 就被标记为待处理,然后被提交到一个全局的容器去,再在上面的回调执行时才会被取出来进行绘制和调整。
UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。
排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。
绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。
UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。
其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。
CFRunLoopObserverRef
CFRunLoopObserverRef:消息循环中的一个监听器,监听runloop状态。随时通知外部当前RunLoop的运行状态(它包含一个函数指针callout将当前状态及时告诉观察者)
数据结构:
// Observer:order(优先级),ativity(监听状态),callout(回调函数)
CFRunLoopObserver {order = ..., activities = ..., callout = ...}
Observer监听 6 种状态:
1. kCFRunLoopEntry = (1UL << 0), // 进入RunLoop
2. kCFRunLoopBeforeTimers = (1UL << 1), // 即将开始Timer处理
3. kCFRunLoopBeforeSources = (1UL << 2), // 即将开始Source处理
4. kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
5. kCFRunLoopAfterWaiting = (1UL << 6), //从休眠状态唤醒
6. kCFRunLoopExit = (1UL << 7), //退出RunLoop
使用场景: AutoreleasePool
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush( ) 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting (准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,不会出现内存泄漏,开发者也不必显示创建 Pool 了。
runloop 使用解决问题:
RunLoop总结:RunLoop的应用场景(三)_weixin_30437481的博客-CSDN博客 tableview刷新图片加载卡顿问题优化。
iOS - 聊聊 autorelease 和 @autoreleasepool_移动开发_weixin_42350379的博客-CSDN博客
浅析RunLoop原理及其应用 – ITPUB tableview滑动时 刷新cell 图片加载卡顿问题优化。
计时器相关:
iOS的三种常见计时器(NStimer、CADisplayLink、dispatch_source_t)的使用_马拉萨的春天的博客-CSDN博客_ios 计时器
参考:
线程出错引起什么问题,面试问到:
OS / 进程中某个线程崩溃,是否会对其他线程造成影响?_布袋和尚-CSDN博客
(最全)RunLoop 原理+使用场景+面试总结 - 简书 ⭐️⭐️⭐️⭐️⭐️
iOS runloop的深入浅出,runloop的理解看这里就够了_移动开发_horisea的博客-CSDN博客
深入浅出 RunLoop(一):初识_移动开发_weixin_42350379的博客-CSDN博客
RunLoop_RunLoop_xiaoxiaobukuang的专栏-CSDN博客