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 高速缓存弊端 | 函数拦截推荐时机 )

NSRunLoop

collectionView使用reloadData和reloadItemsAtIndexPaths冲突

WPF 的命令的自动刷新时机——当你 CanExecute 会返回 true 但命令依旧不可用时可能是这些原因

使用 NSAutoreleasePool 从线程方法返回后 NSRunLoop 出错

NSRunLoop 阻塞传入数据