RunLoop

2017-08-15  本文已影响15人  纯情_小火鸡

我们知道NSTimer是不精准的,如果NSTimer当前所处的线程正在进行大数据处理(假设为一个大循环),NSTimer本次执行会等到这个大数据处理完毕之后才会继续执行。这是因为我们创建的NSTimer默认是被主线程添加到NSDefaultRunLoopMode模式下,一般情况,我们如果想要NSTimer准确,可以将其添加到NSRunLoopCommonModes:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

那么问题就来了,为什么能这么做呢?这就是我们今天的主角RunLoop了。

  1. 基本概念

  2. RunLoop 与线程的关系

  3. 接口及其联系

  4. 调用流程

  5. 应用

1. 基本概念

RunLoop是与线程相关的基本基础设施的一部分。一个RunLoop是一个事件处理循环,用于调度工作并协调传入事件的接收。。目的是在有工作要做的时候保持线程的忙碌,闲置的情况下让线程休眠。

一般来讲,一个线程一次只能执行一个任务,执行完成后线程就会退出。如果我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑是这样的:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

这种模型通常被称作 Event Loop 。RunLoop就是采用的这种机制(通常所说的RunLoop指的是NSRunloop或者CFRunloopRef,CFRunloopRef是纯C的函数,而NSRunloop仅仅是CFRunloopRef的OC封装,并未提供额外的其他功能),安卓里面的Looper机制和此类似。可以理解为一种高级的循环,当有事件发生时, RunLoop 会去找对应的 Handler 处理事件;当没有事件时,RunLoop 会进入休眠状态。如下图,可以看出他的工作流程:从 input source 和 timer source 中接受到事件,然后在线程中去处理事件。

runloop.jpg
Input Source 和 Timer Source

这两个都是 RunLoop 事件的来源,Input Source 传送来自其他应用或线程的异步事件/消息;Input Source 又可以分为三类(按照函数调用栈又可分为Source0和Source1,见CFRunLoopSourceRef)。

Timer Source 传送的是基于定时器的同步事件,可以定时或重复发送。

2. RunLoop 与线程的关系

苹果不允许直接创建 RunLoop,但是提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 这两个函数内部的逻辑大概是下面这样:

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
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());
}

结论:线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)。

3. 接口及其联系

在 CoreFoundation/CFRunLoop中,定义了5个struct:

线程和 RunLoop 是一一对应的,一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer,Source又按照事件分为Source0 和 Source1。

每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

如下图:

2017-03-31-RunLoop结构.png RunLoop_0.png
1. CFRunLoopMode:

系统默认注册了五中Mode:

iOS 中暴露出来的只有 NSDefaultRunLoopMode 和 NSRunLoopCommonModes。 NSRunLoopCommonModes 实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode。

2. CFRunLoopRef:

RunLoop对象,NSRunLoop是基于CFRunLoopRef的一层OC包装。

3. CFRunLoopSourceRef:

是事件产生的地方。Source又分为2种:Source0 和 Source1。

4. CFRunLoopObserverRef:

每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。

RunLoop状态可分为:

5. CFRunLoopTimerRef:

4. 调用流程

RunLoop_1.png

5. 应用

1. AutoreleasePool

当App启动之后,系统会启动主线程并创建RunLoop,在 main thread 中注册了两个 observer ,回调都是_wrapRunLoopWithAutoreleasePoolHandler(),分别会在进去Loop的kCFRunLoopEntry状态回调调用_objc_autoreleasePoolPush方法创建AutoreleasePool;在kCFRunLoopBeforeWaiting进入休眠时调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 来释放旧的池并创建新的池;以及在kCFRunLoopExit退出时释放。

2. 事件响应

系统注册了一个 Source1 用于接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

3. 手势识别

当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。

苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

4. 界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

这个函数内部的调用栈大概是这样的:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
    QuartzCore:CA::Transaction::observer_callback:
        CA::Transaction::commit();
            CA::Context::commit_transaction();
                CA::Layer::layout_and_display_if_needed();
                    CA::Layer::layout_if_needed();
                        [CALayer layoutSublayers];
                            [UIView layoutSubviews];
                    CA::Layer::display_if_needed();
                        [CALayer display];
                            [UIView drawRect];

5. 定时器

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 相似),造成界面卡顿的感觉。

NSTimer的创建通常有两种方式,尽管都是类方法,一种是timerWithXXX,另一种scheduedTimerWithXXX。二者最大的区别就是后者除了创建一个定时器外会自动以NSDefaultRunLoopMode添加到当前线程RunLoop中,不添加到RunLoop中的NSTimer是无法正常工作的。

6. performSelecter

当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

7. RunLoop 与 GCD

RunLoop 与 GCD 是互相协作的关系,RunLoop 的最开始部分使用了 GCD 的 timer 做超时的回调;通过 GCD 调用带有 RunLoop 的线程的 block,会通过 dispatch port CFRunLoopServiceMachPort 把事件发送到该线程的 RunLoop 里面。

比如:

dispatch_async(dispatch_get_main_queue(), ^{});

主线程存在 RunLoop,那么 GCD 会通过 dispatch port CFRunLoopServiceMachPort,把事件发送给 RunLoop,RunLoop 接收到事件后,会执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

6. 使用RunLoop

在次级线程运行 run loop 之前,必须向其添加至少一个 input source 或 timer,否则 run loop 会因没有可监控的 source 而在运行后立刻退出。

除了用 source 外,还可以用 run loop observer 观察 run loop 的各种运行阶段。做法是创建一个 CFRunLoopObserverRef 类型的对象并用 CFRunLoopAddObserver 函数将其添加到 run loop 中。注意的是只能用 Core Foundation 创建 run loop observer,Cocoa 框架无能为力。

下面的示例代码在线程入口函数中创建了 run loop observer 并将其添加到 run loop 中。observer 监听了 run loop 所有的活动,并省略了回调函数 myRunLoopObserver 的实现。

- (void)threadMain
{
    // The application uses garbage collection, so no autorelease pool is needed.
    NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
 
    // Create a run loop observer and attach it to the run loop.
    CFRunLoopObserverContext  context = {0, self, NULL, NULL, NULL};
    CFRunLoopObserverRef    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
            kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
 
    if (observer)
    {
        CFRunLoopRef    cfLoop = [myRunLoop getCFRunLoop];
        CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
    }
 
    // Create and schedule the timer.
    [NSTimer scheduledTimerWithTimeInterval:0.1 target:self
                selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
 
    NSInteger    loopCount = 10;
    do
    {
        // Run the run loop 10 times to let the timer fire.
        [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
        loopCount--;
    }
    while (loopCount);
}

为了不让 run loop 刚运行就立刻退出,上面的代码向 run loop 添加了一个 timer。因为 timer 一旦触发就无效了,依然会导致 run loop 退出,所以这里 repeats 参数传入 YES。但这样会让 run loop 一直运行很久,并需要周期性触发 timer 来唤醒线程,这实际上是轮询的另一种形式罢了。相比之下,input source 等待事件发生后才唤醒线程,在这之前线程保持休眠。

参考文献

上一篇 下一篇

猜你喜欢

热点阅读