Runloop 接收输入时间来自两种不同的来源:输入源(intput source)和定时源(timer source)。输入源传递一步时间。通常消息来自于其他线程或程序。定时源则传递同步时间,发生在特定时间或者重复的时间间隔。两种源都使用程序的某一特定的处理历程来处理到达的时间。
一、什么是RunLoop
- 基本作用
- 保持程序的持续运行(一个死循环,使app不断运行)
- 处理App中的各种事件(触摸、定时器、Selector)
- 节省CPU资源、提高程序性能:该做事的时候做事,该休息的时候休息。
- 如果没有RunLoop
int main(int argc,char * argv[]){ NSLog(@"execute main function");---->程序开始 return 0; ------------------------->程序结束 }
- 有 RunLoop
- 由于 main 函数里面启动了个 RunLoop,所以程序并不会马上退出,保持持续运行状态
int main(int argc,char * argv[]){ BOOL running = YES; -------->程序开始 do {------------------------------ // 执行各种任务,处理各种事件------持续运行 }while(running);--------------------- return 0; }
- 由于 main 函数里面启动了个 RunLoop,所以程序并不会马上退出,保持持续运行状态
二、main 函数中的 RunLoop
- UIApplicationMain函数内部就启动了一个RunLoop
- 所以UIApplicationMain 函数一直没有返回,保持了程序的持续运行
- 这个默认启动的 RunLoop 是跟主线程相关联的
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
三、RunLoop 的输入源
- 输入源异步的发送消息给你的线程,时间的来源取决于输入源的种类:基于端口的输入员和自定义输入源。基于端口的输入源监听程序相应的端口。自定义输入源则监听自定义的事件源。runloop,不关心输入源是基于端口的还是自定义的。系统会实现两种输入源供你使用。两类输入源的区别在于如何显示:基于端口的输入源由内核自动发送,而自定义的则需要人工从其他线程发送。
- 当你创建输入源的时候,需要将其分配给 runloop 中的一个或多个模式。模式只会在特定事件影响监听的源。大多数情况下,runloop 运行在默认模式下,但是你也可以使器运行在自定义模式中。若某一源在当前模式下不被监听,那么任何器生成消息只在 runloop 运行在其关联的模式下才会被传递。
- 基于端口的输入源
- Cocoa 和 CoreFoundation 内置支持使用端口相关的对象和函数来创建基于端口的源。在 Cocoa 里面你从来不需要直接创建输入源。只要简单的创建对象,并使用 NSPort 的方法将该端口天极大到 ruhnloop 中。端口对象会自己处理创建和配置的输入源。
- 自定义输入源
- 为了自定义输入源,必须使用 Core Foundation里面的 CGRunLoopSourceRef类型相关的函数来创建。你可以使用回调函数来配置自定义输入源。Corefondation 会在配置源的不同地方调用回调函数,处理输入时间,在源从 runloop 移除的时候清理它。
- 除了定义在事件到达时自定义输入源的行为,你也必须定义消息传递机制。源的这部分运行在单独的线程里面,并负责在数据等待处理的时候传递数据给源并源并通知它处理数据。消息传递机制的定义取决于你,但是最好不要过于复杂。
- Cocoa 执行 Selector 的源
- 除了基于端口的源,Cocoa 定义了自定义的输入源,允许你在任何线程中执行 seletor。和基于端口的源一样,执行 selector 请求会在目标线程上序列化,减缓许多咋线程上允许多个方法容易引起的同步问题。不像基于端口的源,一个 selector 执行完后会自动从 runloop 里面移除。
- 当在其他线程上面执行 selector 时候,目标线程需有一个活动的 runloop,对于你创建的线程,这意味着线程在你显示的启动 runloop 之前处于等待状态。由于主线程自己启动它的 runloop,那么在程序通过委托调用 applicationDidFinishlaunching:的时候你会遇到线程调用的问题。因为 RunLoop 通过每次循环来处理所有队列的 selector 的调用,而不是通过 loop 的迭代来处理 selector。
四、RunLoop 对象
- iOS 中有2套 API 来访问和使用 RunLoop
- Foundation
- NSRunLoop
- CoreFoundation
- CFRunLoopRef
- Foundation
- NSRunLoop 和 CGRunLoopRef 都代表着 RunLoop 对象
- NSRunLoop 是基于 CFRunLoopRef 的一层 OC 包装, 所以要了解 RunLoop内部结构,需要研究 CFRunLoopRef 层面的 API (Core Foundation层面)
五、RunLoop 与线程
- 每条线程都有唯一的一个与之对应的 RunLoop 对象
- 主线程的 RunLoop 自动创建好了,子线程的 RunLoop 需要主动创建
- RunLoop 在第一次获取时创建,在线程结束时销毁
六、获取RunLoop 对象
- Foundation
//获得主线程的 RunLoop 对象//获得当前线程的 RunLoop 对象 [NSRunLoop currentRunLoop];
[NSRunLoop mainRunLoop]; - Core Foundation
//当前RunLoop CFRunLoopGetCurrent(); //主线程 RunLoop CFRunLoopGetMain();
七、NSRunLoop 相关类
- CoreFoundation中关于RunLoop的5个类
- CFRunLoopRef(运行循环对象)
- CFRunLoopModeRef(1个runLoop可以有很多个Mode,1个Mode可以有很多个Source Observer Timer,
但是在同一时刻只能同时执行一种Mode
,关于更多种类的Mode) - CFRunLoopSourceRef(处理事件)
- CFRunLoopTimerRef(处理定时器相关)
- CFRunLoopObserverRef(观察者,观察是否有事件)
- CFRunLoopModeRef 代表 RunLoop 的运行模式
- 一个 RunLoop 包含若干个 Mode,每个Mode 又包含若干个 Source/Timer、Observer
- 每次 RunLoop 启动时,只能制定其中一个 Mode,这个 Mode 被称作 CurrentMode
- 如果需要切换 Mode,只能退出 Loop,再重新制定一个 Mode 进入
- 这样做主要是为了分割开不同组的 Source/Timer/Observer,让其互不影响
- 系统默认注册了 5个Mode:
kCFRunLoopDefaultMode
:App的默认Mode,通常主线程是在这个 Mode 下运行的UITrackingRunLoopMode
:界面跟踪 Mode,用于ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode 影响- UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个Mode,启动完成之后就不再使用。
- GSEventReceiveRunLoopMode:接收系统时间的内部 Mode,通常用不到。
kCFRunLoopCommonModes(比较特殊)
:这时一个占位用的 Mode,不是一种真正的 Mode。
- CFRunLoopSourceRef 是事件源(输入源)
- Source0:非基于Port的
- Custom Input Sources
- Cocoa Perform Selector Sources
- Source1:基于Port的
- Port- Based Sources
- 举例:输出点击事件的调用栈,
我们可以清楚的看到runloop中做的是__CFRunLoopDoSource0
。
- Source0:非基于Port的
- CFRunLoopTimerRef 处理定时器
- NSTimer 定时器调用栈:__CFRunLoopDoTimer
- 注意:使用不同种类的 Mode 会对定时器的效果有不同的展现
NSDefaultRunLoopMode
:将NSTimer添加到主线程NSRunLoop的默认模式下,只有主线程是默认模式下才能执行NSTimer(滚动scrollView,RunLoop默认进入Tracking模式,所以NSTimer不会有效果)。UITrackingRunLoopMode
:将NSTimer添加到主线程NSRunLoop的追踪模式下,只有主线程是追踪模式下才能执行NSTimer。(例如滚动scrollView的时候就会监听到计时器)NSRunLoopCommonModes
:Common是一个表示,它是将NSDefaultRunLoopMode 和 UITrackingRunLoopMode标记为了Common
所以,只要将 timer 添加到 Common 占位模式下,timer就可以在Default和UITrackingRunLoopMode模式下都能运行
- NSTimer 定时器调用栈:__CFRunLoopDoTimer
- 如果用GCD创建计时器:
- GCD 创建的好处,不受 RunLoopMode 的影响。
//1、创建timer //dispatchQueue:定时器将来回调的方法在哪个线程中执行 dispatch_queue_t queue = dispatch_get_main_queue(); dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); self.timer = timer; //2.设置timer /* 第一个参数:需要设置哪个timer 第二个参数:指定定时器开始的时间 第三个参数:指定间隔时间 第四个参数:定时器的精准度,如果传0代表要求非常精准(系统会让计时器执行时间变得更加准确,性能消耗也会提高),如果传入一个大于0的值,代表我们允许的误差 //例如传入60,就代表允许误差有60秒 */ //设置第一次执行的时间 dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)); //DISPATCH_TIME_NOW dispatch_source_set_timer(timer,start , 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC); //3、设置timer的回调 dispatch_source_set_event_handler(timer, ^{ NSLog(@"%@",[NSRunLoop currentRunLoop]); }); dispatch_resume(timer);
- 在 RunLoop 底层默认会调用这里
/// 9.1 如果一个 Timer 到时间了,触发这个Timer的回调。 if (msg_is_timer) { __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time()) } /// 9.2 如果有dispatch到main_queue的block,执行block。 else if (msg_is_dispatch) { __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); }
- GCD 创建的好处,不受 RunLoopMode 的影响。
- CFRunLoopObserverRef观察者,能够监听RunLoop状态改变
- 监听的时间点:
typedef CF_OPTIONS(CFOptionFlags,CFRunLoopActivity){ kCFRunLoopEntry = (1UL << 0), // 即将进入LOOP kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer kCFRunLoopBeforeSources = (1UL << 2), // 即将进入处理Source kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6), // 刚才休眠中唤醒 kCFRunLoopExit = (1UL << 7), // 即将退出Loop }
- 监听的代码:
我们会看到这几行打印会重复执行- (void)viewDidLoad{ [super viewDidLoad]; //1、创建监听对象 /* 第一个参数:告诉系统如何给Observer对象分配存储空间 第二个参数:需要监听的类型 第三个参数:是否需要重复监听 第四个参数:优先级 第五个参数:监听到对应的状态之后的回调 typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), kCFRunLoopBeforeTimers = (1UL << 1), kCFRunLoopBeforeSources = (1UL << 2), kCFRunLoopBeforeWaiting = (1UL << 5), kCFRunLoopAfterWaiting = (1UL << 6), kCFRunLoopExit = (1UL << 7), kCFRunLoopAllActivities = 0x0FFFFFFFU }; */ CFRunLoopObserverRef oberver= CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { switch (activity) { case kCFRunLoopEntry: NSLog(@"进入RunLoop"); break; case kCFRunLoopBeforeTimers: NSLog(@"即将处理Timer"); break; case kCFRunLoopBeforeSources: NSLog(@"即将处理source"); break; case kCFRunLoopBeforeWaiting: NSLog(@"即将进入睡眠"); break; case kCFRunLoopAfterWaiting: NSLog(@"即将醒来"); break; case kCFRunLoopExit: NSLog(@"退出"); break; default: break; } }); //2、给主线程的RunLoop添加监听 /* 第一个参数:需要监听的 RunLoop 对象 第二个参数:给指定的 RunLoop 对象添加的监听对象 第三个参数:在哪种模式下监听 */ CFRunLoopAddObserver(CFRunLoopGetMain(), oberver, kCFRunLoopCommonModes); NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(demo) userInfo:nil repeats:YES]; } - (void)demo{ NSLog(@"%s",__func__); }
2015-09-06 17:02:04.848 RunLoop观察者[35817:418636] 即将醒来 2015-09-06 17:02:04.849 RunLoop观察者[35817:418636] -[ViewController demo] 2015-09-06 17:02:04.849 RunLoop观察者[35817:418636] 即将处理Timer 2015-09-06 17:02:04.849 RunLoop观察者[35817:418636] 即将处理source 2015-09-06 17:02:04.849 RunLoop观察者[35817:418636] 即将进入睡眠 2015-09-06 17:02:06.848 RunLoop观察者[35817:418636] 即将醒来 2015-09-06 17:02:06.849 RunLoop观察者[35817:418636] -[ViewController demo] 2015-09-06 17:02:06.849 RunLoop观察者[35817:418636] 即将处理Timer 2015-09-06 17:02:06.849 RunLoop观察者[35817:418636] 即将处理source 2015-09-06 17:02:06.849 RunLoop观察者[35817:418636] 即将进入睡眠
- 监听的时间点:
八、关于 RunLoop 的理解
- 理解图(
引用了一张网上很好的图片
):
- 一条线程对应一个 RunLoop,主线程的 RunLoop 只要程序已启动就会默认创建并与主线程绑定好,
RunLoop 底层的实现是通过字典的形式来将 线程 和 RunLoop 来绑定的
,RunLoop 可以理解为懒加载,子线程的 RunLoop 可以调用 currentRunLoop,先从字典里面根据子线程取,如果没有就会去创建并与子线程绑定,保存到字典当中。每个 RunLoop 里面有很多的 Mode,每个 Mode 里面又有很多的source、timer、observer。RunLoop 在同一时刻只能执行一种 Mode,当执行这种 Mode 的时候,只有这种 Mode 中的source、timer、observer 有效,别的 Mode 无效,这样做是为了避免逻辑的混乱。 - 执行流程:先进入 RunLoop,处理系统默认事件,触发事件的时候,RunLoop 醒来处理 timer、source0、source1,处理完再睡觉。
- RunLoop 死掉的情况:
- RunLoop 有个默认的超时时间.
seconds = 9999999999.0
- 线程挂了。
- RunLoop 有个默认的超时时间.
九、RunLoop 应用场景
- NSTimer
- 就是CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
- 如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
- CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和
NSTimer 并不一样,其内部实际是操作了一个 Source
)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。
- ImageView显示
- PerformSelector:
- 当调用 NSObject 的 performSelector:afterDelay:后,世纪上期内部会创建一个 Timer 并添加到当前线程的 RunLoop 中,所以如果当前线程没有 RunLoop,则这个方法会失效。
- 当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
- 常驻线程
创建一个线程来处理耗时且频繁的操作,例如即时聊天音频的压缩,或者经常下载,避免频繁开启线程以便提高性能
, AFNetWorking就是如此。[[NSThread currentThread] setName:@"AFNetworking"]; NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run];
- 自动释放池
- 系统在主线程 RunLoop 里注册了
两个 Observer
,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()
第一个 Observer 监视的事件是 Entry(即将进入Loop)
,其回调内会调用_objc_autoreleasePoolPush()
创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。第二个 Observer 监视了两个事件
:BeforeWaiting(准备进入休眠) 时
调用_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
释放旧的池并创建新池;Exit(即将退出Loop)
时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。- 打印 currentRunLoop 来获取autoreleasePool 的状态
NSLog(@"%@",[NSRunLoop currentRunLoop]);
- 只有两种状态
_wrapRunLoopWithAutoreleasePoolHandler:activities = 0x1 = 1 _wrapRunLoopWithAutoreleasePoolHandler:activities = 0xa0 = 160
- 对比 RunLoop 的活动状态:
对比runLoop 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 }
- 得出结论:
+ _wrapRunLoopWithAutoreleasePoolHandler:activities = 0x1 = 1 = 即将进入RunLoop创建一个自动释放池 + _wrapRunLoopWithAutoreleasePoolHandler:activities = 0xa0 = 160 = 128+32 + 32: 即将进入休眠 1、销毁一个自动释放池 2、创建一个新的自动释放池 + 128:即将退出RunLoop 销毁一个自动释放池
- 系统在主线程 RunLoop 里注册了