RunLoop研究

目录:

  • App启动原理
  • RunLoop 的概念
  • RunLoop 与线程的关系
  • RunLoop 对外的接口
  • RunLoop 的 Mode
  • RunLoop 的内部逻辑
  • RunLoop 的底层实现
  • RunLoop 实现的功能
  • AutoreleasePool
  • 事件响应
  • 手势识别
  • 界面更新
  • 定时器
  • PerformSelecter
  • GCD与RunLoop
  • 网络请求与RunLoop
  • RunLoop 的实际应用举例
    • 常驻线程
    • 计时器与滑动冲突
    • 回光返照
    • tableView卡顿优化

启动原理

从app启动开始,调用堆栈的信息可以看出,程序启动时,默认开启了一个runLoop,存在于Thread1。

RunLoop研究_第1张图片
Snip20161204_5.png

上图中,系统首先调用 start函数,程序开始执行main函数,

       int main(int argc, char * argv[]) {
        @autoreleasepool {
        return UIApplicationMain(argc, argv, nil,   NSStringFromClass([AppDelegate class]));
        }
     }
  • 系统首先通过lib system调用start函数之后,启动main函数(程序的唯一入口)
  • 接着通过main函数的入口函数传入程序运行所需要的参数, 所有args均保存在argv[]数组中.通过对argv[]中的参数进行遍历打印得出2016-12-04 10:55:11.177 08.UI动画特效[21542:1239589] /Users/lx/Library/Developer/CoreSimulator/Devices/E6FC0E7F-0B8B-4D76-9E98-A1A7030C5BAD/data/Containers/Bundle/Application/9AE9DF6D-0A6E-4F5B-B16E-E36954B7F9DE/08.UIÂä®ÁîªÁâπÊïà.app/08.UIÂä®ÁîªÁâπÊïà
    第一个参数为引用程序的执行路径。由此得出,main函数传入的参数为应用程序的执行文件,和其它的辅助参数,第一个参数为应用程序的执行文件。
  • 然后系统开启了一个autoreleasepool{} 它的作用主要是为了适配ARC环境,手动的对中间创建的变量内存空间进行自动管理。由于这个释放池包含了下面的return中的函数,所以return函数中的资源,每次执行完都会被释放。
  • 接着引用程序开启了一个函数UIApplicaitonMain函数。
    这个函数承接main函数传入的参数。通过上图可以看出
    • 开启了一个CFRunLoopRunSpecific,该特殊的runLoop主要是为乐保证程序在启动的过程中不受其它外部输入事件的干扰
    • 同时也启动了一个GSEventRunModal,此为GraphicsServices库执行,主要是为处理程序的硬件输入事件。
    • __CFRunloopServiceMachPort是因为手动暂停,调用了系统内核服务,系统内核给程序发送 mach_msg_trap的消息,程序进入暂停。
    • 此时如果我门不进行暂停,则程序会进入mainRunLoop事件循环,一直接收事件。
    • This function is called in the main entry point to create the application object and the application delegate and set up the event cycle.,用苹果官方的话来说,UIApplicaitonMain它启动程序,同时开启了一个UIApplicaiton的代理,通过反射的方式注册了它的代理类AppDelegate(当然我们业可以集成UIResponse类来自己一个代理类),同时创建了一个程序运行的主runLoop,用于监听程序的输入事件(如人工输入,网路数据输入).

runLoop的概念

  • 下面用一段为代码来做说明。
           while(isLiving) {
            id wakeObj = WakeUpFromSleeping();
            id Event   = GetEvent(wakeObj);
            if(crash || userQuit){
            isLiving = NO; 
           }
           }
    
* 通过上面的为代码将runLoop简写成了一个死循环,在每次的循环周期类,它不停的接受事件,处理事件,等待唤醒和睡眠.
在没有事情做的时候,它会进入睡眠状态,释放CPU资源,在程序接受到事件后,它会从消息队列(messageQueue)中一条一条的将事件取出,然后进行执行,这里只是初步介绍一下,实际在每次runLoop过程中它还进行了 界面刷新,GCD分派事件的调度,系统内核MarchPort事件处理,以及系统强制插入的程序中的其它紧急事件处理。后面会进行一一介绍。
* 总的来说,mainRunLoop主要干了三件事情:
   + 让程序一直活着,保持持续的监听(网络或者用户的输入)
   + 解耦,通过从消息队列的轮询,使得接受到的事件能有序的进行处理(防止一个触摸事件之后,后面的事件都没办法处理)。
   + 节省CPU资源,这个效果就非常明显了,睡眠就是最好的实现方式。

