【iOS】RunLoop 知识点
一、基础篇
1.RunLoop是什么
-
RunLoop字面意思是跑圈,实际就是运行循环(即死循环)
其实它内部就是do-while循环,在这个循环内部不断的处理各种任务(比如Source、Timer、Observer)
2.RunLoop基本作用
- 保持程序持续运行(保证程序不退出)
- 处理(监听)APP中的各种事件(如,监听触摸事件、定时器事件、Selector事件)
- 节省CPU资源,提高程序性能(有事情时就做事情,没事情时就休息待命)
3.获取RunLoop对象
-
Foundation框架:
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象 [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象 // NSRunLoop类是OC编写的,是对CFRunLoopRef的一个简单的封装
-
Core Foundation框架:
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象 CFRunLoopGetMain(); // 获得主线程的RunLoop对象 // CFRunLoopRef是C语言编写的,更底层,开源
二、提高篇
1.RunLoop和线程间的关系
-
一个线程对应一个RunLoop(key和value的关系)。
-
线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop在第一次获取时创建,在线程结束时销毁。
创建子线程的RunLoop直接调用
[NSRunLoop currentRunLoop];
, 这个Get方法是懒加载的。 -
主线程的RunLoop默认是自动开启,其它线程(子线程)的RunLoop需要手动开启。
[[NSRunLoop currentRunLoop] run]; // 手动开启RunLoop
-
分析源码:
/** iOS 开发中能遇到两个线程对象: pthread_t 和 NSThread。 可以通过 pthread_main_thread_np() 或 [NSThread mainThread] 来获取主线程; 通过 pthread_self() 或 [NSThread currentThread] 来获取当前线程。 CFRunLoop 是基于 pthread 来管理的。 苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样: */ /// 全局的Dictionary,key 是 pthread_t(线程), value 是 CFRunLoopRef(RunLoop) static CFMutableDictionaryRef loopsDic; /// 访问 loopsDic 时的锁 static CFSpinLock_t loopsLock; /// 获取一个 pthread 对应的 RunLoop。 CFRunLoopRef _CFRunLoopGet(pthread_t thread) { OSSpinLockLock(&loopsLock); if (!loopsDic) { // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。 loopsDic = CFDictionaryCreateMutable(); CFRunLoopRef mainLoop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop); } /// 直接从 Dictionary 里获取。 CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread)); if (!loop) { /// 取不到时,创建一个 loop = _CFRunLoopCreate(); CFDictionarySetValue(loopsDic, thread, loop); /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。 _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop); } OSSpinLockUnLock(&loopsLock); return loop; } CFRunLoopRef CFRunLoopGetMain() { return _CFRunLoopGet(pthread_main_thread_np()); } CFRunLoopRef CFRunLoopGetCurrent() { return _CFRunLoopGet(pthread_self()); }
2.RunLoop相关类
-
Core Foundation中关于RunLoop的5个类:
CFRunLoopRef // 获得当前RunLoop和主RunLoop CFRunLoopModeRef // 代表的是RunLoop的运行模式 CFRunLoopSourceRef // 事件源,输入源 CFRunLoopTimerRef // 定时器时间 CFRunLoopObserverRef // 观察者,能够监听RunLoop的状态改变
RunLoop的相关类之间的关系如下:
[图片上传失败...(image-9690b1-1601390921288)]
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。但RunLoop每次只能选择一个模式运行。要保证运行循环RunLoop不退出,每个模式里面至少存在一个Source或者一个Timer,Observer可以有也可以没有,只是监听RunLoop的运行状态。
CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。
Source0:基于用户主动触发的事件(触摸事件,performSelector 都会触发Source0事件)
点击button 或点击屏幕,当点击屏幕时,手指和屏幕产生一个事件,这个事件会自动打包生成一个Source0事件
Source1:基于Port的线程间通信(与内核相关,自发调用的)
注意:Source1在处理的时候会分发一些操作给Source0去处理CFRunLoopTimerRef 是基于时间的触发器。
其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
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 // 运行循环所有活动 };
添加观察者到运行循环的代码:
// 监听RunLoop的各种活动状态(包括唤醒,休息,以及处理各种事件等...) - (void)observerRunLoopActivity { /* 1.创建观察者 参数1: 分配内存空间的方式,传默认 参数2: RunLoop的运行状态 参数3: 是否持续观察 参数4: 优先级,传0 参数5: 观察者观测到状态改变时触发的方法 */ CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { switch (activity) { case kCFRunLoopEntry: NSLog(@"RunLoop进入"); break; case kCFRunLoopBeforeTimers: NSLog(@"RunLoop要处理定时器(Timers)事件了"); break; case kCFRunLoopBeforeSources: NSLog(@"RunLoop要处理输入源(Sources)事件了"); break; case kCFRunLoopBeforeWaiting: NSLog(@"RunLoop要休息了"); break; case kCFRunLoopAfterWaiting: NSLog(@"RunLoop醒来了"); break; case kCFRunLoopExit: NSLog(@"RunLoop退出了"); break; default: break; } }); /* 2.添加观察者到运行循环 参数1: 要监听哪个RunLoop, 传入当前的运行循环 参数2: 观察者/监听者, 观察运行循环的各种状态 参数3: 运行循环的模式,要监听RunLoop在哪种运行模式下的状态 */ CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); /** 3.释放观察者 CF的内存管理(Core Foundation):凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release */ CFRelease(observer); }
3.RunLoop的model
-
RunLoop 有五种运行模式,其中我们常用的是1、2、5这三个。
kCFRunLoopDefaultMode // 1> App的默认Mode,通常主线程是在这个Mode下运行 UITrackingRunLoopMode // 2> 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响 UIInitializationRunLoopMode // 3> 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用 GSEventReceiveRunLoopMode // 4> 接受系统事件的内部 Mode,通常用不到 kCFRunLoopCommonModes // 5> 这是一个占位用的Mode,不是一种真正的Mode
-
Model间的切换:
我们平时在开发中一定遇到过,当我们使用NSTimer每一段时间执行一些事情时滑动UIScrollView,NSTimer就会暂停,当我们停止滑动以后,NSTimer又会重新恢复的情况
// 创建定时器并添加到RunLoop // [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES]; NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES]; // 把定时器添加到RunLoop中 // 1.NSDefaultRunLoopMode 默认运行模式,此时定时器任务只会在默认模式下执行 [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; // 当scrollView滑动的时候,timer失效,停止滑动时,timer恢复 // 原因:当scrollView滑动的时候,RunLoop的Mode会自动切换成UITrackingRunLoopMode模式,因此timer失效,当停止滑动,RunLoop又会切换回NSDefaultRunLoopMode模式,因此timer又会重新启动了 // 2. UITrackingRunLoopMode 界面跟踪模式,此时定时器任务只会在滑动scrollView时执行 [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode]; // 3. 那个如何让timer在两个模式下都可以运行呢?(即滚动视图时,不会对定时器产生影响) // 3.1 在两个模式下都添加timer 是可以的,但是timer添加了两次,并不是同一个timer // 3.2 使用占位的运行模式 NSRunLoopCommonModes标记,凡是被打上NSRunLoopCommonModes标记的都可以运行,因此也就是说如果我们使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode两种模式下运行 [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
在实际开发中,一般不把timer放到主线程的RunLoop中,因为主线程在执行阻塞的任务时,timer计时会不准。
如何让计时准确?如果timer在主线程中阻塞了怎么办?
1》放入子线程中(即要开辟一个新的线程,但是成本是需要开辟一个新的线程)
2》写一种跟RunLoop没有关系的计时,即GCD。(不会阻塞,推荐使用这种)// GCD定时器(常用) // 创建队列 dispatch_queue_t queue = dispatch_get_global_queue(0, 0); // 1.创建一个GCD定时器 /* 第一个参数:表明创建的是一个定时器 第四个参数:队列 */ dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); // 需要对timer进行强引用,保证其不会被释放掉,才会按时调用block块 // 局部变量,让指针强引用 self.timer = timer; // 2.设置定时器的开始时间,间隔时间,精准度 /* 第1个参数:要给哪个定时器设置 第2个参数:开始时间 第3个参数:间隔时间 第4个参数:精准度 一般为0 在允许范围内增加误差可提高程序的性能 GCD的单位是纳秒 所以要*NSEC_PER_SEC */ dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC); // 3.设置定时器要执行的事情 dispatch_source_set_event_handler(timer, ^{ NSLog(@"---%@--",[NSThread currentThread]); // 取消定时 if (判断条件) { dispatch_source_cancel(timer); self.timer = nil; } }); // 4.启动 dispatch_resume(timer);
4.应用场景
-
定时器:实例化定时器并指定监听方法后,需要把定时器加到RunLoop上。加到RunLoop上之后,才能在每个时间触发的时候去监听事件。
self.timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES]; [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
使用定时器还有一个细节:就是在定时器不使用的时候,必须要销毁,否则会产生循环引用。
target后有一个self,定时器会对self强引用;viewController本身也会对定时器强引用(定时器通常会保存到viewController的实例变量/属性中),所以就会产生循环引用。
-
常驻线程:永远活着的线程。开启一个子线程,再手动开启这个子线程的RunLoop,这个子线程就是常驻线程。
常驻线程的生命周期跟APP相同。跟主线程并行,永远不会被销毁,一直在后台默默的做一些事情。(常驻线程的使用一般比较少,在实际开发中基本上没有这种需求)
在子线程里面也有运行循环(RunLoop),这个运行循环(RunLoop)默认不被开启;只有我们调用它的时候才会被开启(即需要手动开启)。
[图片上传失败...(image-98d89b-1601390921288)]
特别注意:
在启动RunLoop之前建议用 @autoreleasepool {…}包裹。
意义:创建一个大释放池,释放{}期间创建的临时对象,一般好的框架的作者都会这么做。
- (void)executeTask { @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]; } }
-
自动释放池
Q:autoreleasePool对象是什么时候释放的? A:自动释放池的释放和创建与RunLoop有关。 当RunLoop开启时,就会自动创建一个自动释放池。 当Runloop准备休眠的时候,会释放旧的autoreleasePool对象,再重新创建一个新的空的autoreleasePool对象。 当RunLoop从休眠中被唤醒的时候,Timer,Source等新的事件就会放到新的自动释放池中。 当Runloop即将退出的时候,会释放掉相关所有的autoreleasePool对象。
注意:只有主线程的RunLoop会默认启动。也就意味着会自动创建自动释放池,子线程需要在线程调度方法中手动添加自动释放池。
-
performSelector方法
performSelector其实是创建了一个Timer,然后添加到当前的线程中。如果当前线程没有Runloop,这个方法则走不通的。
// 可以设置只在某个运行模式(modes)下执行方法(aSelector) - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes; - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
-
用于Socket开发:使用RunLoop能够监听网络端口数据的接收与发送情况。(平常企业开发中用Socket开发比较少,通常是做硬件通讯的时候,用得比较多。比如:智能家居开发、游戏机等)
-
iOS中默认开启的事件循环,保证主线程不退出。
[图片上传失败...(image-5ddd5e-1601390921288)]
第14行代码的UIApplicationMain函数内部就启动了一个RunLoop
所以UIApplicationMain函数一直没有返回,保持了程序的持续运行
这个默认启动的RunLoop是跟主线程相关联的。
三、总结
-
RunLoop知识点的大致框架:
[图片上传失败...(image-b32310-1601390921288)]
-
思考:以后为了增加用户体验,在用户UI交互的时候不做事件处理,我们可以把需要做的操作放到NSDefaultRunLoopMode。
-
RunLoop处理逻辑流程图:
[图片上传失败...(image-6a5199-1601390921288)]
在实际中的使用场景其实很明确了, 在程序中中有大量临时变量(循环/遍历中)的时候最好手动创建autoreleasepool{}
四、面试题
- 讲讲 RunLoop,项目中有用到吗?
- RunLoop内部实现逻辑?
- Runloop和线程的关系?
- timer 与 Runloop 的关系?
- 程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
- Runloop 是怎么响应用户操作的, 具体流程是什么样的?
- 说说RunLoop的几种状态?
- Runloop的mode作用是什么?