NSRunLoop-使用场景分析
Posted 乌戈勒
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NSRunLoop-使用场景分析相关的知识,希望对你有一定的参考价值。
前言
NSRunLoop是一个比较深奥难懂的ios知识,我觉得很有必要花上一段时间对它进行深入的理解学习,之前针对NSRunLoop进行了一篇总结,可以参考。
NSRunLoop-深入剖析
如果从这些文字理论上去理解NSRunLoop,是很深奥难懂的,所以,我这里专门总结了一些跟NSRunLoop有关的使用场景,方便加深理解。
先看下苹果系统是怎么使用runloop的,后面再看看我们开发的时候,能用runloop做些什么?
一、苹果用RunLoop 实现的功能
1、AutoreleasePool自动释放池的创建和释放:
App启动后,通过打印主线程的RunLoop,我们可以看到,系统在主线程自动生成了两个观察者Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
<CFRunLoopObserver 0x7fad438034b0 [0x10c874a40]>valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10c9fb4c2), context = <CFArray 0x7fad43803070 [0x10c874a40]>type = mutable-small, count = 1, values = (
0 : <0x7fad44000048>
)
...
<CFRunLoopObserver 0x7fad43809d30 [0x10c874a40]>valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10c9fb4c2), context = <CFArray 0x7fad43803070 [0x10c874a40]>type = mutable-small, count = 1, values = (
0 : <0x7fad44000048>
)
1、一个观察者监测RunLoop的kCFRunLoopEntry状态(最高优先级order = -2147483647,由于优先级最高,因此先于所有其他回调操作生成)。
设置这个观察者的目的在于在RunLoop启动刚进入的时候生成自动释放池。
2、另一个观察者监测RunLoop的kCFRunLoopBeforeWaiting状态和kCFRunLoopExit状态(最低优先级order = 2147483647)。
该观察者的任务是当RunLoop在即将休眠时,调用_objc_autoreleasePoolPop()销毁自动释放池,根据池中的记录,向池中所有对象发送realse方法真正地减少其引用计数,并调用_objc_autoreleasePoolPush()创建新的自动释放池,当RunLoop即将退出的时候调用_objc_autoreleasePoolPop()销毁池子。
而主线程中执行的代码,通常都是写在RunLoop事件回调,Timer回调中,处于自动释放池范围内,因此不会出现内存泄漏,开发人员也不需要显示在主线程创建自动释放池了。
oc中的自动释放池本质上就是延迟释放,将向自动释放池中对象发送的释放消息存在pool中,当pool即将被销毁的时候向其中所有对象发送realse消息使其计数减小。
而自动释放池的创建和销毁也是由RunLoop控制的。
结论:
自动释放池什么时候释放?通过Observer监听RunLoop的状态,一旦即将进入睡眠或者退出状态,则会销毁自动释放池,然后向池中对象发送release方法,减少引用计数。
2、事件响应(如UIButton点击):
苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
<CFRunLoopSource 0x7fad43801e30 [0x10c874a40]>signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x7fad43801970 [0x10c874a40]>valid = Yes, port = 1e03, source = 0x7fad43801e30, callout = __IOHIDEventSystemClientAvailabilityCallback (0x10eb2a444), context = <CFMachPort context 0x7fad42508470>
···
<CFRunLoopSource 0x7fad43801cb0 [0x10c874a40]>signalled = No, valid = Yes, order = 0, context = <CFMachPort 0x7fad43801690 [0x10c874a40]>valid = Yes, port = 1d03, source = 0x7fad43801cb0, callout = __IOHIDEventSystemClientQueueCallback (0x10eb2a293), context = <CFMachPort context 0x7fad42508470>
底层处理过程:
1、当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。
2、SpringBoard 只接收按键(锁屏/静音等)、触摸、加速、接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。
3、随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。
4、_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。普通事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。
注意:
当用户点击屏幕的时候最先是产生了一个硬件事件,底层封装后触发RunLoop的Source1源,回调__IOHIDEventSystemClientQueueCallback函数处理,然后再触发RunLoop中的一个Source0的源,回调_UIApplicationHandleEventQueue函数将事件包装成UIEvent处理并分发,通过函数调用栈可以看到:
// 点击event的调用栈
0 test2 0x000000010024356d -[UITestGestureRecognizer touchesBegan:withEvent:] + 157
1 UIKit 0x0000000101575bcf -[UIGestureRecognizer _touchesBegan:withEvent:] + 113
2 UIKit 0x00000001010fb196 -[UIWindow _sendGesturesForEvent:] + 377
3 UIKit 0x00000001010fc6c4 -[UIWindow sendEvent:] + 849
4 UIKit 0x00000001010a7dc6 -[UIApplication sendEvent:] + 263
5 UIKit 0x0000000101081553 _UIApplicationHandleEventQueue + 6660
6 CoreFoundation 0x0000000100bf6301 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
7 CoreFoundation 0x0000000100bec22c __CFRunLoopDoSources0 + 556
8 CoreFoundation 0x0000000100beb6e3 __CFRunLoopRun + 867
9 CoreFoundation 0x0000000100beb0f8 CFRunLoopRunSpecific + 488
10 GraphicsServices 0x0000000104550ad2 GSEventRunModal + 161
11 UIKit 0x0000000101086f09 UIApplicationMain + 171
12 test2 0x0000000100243adf main + 111
13 libdyld.dylib 0x00000001034aa92d start + 1
调用栈显示,处理事件相应的时候是由Source0分发的event。而实际上这个事件的识别和包装是由Source0处理的。
3、手势识别:
系统默认注册了一个Source0源用以分发事件,回调_UIApplicationHandleEventQueue方法,然后系统会将对应的UIGestureRecognizer标记为待处理,然后还注册了一个观察者,监听RunLoop即将进入休眠的事件,回调_UIGestureRecognizerUpdateObserver()函数,该函数内部会获取到所有被标记为待处理的UIGestureRecognizer,执行相应的回调。
···
<CFRunLoopSource 0x7fad4380a9b0 [0x10c874a40]>signalled = No, valid = Yes, order = -1, context = <CFRunLoopSource context>version = 0, info = 0x7fad43900630, callout = _UIApplicationHandleEventQueue (0x10c9fbb4f)
···
<CFRunLoopObserver 0x7fad4388e010 [0x10c874a40]>valid = Yes, activities = 0x20, repeats = Yes, order = 0, callout = _UIGestureRecognizerUpdateObserver (0x10ceea41f), context = <CFRunLoopObserver context 0x0>
当用户触发手势操作,底层封装后触发RunLoop的Source1源,回调__IOHIDEventSystemClientQueueCallback函数处理,然后再触发RunLoop中的一个Source0的源,回调_UIApplicationHandleEventQueue函数将事件包装成UIEvent处理并分发,如果 _UIApplicationHandleEventQueue() 识别到是一个guesture手势,会调用Cancel方法将当前的touchesBegin/Move/End 系列回调打断,随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,其回调函数为 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调,最后发送给对应GestureRecognizer的target。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。通过函数调用栈可以看到:
// gestureRecognizer 手势 的调用栈
test2 0x000000010c34e55f -[ViewController touchedButton] + 63
2 UIKit 0x000000010d68ab28 _UIGestureRecognizerSendTargetActions + 153
3 UIKit 0x000000010d68719a _UIGestureRecognizerSendActions + 162
4 UIKit 0x000000010d685197 -[UIGestureRecognizer _updateGestureWithEvent:buttonEvent:] + 843
5 UIKit 0x000000010d68d655 ___UIGestureRecognizerUpdate_block_invoke898 + 79
6 UIKit 0x000000010d68d4f3 _UIGestureRecognizerRemoveObjectsFromArrayAndApplyBlocks + 342
7 UIKit 0x000000010d67ae75 _UIGestureRecognizerUpdate + 2634
8 UIKit 0x000000010d20748e -[UIWindow _sendGesturesForEvent:] + 1137
9 UIKit 0x000000010d2086c4 -[UIWindow sendEvent:] + 849
10 UIKit 0x000000010d1b3dc6 -[UIApplication sendEvent:] + 263
11 UIKit 0x000000010d18d553 _UIApplicationHandleEventQueue + 6660
12 CoreFoundation 0x000000010cd02301 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17
13 CoreFoundation 0x000000010ccf822c __CFRunLoopDoSources0 + 556
14 CoreFoundation 0x000000010ccf76e3 __CFRunLoopRun + 867
15 CoreFoundation 0x000000010ccf70f8 CFRunLoopRunSpecific + 488
16 GraphicsServices 0x000000011065cad2 GSEventRunModal + 161
17 UIKit 0x000000010d192f09 UIApplicationMain + 171
18 test2 0x000000010c34eaef main + 111
19 libdyld.dylib 0x000000010f5b692d start + 1
4、界面刷新:
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。
// 系统注册了一个观察者:
<CFRunLoopObserver 0x7fad42419d40 [0x10c874a40]>valid = Yes, activities = 0xa0, repeats = Yes, order = 2000000, callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv (0x110afb79c), context = <CFRunLoopObserver context 0x0>
苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。
这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
5、GCD任务:
实际上 GCD 提供的某些接口用到了 RunLoop, 例如 dispatch_async()。
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。
但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
6、Timer定时器:
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时,即使一帧的卡顿也会让用户有所察觉。
7、PerformSelector:
当调用 NSObject 的 performSelector:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。
所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
8、网络请求:
iOS中,关于网络请求的接口,自下而上有如下几层:
CFSocket
CFNetwork ->ASIHttpRequest
NSURLConnection ->AFNetworking
NSURLSession ->AFNetworking2, Alamofire
1、CFSocket 是最底层的接口,只负责 socket 通信。
2、CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。
3、NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。
4、NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。
下面主要介绍 NSURLConnection 的工作过程:
通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。
实际上,start 这个函数的内部会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。
CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。
当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。
其中 CFSocket 线程是处理底层 socket 连接的。
NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。
NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。
二、开发者的使用场景
1、线程池
线程池的使用是:
为了避免程序使用线程的时候,频繁地创建和销毁线程,从而造成不必要的过度消耗,因此通过线程池机制维持几个常驻线程,避免使用的时候频繁创建和销毁,达到节省资源和加快响应速度的目的。
但是除了主线程外的线程默认是创建后执行完毕销毁的,这个时候,如果还需要维护住该线程,则需要手动创建该线程对应的RunLoop。
RunLoop启动后,可以向该RunLoop添加一个port或者Timer用以触发线程接收自己生成的事件,从而分发处理。
在这种方式中,RunLoop执行的主要任务是维持线程不退出,然后应用就可以利用performSelector:onThread:等方式,将自己的事件传递给RunLoop处理了。
代码示例一:如果创建常驻线程
//著名的开源框架AFNetworking就是这样生成网络线程的。
//生成“常驻线程”,避免线程的频繁创建、销毁,过度消耗性能和资源
// 注意:默认情况下一个线程只能使用一次, 也就是说只能执行一个操作, 执行完毕之后就不能使用了,所以,我们要保证子线程不被销毁。
// 子线程的NSRunLoop 需要手动创建,子线程的NSRunLoop 需要手动开启。
// 如果子线程的NSRunLoop没有设置source or timer,或者limitDate到了,那么子线程的NSRunLoop会立即关闭。
+ (void)networkRequestThreadEntryPoint:(id)__unused object
NSLog(@"---start---");
@autoreleasepool
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// 方案一:采用死循环的方式是行不通的,起码不建议
// while (1)
// NSLog(@"%s---%@", __func__, [NSThread currentThread]);
// BOOL result = [runLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
//
// 方案二:向该RunLoop添加一个port
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 方案三:向该RunLoop添加一个timer
// NSTimer *timer =
// [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
// [runLoop addTimer:timer forMode:NSRunLoopCommonModes];
[runLoop run];
// NSRunLoop只会检查有没有source和timer, 没有就关闭, 不会检查observer
// 注意:下面的代码永远不会被执行
NSLog(@"---end---");
+ (NSThread *)networkRequestThread
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
);
return _networkRequestThread;
- (void)start
[self.lock lock];
if ([self isCancelled])
[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
else if ([self isReady])
self.state = AFOperationExecutingState;
[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
[self.lock unlock];
- (void)test
NSLog(@"%s-----%@", __func__, [NSThread currentThread]);
代码示例二:performSelector:onThread:withObject:waitUntilDone:的详解
// 个人测试代码
- (void)viewDidLoad
[super viewDidLoad];
// 在子线程执行下面代码
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^
NSLog(@"---start---");
// 测试一:waitUntilDone=NO
[self performSelector:@selector(operation) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO];
// 测试二:waitUntilDone=YES
// [self performSelector:@selector(operation) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:YES];
// 测试三:waitUntilDone=NO且operation方法放到当前线程去执行,当前线程在当前函数结束后,就结束了。
// [self performSelector:@selector(operation) onThread:[NSThread currentThread] withObject:nil waitUntilDone:NO];
// 测试一和测试二的结论:waitUntilDone=YES表示:operation方法所在线程执行结束后,才会执行当前线程,即下面的输出代码。
// 测试三的结论:如果waitUntilDone=NO,会发现operation方法可能会不执行。
NSLog(@"---end---");
);
- (void)operation
NSLog(@"%s-----%@", __func__, [NSThread currentThread]);
2、通过监听RunLoop处理事件
通过添加Observer监听主线程RunLoop,可以仿造主线程RunLoop进行UI更新,例如Facebook的开源框架AsyncDisplayKit。
AsyncDisplayKit框架就是仿造QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制。
平时的时候将UI对象的创建、设置属性的事件放在队列中,通过在主线程添加Observer监听主线程RunLoop的kCFRunLoopBeforeWaiting 和 kCFRunLoopExit事件,在收到回调时,遍历队列里的所有事件并执行。
因此,通过合理利用RunLoop机制,可以将很多不是必须在主线程中执行的操作放在子线程中实现,然后在合适的时机同步到主线程中,这样可以节省在主线程执行操作的时间,避免卡顿。
Talk is cheap, show me the code!
示例一:在主线程添加Observer监听主线程RunLoop的各种事件回调。(如手势响应)
- (void)start
dispatch_async(dispatch_get_main_queue(), ^
[self setupRunloopObserver];
);
- (void)setupRunloopObserver
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^
CFRunLoopRef runloop = CFRunLoopGetCurrent();
CFRunLoopObserverRef enterObserver;
enterObserver = 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(@"RunLoop刚从睡眠中唤醒");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop即将退出");
break;
default:
break;
);
CFRunLoopAddObserver(runloop, enterObserver, kCFRunLoopCommonModes);
CFRelease(enterObserver);
);
结论:上面的代码是监测主线程runloop的各种事件回调,只要手指触摸屏幕,或者其它操作,就会有相应的事件回调。
示例二:监测子线程runloop的各种事件回调。
- (void)setupRunloopObserver
dispatch_async(dispatch_get_global_queue(0, 0), ^
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity)
switch (activity)
case kCFRunLoopEntry:
NSLog(@"即将进入runloop");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"即将处理timer");
break;
case kCFRunLoopBeforeSources:
NSLog(@"即将处理input Sources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"即将睡眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"从睡眠中唤醒,处理完唤醒源之前");
break;
case kCFRunLoopExit:
NSLog(@"退出");
break;
default:
break;
);
// 没有任何事件源则不会进入runloop
// [NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(doFireTimer) userInfo:nil repeats:YES];
[NSTimer scheduledTimerWithTimeInterval:3 target:self selector:@selector(doFireTimer) userInfo:nil repeats:NO];
CFRunLoopAddObserver([[NSRunLoop currentRunLoop] getCFRunLoop], observer, kCFRunLoopDefaultMode);
[[NSRunLoop currentRunLoop] run];
);
- (void)doFireTimer
NSLog(@"---fire---");
1、如果定时器 repeats=NO,那么输出结果:
2018-04-10 10:19:24.512 keke[10214:10428126] 即将进入runloop
2018-04-10 10:19:24.512 keke[10214:10428126] 即将处理timer
2018-04-10 10:19:24.512 keke[10214:10428126] 即将处理input Sources
2018-04-10 10:19:24.512 keke[10214:10428126] 即将睡眠
2018-04-10 10:19:24.512 keke[10214:10428126] 从睡眠中唤醒,处理完唤醒源之前
2018-04-10 10:19:24.512 keke[10214:10428126] —fire—
2018-04-10 10:19:24.512 keke[10214:10428126] 退出
…
…2、如果定时器 repeats=YES,那么runloop就不会退出,输出结果:
2018-04-10 10:19:24.512 keke[10214:10428126] 即将进入runloop
2018-04-10 10:19:24.512 keke[10214:10428126] 即将处理timer
2018-04-10 10:19:24.512 keke[10214:10428126] 即将处理input Sources
2018-04-10 10:19:24.512 keke[10214:10428126] 即将睡眠
2018-04-10 10:19:24.512 keke[10214:10428126] 从睡眠中唤醒,处理完唤醒源之前
2018-04-10 10:19:24.512 keke[10214:10428126] —fire—
…
2018-04-10 10:19:24.512 keke[10214:10428126] 即将处理timer
2018-04-10 10:19:24.512 keke[10214:10428126] 即将处理input Sources
2018-04-10 10:19:24.512 keke[10214:10428126] 即将睡眠
2018-04-10 10:19:24.512 keke[10214:10428126] 从睡眠中唤醒,处理完唤醒源之前
2018-04-10 10:19:24.512 keke[10214:10428126] —fire—
…
2018-04-10 10:19:24.512 keke[10214:10428126] 即将处理timer
2018-04-10 10:19:24.512 keke[10214:10428126] 即将处理input Sources
2018-04-10 10:19:24.512 keke[10214:10428126] 即将睡眠
2018-04-10 10:19:24.512 keke[10214:10428126] 从睡眠中唤醒,处理完唤醒源之前
2018-04-10 10:19:24.512 keke[10214:10428126] —fire—
…
…
以上是关于NSRunLoop-使用场景分析的主要内容,如果未能解决你的问题,请参考以下文章
Android 逆向函数拦截 ( 使用 cache_flush 系统函数刷新 CPU 高速缓存 | 刷新 CPU 高速缓存弊端 | 函数拦截推荐时机 )
collectionView使用reloadData和reloadItemsAtIndexPaths冲突
WPF 的命令的自动刷新时机——当你 CanExecute 会返回 true 但命令依旧不可用时可能是这些原因