#### RunLoop 与线程的关系
+ 下面先来看一段源码:
```objective-c
          /// 全局的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());
           }
  • 上述代码中定义了两个重要的函数CFRunLoopGetMain()和CFRunLoopGetCurrent();在初始化获取的时候,首先会去全局字典中获取,当没有的时候就会重新根据对应的线程生成一个runLoop,它的key及为当前对应的线程,所以从这里可以看出,每个线程与它的RunLoop是一一对应的。同时也注册了一个线程销毁时的回调block _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
  • 总结他们的关系如下:
    • 主线程在程序运行的时候会创建唯一的一个MainRunLoop(根runLoop,实际测试还可以嵌套,因为它时while循环为前提的,在一个runLoop上可以嵌套其他的runLoop),在这里称最上层的RunLoop为根runLoop.
    • 线程与RunLoop之间是一一对应的,子线程需要通过CFGetCurrentRunLoop才能获取对应的runLoop。
    • 线程销毁的时候,和它绑定的runLoop也会一起销毁。

RunLoop关键类

  • 通过CoreFoundation的源码介绍中,主要包含一下几个类.
    • CFRunLoopRef
    • CFRunLoopModeRef //runLoop的运行
    • CFRunLoopSourceRef //runLoop对应的事件
    • CFRunLoopTimerRef //runLoop对应计时器
    • CFRunLoopObserverRef //runLoop状态观察者
    • 一个runLoop包含若干的Mode,常用的有NSDefaultRunLoopMode,UITrackingRunLoopMode;
    • 一个Mode对应若干个source和若干的timers,以及若干的observers,
    • 苹果规定程序一次只能运行一个Mode,所以在runLoop切换Mode的时候只能退出当前的Mode,再重新指定一个新的Mode,这样做能够将不同组之间的timers/sources/observes分离开来,便于管理。通常我们把runLoop运行的模式叫做CurrentMode。
    • 苹果指定了一个非常特殊的Mode,那就是UITrackingMode,该Mode模式下,runLoop只处理滑动事件,保证用户的滑动不受其它事件的干扰。但是这个模式会和添加到DefaultMode下的timers存在冲突,下面会讲道解决方案。
    • CFRunLoopSourceRef,主要是用来定义系统事件,和程序运行执行过程中的外部输入事件,如内核MachPort派发事件,CFSocket网络资源输入事件,外部touch事件,以及程序运行代码手动添加的事件。为了便于区分和管理. 苹果设定了两种source事件类型。
      • source0: 它主要用于处理应用程序负责管理的事件,如UIEvent事件,CFSocket输入事件。它包含了一个回调指针,并不能主动的触发事件,而是依靠外部的事件来触发,比如用户点击了某个地方,或者网络接口的数据来了。需要先调用CFRunLoopSourceSingal(source),将这个source标记为待处理,然后手动调用CFRunLoopWakUp(runLoop)来唤醒runLoop后处理该事件。
      • source1: 主要是用来处理系统派发的事件,或者进程间通信(实际用不上,iOS为单进程,通常一次只能运行一个app).它是由系统内核来管理的,由MachPort进行驱动,如CFMatchPort(NSPort是对它的封装),CFMessagePort,每个进程被操作系统绑定了不同的PortId,通常情况下通过对该portId发送消息会触发source1事件。比如点击Xcode的暂停,起始底层调用了mach_msg_trap,使得程序进入暂停。
  • CFRunLoopTimerRef 是基于时间的触发器,(NSTimer是对它的上层封装).它包括了一个事件长度和一个回调指针。当加入到RunLoop的时候,RunLoop会注册对应的时间点,当时间到的时候,RunLoop会被自己注册的时间节点事件回调并唤醒,执行注册的时间节点数据。实际上我们app中有许多的时间都是依赖与NSTimer和CFRunLoopTimerRef,例如 CAAniamtion,CATransition,Push,Pop,CADisplayXXX,timerScheduleXXX,timerIntervalXXX,performDelay,PerformAfterXXX都是基于注册时间片回调事件来执行的。这里需要注意的是在执行动画操作的时候, RunLoop通常是在这一轮的循环中搜集完所有的动画执行事件之后再进行集中处理。
  • CFRunLoopObserverRef, 观察者的主要作用就是监视runLoop的生命周期和活动变化,简单来说吧就是 runLoop的创建-> 运行-> 挂起->唤醒->.... >死亡。runLoop在每次运行循环中把自己的状态变化通过注册回调指针告诉对应的observe,这样它的每一次状态变化时,observe都能通过回调指针获取它对应的状态,进行相关的处理。附上观察的时间点。
          typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
          kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
          kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
          kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
          kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
          kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
          kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
          };

RunLoop 的 Mode

  • 前面提到的 source,timer,observe 他们分别以set,array,array的方式保存在Mode中。这里先把每个source,timer,observe称作为一个item,便于后文描述。 在RunLoop内部做了相关的处理,当相同的几个Item添加到同一mode中是不会有效果的,相同的item可以添加到不同的Mode中去,当一个Mode中没有Item的时候,那么这个Mode就会自动退出, 这点不难理解,mode存在的意义就是为了让RunLoop在指定的模式上,执行指定的事件,当没有Item去执行的时候,这个Mode也就不存在了。RunLoop也就退出了。
  • CFRunLoop和CFRunLoopMode的结构
        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
        CFRunLoopModeRef _currentMode;    // Current Runloop Mode
        CFMutableSetRef _modes;           // Set
   
        };
  • 这里有个commonModes的概念, 可以将Mode标记为Common属性,当切换RunLoop模式的时候,runLoop会自动将commonModelItems集合中的 items(source/timer/observe)同步到具有Common标记的所有Mode中。它就相当于一个占位作用,放边我门将两种不同的Mode下的Item内容组合到一起执行。 比如 我门将NSTimer添加到NSDefaultRunLoopMode中,在执行滑动的时候,NSTimer事件就不会执行,这是应为在滑动的时候系统会以用户的滑动为主,优先切换到 UITrackingRunLoopMode模式下,如果我门将 timer添加到commonRunLoopMode下面,则RunLoop在切换模式的时候,会将Timer事件同步到common标记的Mode中,继续执行。另外一种方式直接将Timer添加打UITrackingRunLoopMode中。

RunLoop 的内部逻辑

  • 这里引用苹果的一张图,这张图也看了很多遍了,每次看都会有新的不同的见解,这里说一下我个人的看法,有什么不对的地方还请大家指教。


    RunLoop研究_第2张图片
    Snip20161204_6.png
  • 左边表示了某个线程对应RunLoop每次循环执行的事件
    • start表示runLoop开始执行,end表示本次循环结束
    • runUntilDate: 表示runTime执行在循环监听,直到监听到需要处理某件事情。这一点可以通过它下方定义的四个事件看出。
    • handlePort: 应该是处理端口发送的消息事件,主要是来自系统内核派发的事件。
    • customSrc: 用户产生的source事件,例如,UIEvent事件,活着其它代码触发的事件。
    • mySelector: 用户定义了@selector事件。
    • timerFired:主要用于界面刷新,及其它和NSTimer相关的需要注册事件回调的事件。
  • 右边定义了两大块的输入事件,inputSources:
    • 其中Port 是基于系统内核的 inputSource事件, 主要来自于MachPort的消息事件。 custom为用户输入产生的source事件,如UIEvent。performSelector:OnThread:强行往某个线程中注册方法,主要用于线程间通信。对应线程会在合适的时间执行这个方法
    • timer sources ,这一类时间主要基于注册时间节点的回调指针来实现,比如 界面的周期性刷新,画面专场动画,延迟执行,计时器任务等。
  • 接下来看看RunLoop每次while循环中执行了哪些事
          /// 此处为程序启动时指定的RunLoop用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运行过程中的一次完整的执行流程.
    1. 首先告知 observe将要进入RunLoop此次事件循
    2. 通知observe将要处理timer(基于timer时间片注册的回调时间)
    3. 通知observe将要处理source0事件(外界触发产生的事件,同常是从消息队列依次取出执行)。
    4. 处理source0事件(从messageQueue取出事件执行)
    5. 看是否有基于MachPort派发的事件,有则直接进行条转到上图步骤9中下方,
    6. 判断timer注册事件是否到了并执行
    7. 判断有无GCD派发到主线程的事件并执行
    8. 如果是基于MachPort的事件触发了,则执行machPort事件。
    9. 如果没有事件执行,则进行挂起,等待外界通知,timer唤醒,machPort发送消息唤醒,活着被其它调用这手动唤醒
    10. 最后在程序退出的时候结束每次循环。
  • 总的来说在一次循环过程中,它需要干着么多事情, 告诉观察者自己的状态,以便观察者在合适的时机(通常用于休眠时处理对应注册的事件) ,注册timer回调事件,处理消息队列添加的事件,处理手动加入的block块事件,处理GCD派发的事件,处理来自系统内核发送的指令事件,实时的判定是否超时,是否接到外部退出指令事件。

RunLoop 功能实现

        CFRunLoop {
          current mode = kCFRunLoopDefaultMode
          common modes = {
            UITrackingRunLoopMode
            kCFRunLoopDefaultMode
        }
  
        common mode items = {
  
        // source0 (manual)
        CFRunLoopSource {order =-1, {
            callout = _UIApplicationHandleEventQueue}}
        CFRunLoopSource {order =-1, {
            callout = PurpleEventSignalCallback }}
        CFRunLoopSource {order = 0, {
            callout = FBSSerialQueueRunLoopSourceHandler}}
  
        // source1 (mach port)
        CFRunLoopSource {order = 0,  {port = 17923}}
        CFRunLoopSource {order = 0,  {port = 12039}}
        CFRunLoopSource {order = 0,  {port = 16647}}
        CFRunLoopSource {order =-1, {
            callout = PurpleEventCallback}}
        CFRunLoopSource {order = 0, {port = 2407,
            callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}}
        CFRunLoopSource {order = 0, {port = 1c03,
            callout = __IOHIDEventSystemClientAvailabilityCallback}}
        CFRunLoopSource {order = 0, {port = 1b03,
            callout = __IOHIDEventSystemClientQueueCallback}}
        CFRunLoopSource {order = 1, {port = 1903,
            callout = __IOMIGMachPortPortCallback}}
  
        // Ovserver
        CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry
            callout = _wrapRunLoopWithAutoreleasePoolHandler}
        CFRunLoopObserver {order = 0, activities = 0x20,          // BeforeWaiting
            callout = _UIGestureRecognizerUpdateObserver}
        CFRunLoopObserver {order = 1999000, activities = 0xa0,    // BeforeWaiting | Exit
            callout = _afterCACommitHandler}
        CFRunLoopObserver {order = 2000000, activities = 0xa0,    // BeforeWaiting | Exit
            callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
        CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit
            callout = _wrapRunLoopWithAutoreleasePoolHandler}
  
        // Timer
        CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0,
            next fire date = 453098071 (-4421.76019 @ 96223387169499),
            callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)}
    },
  
        modes = {
        CFRunLoopMode  {
            sources0 =  { /* same as 'common mode items' */ },
            sources1 =  { /* same as 'common mode items' */ },
            observers = { /* same as 'common mode items' */ },
            timers =    { /* same as 'common mode items' */ },
        },
  
          CFRunLoopMode  {
            sources0 =  { /* same as 'common mode items' */ },
            sources1 =  { /* same as 'common mode items' */ },
            observers = { /* same as 'common mode items' */ },
            timers =    { /* same as 'common mode items' */ },
        },
  
          CFRunLoopMode  {
            sources0 = {
                CFRunLoopSource {order = 0, {
                    callout = FBSSerialQueueRunLoopSourceHandler}}
            },
            sources1 = (null),
            observers = {
                CFRunLoopObserver >{activities = 0xa0, order = 2000000,
                    callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
            )},
            timers = (null),
          },
  
            CFRunLoopMode  {
            sources0 = {
                CFRunLoopSource {order = -1, {
                    callout = PurpleEventSignalCallback}}
              },
            sources1 = {
                CFRunLoopSource {order = -1, {
                    callout = PurpleEventCallback}}
            },
              observers = (null),
              timers = (null),
          },
         
          CFRunLoopMode  {
            sources0 = (null),
            sources1 = (null),
            observers = (null),
            timers = (null),
            }
          }
      }
  • 从上面的代码可以看出, runLoop默认为DefaultRunLoop模式,苹果对外暴露了两个Mode, kCFRunLoopDefaultMode,程序在空闲时运行的默认Mode,UITrackingMode,程序在滑动的时候的Mode。他们两个都被标示上了Common标记。程序在正常运行的时候一般会在这两个模式中间进行自由切换,如果采用Timer计时器需要将其也添加到CommonMode中,这样在下一次计时器回调操作里面才会被执行。
  • 在commonMode Items中,包含了runLoop运行所需的sources,timers,observes;
  • source0 默认包含了三个回调指针, 基于外部条件输入事件触发, _UIApplicationHandleEventQueue主要用于处理UIEvent事件,FBSSerialQueueRunLoopSourceHandler主要用于处理应用程序内部服务事件。PurpleEventSignalCallback特定的回调事件。
  • source1事件主要基于matchPort触发,定义了四种回调分别处理系统级别的通知事件。
  • Observer,_wrapRunLoopWithAutoreleasePoolHandler主要用于管理对应的自动释放池的创建和销毁,主要是为了保证在每次挂起和运行期间所有的使用的变量都能正常的销毁和释放。将要开始运行时,创建自动释放池,将挂起活着退出时,销毁自动释放池。 _UIGestureRecognizerUpdateObserver 手势识别的监听者,保证手势滑动与runLoop循环不冲突。_afterCACommitHandler和_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv 应该是为了告知动画执行对象当前runLoop的执行状态。
  • timer: 主要是为定义app调度的时间片,以及对应时间节点需要唤醒执行的的回调方法。
  • 在系统内部还存在很多子定义的Mode,如UIInitializationRunLoopMode,主要是用于程序启动过程中,确保程序运行所需调度的资源不被中断,程序能正常启动。GSEventReceiveRunLoopMode注册程序内部的接受事件的Mode。
  • runLoop的事件执行均是通一系列的回调指针执行的,如下代码所示:
        {
        /// 1. 通知Observers,即将进入RunLoop
          /// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
           __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {
  
        /// 2. 通知 Observers: 即将触发 Timer 回调。
                    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
          /// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
                __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
  
        /// 4. 触发 Source0 (非基于port的) 回调。
            __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(s  ource0);
          __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
  
        /// 6. 通知Observers,即将进入休眠
        /// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
                __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
  
        /// 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();
         
  
        /// 8. 通知Observers,线程被唤醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
  
        /// 9. 如果是被Timer唤醒的,回调Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
  
        /// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
  
        /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
  
  
        } while (...);
  
        /// 10. 通知Observers,即将退出RunLoop
          /// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
          __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION_  _(kCFRunLoopExit);
}

AutoreleasePool

  • 在app启动的时候,manRunLoop的observe中就多了两个_wrapRunLoopWithAutoreleasePoolHandler(),他们的优先级级别在RunLoopEntry阶段最高, BeforeWaiting阶段最低,目的就是为了保证在在执行其它回调函数之前优先执行 _objc_autoreleasePoolPush()创建自动释放池,进行内存管理,在runLoop将要挂起的时候等待其它回调函数都执行完了最后才执行_objc_autoreleasePoolPush()将自动释放池释放,保证分配的临时变量都被销毁。这样所有事件的回调都是自动释放池的创建和销毁之间执行,从而有效避免了内存泄漏。

事件响应

  • 从上面代码可以看出,source1事件有一个__IOHIDEventSystemClientQueueCallback指针,它主要用于接收来自系统级别的事件,当一个硬件事件(触摸/锁屏/摇晃)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后app通过内部source0注册_UIApplicationHandleEventQueue事件,将产生的事件在应用程序内进行派发。通过UIWindow->rootViewController-> subViewControllers/subViews->target(执行则处理事件)-(不执行则传递给上一层)> superViewControllers/superView->UIWindow->UIApplication.

手势识别

  • 当_UIApplicationHandleEventQueue()时,如果检测到了手势,则touchesXXX方法会被打断,随后系统通将对应的UIGestureRecognizer标记为待处理。然后通过RunLoop注册的观察者,当_UIGestureRecognizerUpdateObserver()对runLoop的状态进行监听,如果发现runLoop将要睡觉,系统就会获取之前标记的手势,让runLoop去处理手势的回调事件。

界面更新

当在操作UI,比如改变了frame,更新了UIView/CALayer的层次时,或者手动调用了setNeedsLayout和setNeedsDisplay方法后将这个UIView或者CALayer标记为待处理。并提交到一个全局的容器中去. 由于前面苹果对RunLoop也注册了一个_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv观察者,当观察者发现runLoop将要休息或者空闲的时候,就会让runloop去取前面的标记事件去处理。 这也是为甚么如果界面其它source0事件处理耗时较长的时候主界面会的原因,因为一般用户自定义的事件是在runLoop正常运行时就开始执行,而手势和界面动画,布局相关的事件是在 runLoop收集完这一次循环中标记的所有事件后,并且将要休眠的时候才处理,排布的优先级别较高,如果前面的事件耗时较久,超时了,就会导致后面的界面刷新卡顿,或者直接放次本次循环的刷新。

定时器

NSTimer基于CFRunLoopTimerRef封装,当一个NSTimer注册到RunLoop中后,runLoop会为其重复的事件点注册好事件,为了节省资源,通常情况下runLoop并不会在非常精确的事件点执行这个事件,通常会在注册事件的时候指定一个tolerance(误差值),当标示的时间节点到后,会直接执行,如果执行的事件较长,超过了下一次的事件节点,那么系统就会放次该次的执行。CADisplayLink定义了一个和屏幕刷新频率相同的计时器。

PerformSelecter

当我门调用PerformXXX方法时,系统内部会自动创建一个timer去执行该事件。如果当前的线程没有runLoop,那么该事件便会失效

GCD与RunLoop

  • 从上面runLoop每次while执行的顺序可以看出,有一段是用来询问GCD是否有派遣到mainrunLoop中的任务。
  • 当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

网络请求与RunLoop

  • CFSocket是底层的网络接口,主要负责socket之间的通信。通过远程主机与目标主机之间的IP+PortId传输数据到指定的缓冲区。 CFNetwork基于CFScoket的封装
  • NSURLConnection基于CFNetwork封装,提供面向对象的接口,目前已经被抛弃。
  • NSURLSession用于取代NSURLConnection,底层实现和NSURLConnection相似。通过底层常驻的资源加载子线程(com.apple.NSURLConnectionLoader )接收来自socket的Source1事件。
  • 在网络传输时,NSURLConnection默认创建了两个线程,com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private. 其中CFSocket线程主要处理底层进程间端口通信的。在NSURLConnectionLoader这个线程会使用RunLoop来接收来自底层socket的source1事件。并通过之前添加的 Source0 通知到上层的 Delegate。下图中 CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。
    如下图所示:
    ![Upload Snip20161204_7.png failed. Please try again.]

常驻线程

  • 下面这段代码为AFNetworking中的常驻线程代码实现过程,定义了一个一次任务的子线程,在创建线程的通知指定了一个runLoop,并通过给runLoop添加监听的端口,保持线程不被销毁。这样只需要调用performSelector:onThreadXXX就能让该线程执行我们的任务。
          +(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)networkRequestThreadEntryPoint:(id)__unused object{ 
              @autoreleasepool{
               //设置线程名,获取currentRunloop。设置currentRunLoopPort用语循环监听,保证runLoop不被退出。
             [[NSThread currentThread] setName:@"AFNetworking"];
              NSRunLoop* runLooop = [NSRunLoop currentRunLoop];
               [runLoop addPort:[NSMatchPort port] forMode:NSDefaultRunLoopMode];
           [runLoop run];
            }
             }
    

计时器与滑动冲突

  • 通过利用runLoop的相关特性,可以将NSTimer事件添加到CommonModes中来避免与UITrackingMode的冲突。

回光返照

  • 在runLoop退出时,直接获取当前的runLoop,并获取到它所有的items,让程序再跑一会,比如做一个友情提示.
      CFRunLoopRef runloop = CFRunLoopGetCurrent();
      NSArray* allModes =    CFBridgingRelease(CFRunLoopCopyAllModes(runLoop));
       while(1){
       for(NSString* mode in allModes ) {
        CFRunLoopRunInMode((CFStringRef)mode,0.001,false);
       }
       }

tableView卡顿优化

  • 比如我们通常会在子线程加载图片并在主线程设置图片,但如果用户滑动速度够快,需同时设置的图片数量叫多,这时候为了避免卡顿,则可以通过设置图片延迟执行,活着添加到NSDefaultRunLoopMode中来避免。
       [self.headImage performSelector:@selector(setImage:) withObject:downLoadImage delay:0 inMode:NSDefaultRunLoopMode];

参考文献:
coreFoundation源码: http://opensource.apple.com/tarballs/CF/CF-855.17.tar.gz
runLoop线下分享: http://www.ikoo8.com/video_play/XODgxODkzODI0.html

你可能感兴趣的:(RunLoop研究)