深入理解RunLoop

作者: 小凉介 | 来源:发表于2018-03-25 15:54 被阅读902次

    最近看了很多RunLoop的文章,看完很懵逼,决心整理一下,文章中大部分内容都是引用大神们的,但好歹对自己有个交代了,花了一个周天加几个晚上熬夜完成的,有个产出还是很爽的,不多比比了,下面开始吧。

    什么是RunLoop?

    RunLoop是一个接收处理异步消息事件的循环,一个循环中:等待事件发生,然后将这个事件送到能处理它的地方。

    RunLoop实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件)和消息,从而保持程序的持续运行;而且在没有事件处理的时候,会进入睡眠模式,从而节省CPU资源,提高程序性能。

    Event Loop模型伪代码

    int main(int argc, char * argv[]) {    
         //程序一直运行状态
         while (AppIsRunning) {
              //睡眠状态,等待唤醒事件
              id whoWakesMe = SleepForWakingU  p();
              //得到唤醒事件
              id event = GetEvent(whoWakesMe);
              //开始处理事件
              HandleEvent(event);
         }
         return 0;
    }
    
    Screen Shot 2018-03-25 at 3.41.32 PM.png
    • mach kernel属于苹果内核,RunLoop依靠它实现了休眠和唤醒而避免了CPU的空转。
    • Runloop是基于pthread进行管理的,pthread是基于c的跨平台多线程操作底层API。它是mach thread的上层封装(可以参见Kernel Programming Guide),和NSThread一一对应(而NSThread是一套面向对象的API,所以在iOS开发中我们也几乎不用直接使用pthread)。
    Screen Shot 2018-03-25 at 4.28.26 PM.png

    RunLoop的组成

    RunLoop构成

    CFRunLoop对象可以检测某个task或者dispatch的输入事件,当检测到有输入源事件,CFRunLoop将会将其加入到线程中进行处理。比方说用户输入事件、网络连接事件、周期性或者延时事件、异步的回调等。

    RunLoop可以检测的事件类型一共有3种,分别是CFRunLoopSource、CFRunLoopTimer、CFRunLoopObserver。可以通过CFRunLoopAddSource, CFRunLoopAddTimer或者CFRunLoopAddObserver添加相应的事件类型。

    要让一个RunLoop跑起来还需要run loop modes,每一个source, timer和observer添加到RunLoop中时必须要与一个模式(CFRunLoopMode)相关联才可以运行。

    上面是对于CFRunLoop官方文档的解释

    RunLoop的主要组成
    RunLoop共包含5个类,但公开的只有Source、Timer、Observer相关的三个类。

    CFRunLoopRef
    CFRunLoopModeRef
    CFRunLoopSourceRef
    CFRunLoopTimerRef
    CFRunLoopObserverRef
    
    
    image

    CFRunLoopSourceRef
    source是RunLoop的数据源(输入源)的抽象类(protocol),Source有两个版本:Source0 和 Source1

    • source0:只包含了一个回调(函数指针),使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。处理App内部事件,App自己负责管理(出发),如UIEvent(Touch事件等,GS发起到RunLoop运行再到事件回调到UI)、CFSocketRef。
    • Source1:由RunLoop和内核管理,由mach_port驱动(特指port-based事件),如CFMachPort、CFMessagePort、NSSocketPort。特别要注意一下Mach port的概念,它是一个轻量级的进程间通讯的方式,可以理解为它是一个通讯通道,假如同时有几个进程都挂在这个通道上,那么其它进程向这个通道发送消息后,这些挂在这个通道上的进程都可以收到相应的消息。这个Port的概念非常重要,因为它是RunLoop休眠和被唤醒的关键,它是RunLoop与系统内核进行消息通讯的窗口。

    CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用(底层基于使用mk_timer实现)。它受RunLoop的Mode影响(GCD的定时器不受RunLoop的Mode影响),当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。如果线程阻塞或者不在这个Mode下,触发点将不会执行,一直等到下一个周期时间点触发。

    CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个

    enum CFRunLoopActivity {
        kCFRunLoopEntry              = (1 << 0),    // 即将进入Loop   
        kCFRunLoopBeforeTimers      = (1 << 1),    // 即将处理 Timer        
        kCFRunLoopBeforeSources     = (1 << 2),    // 即将处理 Source  
        kCFRunLoopBeforeWaiting     = (1 << 5),    // 即将进入休眠     
        kCFRunLoopAfterWaiting      = (1 << 6),    // 刚从休眠中唤醒   
        kCFRunLoopExit               = (1 << 7),    // 即将退出Loop  
        kCFRunLoopAllActivities     = 0x0FFFFFFFU  // 包含上面所有状态  
    };
    typedef enum CFRunLoopActivity CFRunLoopActivity;
    
    

    这里要提一句的是,timer和source1(也就是基于port的source)可以反复使用,比如timer设置为repeat,port可以持续接收消息,而source0在一次触发后就会被runloop移除。

    上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

    RunLoop主要处理以下6类事件

    static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
    static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
    static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
    
    

    RunLoop的Mode

    CFRunLoopMode 和 CFRunLoop的结构大致如下:

    struct __CFRunLoopMode {
        CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
        CFMutableSetRef _sources0;    // Set
        CFMutableSetRef _sources1;    // Set
        CFMutableArrayRef _observers; // Array
        CFMutableArrayRef _timers;    // Array
        ...
    };
    
    struct __CFRunLoop {
        CFMutableSetRef _commonModes;     // Set
        CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
        CFRunLoopModeRef _currentMode;    // Current Runloop Mode
        CFMutableSetRef _modes;           // Set
        ...
    }; 
    
    

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

    • kCFDefaultRunLoopMode App的默认Mode,通常主线程是在这个Mode下运行
    • UITrackingRunLoopMode 界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
    • UIInitializationRunLoopMode 在刚启动App时第进入的第一个Mode,启动完成后就不再使用
    • GSEventReceiveRunLoopMode 接受系统事件的内部Mode,通常用不到
    • kCFRunLoopCommonModes 这是一个占位用的Mode,不是一种真正的Mode

    其中kCFDefaultRunLoopMode、UITrackingRunLoopMode是苹果公开的,其余的mode都是无法添加的。那为何我们又可以这么用呢

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

    什么是CommonModes?

    一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里
    主线程的 RunLoop 里有 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode,这两个Mode都已经被标记为”Common”属性。当你创建一个Timer并加到DefaultMode时,Timer会得到重复回调,但此时滑动一个 scrollView 时,RunLoop 会将 mode 切换为TrackingRunLoopMode,这时Timer就不会被回调,并且也不会影响到滑动操作。
    如果想让scrollView滑动时Timer可以正常调用,一种办法就是手动将这个 Timer 分别加入这两个 Mode。另一种方法就是将 Timer 加入到CommonMode 中。

    怎么将事件加入到CommonMode?

    我们调用上面的代码将 Timer 加入到CommonMode 时,但实际并没有 CommonMode,其实系统将这个 Timer 加入到顶层的 RunLoop 的 commonModeItems 中。commonModeItems 会被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。
    这一步其实是系统帮我们将Timer加到了kCFRunLoopDefaultMode和UITrackingRunLoopMode中。

    在项目中最常用的就是设置NSTimer的Mode,比较简单这里就不说了。

    RunLoop运行机制

    image

    当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。每次线程运行RunLoop都会自动处理之前未处理的消息,并且将消息发送给观察者,让事件得到执行。RunLoop运行时首先根据modeName找到对应mode,如果mode里没有source/timer/observer,直接返回。

    /// 用DefaultMode启动
    void CFRunLoopRun(void) {
        CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
    }
     
    /// 用指定的Mode启动,允许设置RunLoop超时时间
    int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
        return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
    }
     
    /// RunLoop的实现
    int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
        
        /// 首先根据modeName找到对应mode
        CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
        /// 如果mode里没有source/timer/observer, 直接返回。
        if (__CFRunLoopModeIsEmpty(currentMode)) return;
        
        /// 1. 通知 Observers: RunLoop 即将进入 loop。
        __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
        
        /// 内部函数,进入loop
        __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
            
            Boolean sourceHandledThisLoop = NO;
            int retVal = 0;
            do {
     
                /// 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
                /// 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
                /// 执行被加入的block
                __CFRunLoopDoBlocks(runloop, currentMode);
                
                /// 4. RunLoop 触发 Source0 (非port) 回调。
                sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
                /// 执行被加入的block
                __CFRunLoopDoBlocks(runloop, currentMode);
     
                /// 5. 如果有 Source1 (基于port) 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息。
                if (__Source0DidDispatchPortLastTime) {
                    Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                    if (hasMsg) goto handle_msg;
                }
                
                /// 通知 Observers: RunLoop 的线程即将进入休眠(sleep)。
                if (!sourceHandledThisLoop) {
                    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
                }
                
                /// 7. 调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
                /// • 一个基于 port 的Source 的事件。
                /// • 一个 Timer 到时间了
                /// • RunLoop 自身的超时时间到了
                /// • 被其他什么调用者手动唤醒
                __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                    mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
                }
     
                /// 8. 通知 Observers: RunLoop 的线程刚刚被唤醒了。
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
                
                /// 收到消息,处理消息。
                handle_msg:
     
                /// 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);
                } 
     
                /// 9.3 如果一个 Source1 (基于port) 发出事件了,处理这个事件
                else {
                    CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                    sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                    if (sourceHandledThisLoop) {
                        mach_msg(reply, MACH_SEND_MSG, reply);
                    }
                }
                
                /// 执行加入到Loop的block
                __CFRunLoopDoBlocks(runloop, currentMode);
                
     
                if (sourceHandledThisLoop && stopAfterHandle) {
                    /// 进入loop时参数说处理完事件就返回。
                    retVal = kCFRunLoopRunHandledSource;
                } else if (timeout) {
                    /// 超出传入参数标记的超时时间了
                    retVal = kCFRunLoopRunTimedOut;
                } else if (__CFRunLoopIsStopped(runloop)) {
                    /// 被外部调用者强制停止了
                    retVal = kCFRunLoopRunStopped;
                } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
                    /// source/timer/observer一个都没有了
                    retVal = kCFRunLoopRunFinished;
                }
                
                /// 如果没超时,mode里没空,loop也没被停止,那继续loop。
            } while (retVal == 0);
        }
        
        /// 10. 通知 Observers: RunLoop 即将退出。
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    } 
    
    

    RunLoop的挂起和唤醒

    RunLoop的挂起

    RunLoop的挂起是通过_CFRunLoopServiceMachPort —call—> mach_msg —call—> mach_msg_trap这个调用顺序来告诉内核RunLoop监听哪个mach_port(上面提到的消息通道),然后等待事件的发生(等待与InputSource、Timer描述内容相关的事件),这样内核就把RunLoop挂起了,即RunLoop休眠了。

    RunLoop的唤醒

    这接种情况下会被唤醒

    1. 存在Source0被标记为待处理,系统调用CFRunLoopWakeUp唤醒线程处理事件
    2. 定时器时间到了
    3. RunLoop自身的超时时间到了
    4. RunLoop外部调用者唤醒

    当RunLoop被挂起后,如果之前监听的事件发生了,由另一个线程(或另一个进程中的某个线程)向内核发送这个mach_port的msg后,trap状态被唤醒,RunLoop继续运行

    处理事件

    1. 如果一个 Timer 到时间了,触发这个Timer的回调
    2. 如果有dispatch到main_queue的block,执行block
    3. 如果一个 Source1 发出事件了,处理这个事件

    事件处理完成进行判断

    1. 进入loop时传入参数指明处理完事件就返回(stopAfterHandle)
    2. 超出传入参数标记的超时时间(timeout)
    3. 被外部调用者强制停止__CFRunLoopIsStopped(runloop)
    4. source/timer/observer 全都空了__CFRunLoopModeIsEmpty(runloop, currentMode)

    RunLoop 的底层实现

    关于这个大家可以看ibireme的深入理解RunLoop一文,我这里选择一些觉得比较重要又不是那么难懂的。
    Mach消息发送机制看这篇文章Mach消息发送机制

    为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作,如下图:

    image

    RunLoop 的核心就是一个 mach_msg() (见上面代码的第7步),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在 mach_msg_trap() 这个地方。

    RunLoop和线程

    RunLoop和线程是息息相关的,我们知道线程的作用是用来执行特定的一个或多个任务,但是在默认情况下,线程执行完之后就会退出,就不能再执行任务了。这时我们就需要采用一种方式来让线程能够处理任务,并不退出。所以,我们就有了RunLoop。

    iOS开发中能遇到两个线程对象: pthread_t和NSThread,pthread_t和NSThread 是一一对应的。比如,你可以通过 pthread_main_thread_np()或 [NSThread mainThread]来获取主线程;也可以通过pthread_self()或[NSThread currentThread]来获取当前线程。CFRunLoop 是基于 pthread 来管理的。

    线程与RunLoop是一一对应的关系(对应关系保存在一个全局的Dictionary里),线程创建之后是没有RunLoop的(主线程除外),RunLoop的创建是发生在第一次获取时,销毁则是在线程结束的时候。只能在当前线程中操作当前线程的RunLoop,而不能去操作其他线程的RunLoop。

    苹果不允许直接创建RunLoop,但是可以通过[NSRunLoop currentRunLoop]或者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时,则需要手动创建和运行RunLoop(尤其是在子线程中, 主线程中的Main RunLoop除外),我看到别人举了这么个例子,很有意思

    调用[NSTimer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:]带有schedule的方法簇来启动Timer.
    
    

    此方法会创建Timer并把Timer放到当前线程的RunLoop中,随后RunLoop会在Timer设定的时间点回调Timer绑定的selector或Invocation。但是,在主线程和子线程中调用此方法的效果是有差异的,即在主线程中调用scheduledTimer方法时timer可以在设定的时间点触发,但是在子线程里则不能触发。这是因为子线程中没有创建RunLoop且更没有启动RunLoop,而主线程中的RunLoop默认是创建好的且一直运行着。所以,子线程中需要像下面这样调用。

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
      [[NSRunLoop currentRunLoop] run];
    });
    
    那为什么下面这样调用同样不会触发Timer呢?
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
      [[NSRunLoop currentRunLoop] run];
      [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
    });  
    
    

    我的分析是:scheduledTimerWithTimeInterval内部在向RunLoop传递Timer时是调用与线程实例相关的单例方法[NSRunLoop currentRunLoop]来获取RunLoop实例的,即RunLoop实例不存在就创建一个与当前线程相关的RunLoop并把Timer传递到RunLoop中,存在则直接传Timer到RunLoop中即可。而在RunLoop开始运行后再向其传递Timer时,由于dispatch_async代码块里的两行代码是顺序执行,[[NSRunLoop currentRunLoop] run]是一个没有结束时间的RunLoop,无法执行到“[NSTimer scheduledTimerWithTimeInterval:…”这一行代码,Timer也就没有被加到当前RunLoop中,所以更不会触发Timer了。

    苹果用 RunLoop 实现的功能

    AutoreleasePool

    App启动之后,系统启动主线程并创建了RunLoop,在main thread中注册了两个observer,回调都是_wrapRunLoopWithAutoreleasePoolHandler()

    第一个Observer监视的事件

    1. 即将进入Loop(kCFRunLoopEntry),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其order是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

    第二个Observer监视了两个事件

    1. 准备进入休眠(kCFRunLoopBeforeWaiting),此时调用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 来释放旧的池并创建新的池。

    2. 即将退出Loop(kCFRunLoopExit)此时调用 _objc_autoreleasePoolPop()释放自动释放池。这个 Observer的order是2147483647,确保池子释放在所有回调之后。

    我们知道AutoRelease对象是被AutoReleasePool管理的,那么AutoRelease对象在什么时候被回收呢?

    第一种情况:在我们自己写的for循环或线程体里,我们都习惯用AutoReleasePool来管理一些临时变量的autorelease,使得在for循环或线程结束后回收AutoReleasePool的时候来回收AutoRelease临时变量。

    另一种情况:我们在主线程里创建了一些AutoRelease对象,这些对象可不能指望在回收Main AutoReleasePool时才被回收,因为App一直运行的过程中Main AutoReleasePool是不会被回收的。那么这种AutoRelease对象的回收就依赖Main RunLoop的运行状态,Main RunLoop的Observer会在Main RunLoop结束休眠被唤醒时(kCFRunLoopAfterWaiting状态)通知UIKit,UIKit收到这一通知后就会调用_CFAutorleasePoolPop方法来回收主线程中的所有AutoRelease对象。

    在主线程中执行代码一般都是写在事件回调或Timer回调中的,这些回调都被加入了main thread的自动释放池中,所以在ARC模式下我们不用关心对象什么时候释放,也不用去创建和管理pool。(如果事件不在主线程中要注意创建自动释放池,否则可能会出现内存泄漏)。

    NSTimer(timer触发)

    上文说到了CFRunLoopTimerRef,其实NSTimer的原型就是CFRunLoopTimerRef。一个Timer注册 RunLoop 之后,RunLoop 会为这个Timer的重复时间点注册好事件。有两点需要注意:

    1. 但是需要注意的是RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。这个误差默认为0,我们可以手动设置这个误差。文档最后还强调了,为了防止时间点偏移,系统有权力给这个属性设置一个值无论你设置的值是多少,即使RunLoop 模式正确,当前线程并不阻塞,系统依然可能会在 NSTimer 上加上很小的的容差。
    2. 我们在哪个线程调用 NSTimer 就必须在哪个线程终止

    在RunLoop的Mode中也有说到,NSTimer使用的时候注意Mode,比如我之前开发时候用NSTimer写一个Banner图片轮播框架,如果不设置Timer的Mode为commonModes那么在滑动TableView的时候Banner就停止轮播

    DispatchQueue.global().async {
        // 非主线程不能使用 Timer.scheduledTimer进行初始化
    //                    self.timer = Timer.scheduledTimer(timeInterval: 6.0, target: self, selector: #selector(TurnPlayerView.didTurnPlay), userInfo: nil, repeats: false)
        
        if #available(iOS 10.0, *) {
            self.timer = Timer(timeInterval: 6.0, repeats: true, block: { (timer) in
                self.setContentOffset(CGPoint(x: self.frame.width*2, y: self.contentOffset.y), animated: true)
            })
        } else {
            // Fallback on earlier versions
        }
        
        
        RunLoop.main.add(self.timer!, forMode: RunLoopMode.commonModes)
    }
    
    

    和GCD的关系

    1. RunLoop底层用到GCD
    2. RunLoop与GCD并没有直接关系,但当GCD使用到main_queue时才有关系,如下:
    //实验GCD Timer 与 Runloop的关系,只有当dispatch_get_main_queue时才与RunLoop有关系
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        NSLog(@"GCD Timer...");
    });
    

    当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。同理,GCD的dispatch_after在dispatch到main_queue时的timer机制才与RunLoop相关。

    PerformSelecter

    NSObject的performSelecter:afterDelay: 实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
    NSObject的performSelector:onThread: 实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
    其实这种方式有种说法也叫创建常驻线程(内存),AFNetworking也用到这种技法。举个例子,如果把RunLoop去掉,那么test方法就不会执行。

    
    class SecondViewController: UIViewController {
        
        var thread: Thread!
        
        override func viewDidLoad() {
            super.viewDidLoad()
            self.view.backgroundColor = UIColor.red
            thread = Thread.init(target: self, selector: #selector(SecondViewController.run), object: nil)
            thread.start()
            
        }
        
        @objc func run() {
            
            print("run -- ")
            RunLoop.current.add(Port(), forMode: .defaultRunLoopMode)
            RunLoop.current.run()
        }
        
        @objc func test() {
            print("test --  \(Thread.current)")
        }
        
        
        override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
            //        self.test()
            
            self.perform(#selector(SecondViewController.test), on: thread, with: nil, waitUntilDone: false)
            
        }
        
        
    }
    
    

    网络请求

    iOS中的网络请求接口自下而上有这么几层

    Screen Shot 2018-03-26 at 2.13.00 PM.png

    其中CFSocket和CFNetwork偏底层,早些时候比较知名的网络框架AFNetworking是基于NSURLConnection编写的,iOS7之后新增了NSURLSession,NSURLSession的底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),之后AFNetworking和Alamofire就是基于它封装的了。

    image

    通常使用 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通过一些基于mach port的Source1接收来自底层CFSocket的通知。当收到通知后,其会在合适的时机向CFMultiplexerSource等Source0发送通知,同时唤醒Delegate线程的RunLoop来让其处理这些通知。CFMultiplexerSource会在Delegate线程的RunLoop对Delegate执行实际的回调。

    事件响应

    苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。

    当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。

    触摸事件其实是Source1接收系统事件后在回调 __IOHIDEventSystemClientQueueCallback()内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。source0一定是要唤醒runloop及时响应并执行的,如果runloop此时在休眠等待系统的 mach_msg事件,那么就会通过source1来唤醒runloop执行。

    _UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。

    image

    手势识别

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

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

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

    UI更新

    Core Animation 在 RunLoop 中注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件 。当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。当Oberver监听的事件到来时,回调执行函数中会遍历所有待处理的UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

    如果此处有动画,通过 DisplayLink 稳定的刷新机制会不断的唤醒runloop,使得不断的有机会触发observer回调,从而根据时间来不断更新这个动画的属性值并绘制出来。

    函数内部的调用栈

    _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];
    
    

    绘图和动画有两种处理的方式:CPU(中央处理器)和GPU(图形处理器)
    CPU: CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等
    GPU: GPU 进行变换、合成、渲染.

    关于CADisplayLink的描述有两种

    CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。

    CADisplayLink是一个执行频率(fps)和屏幕刷新相同(可以修改preferredFramesPerSecond改变刷新频率)的定时器,它也需要加入到RunLoop才能执行。与NSTimer类似,CADisplayLink同样是基于CFRunloopTimerRef实现,底层使用mk_timer(可以比较加入到RunLoop前后RunLoop中timer的变化)。和NSTimer相比它精度更高(尽管NSTimer也可以修改精度),不过和NStimer类似的是如果遇到大任务它仍然存在丢帧现象。通常情况下CADisaplayLink用于构建帧动画,看起来相对更加流畅,而NSTimer则有更广泛的用处。

    不管怎么样CADisplayLink和NSTimer是有很大不同的,详情可以参考这篇文章CADisplayLink

    ibireme根据CADisplayLink的特性写了个FPS指示器YYFPSLabel,代码非常少
    原理是这样的:既然CADisplayLink可以以屏幕刷新的频率调用指定selector,而且iOS系统中正常的屏幕刷新率为60Hz(60次每秒),所以使用 CADisplayLink 的 timestamp 属性,配合 timer 的执行次数计算得出FPS数

    参考文章
    深入理解RunLoop
    iOS 事件处理机制与图像渲染过程
    RunLoop学习笔记(一) 基本原理介绍
    iOS刨根问底-深入理解RunLoop
    【iOS程序启动与运转】- RunLoop个人小结
    RunLoop的前世今生
    Runloop知识树

    相关文章

      网友评论

      • 搬砖的小红帽:那为什么下面这样调用同样不会触发Timer呢?
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        [[NSRunLoop currentRunLoop] run];
        [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
        });

        我在scheduledTimerWithTimeInterval方法后再调用 [[NSRunLoop currentRunLoop] run]方法,定时器被触发了;在scheduledTimerWithTimeInterval方法后随便打印一个东西,也能执行。
        所以这个问题,我认为是执行第一行代码时,runloop里面的mode是空的,然后runloop退出了,第二行代码把timer加到runloop里,但是并没有调用 [[NSRunLoop currentRunLoop] run]让runloop跑起来,所以没有触发定时器
      • 魂灬淡::flushed: 你自己写了哪几个字
      • CoderDancer:问下,子线程,默认不开启runloop的情况下,autorealeasepool怎么执行的
      • 新地球说着一口陌生腔调:你好 请问一下 你这个是从英文文档翻译过来的吗
        小凉介:@新地球说着一口陌生腔调 参考了很多文章,只能说参考的原文章作者很牛:smile:
        新地球说着一口陌生腔调:那就是你自己理解的?这么🐂啊 佩服
        小凉介:@新地球说着一口陌生腔调 不是的

      本文标题:深入理解RunLoop

      本文链接:https://www.haomeiwen.com/subject/mlahcftx.html