iOS底层原理-RunLoop

  • 准备工作

    RunLoop源码下载地址

  • RunLoop 概念

    • RunLoop 简介

      RunLoop其实就是一种事务处理的循环,是事件接受、分发的机制的实现,用来不停的调度工作以及处理输入事件。其本质就是一个 do-while循环,如果有输入则执行对应事件,没有则休息,这里RunLoopdo-while和普通的do-while循环不一样,普通的循环会让cpu处于忙等待的情况,当cpu跑满了也就崩溃了。而runloopdo-while循环是一种闲等待,也就是不会消耗cpu所以runloop具有休眠功能。
      下图是runloop运行原理图:
      171000560455268.png

  • RunLoop 作用

    • 保持程序持续运行。程序启动就会自动开启一个runloop在主线程
    • 处理App中的各种事件
    • 节省CPU资源,提高程序性能 有事情则做事,没事情则休眠
  • RunLoop 和线程之间的关系

    从上文中的 runloop运行原理图可以知道两者之间的关系是一一对应的关系

  • RunLoop 底层源码分析

    • RunLoop 本质

      RunLoop底层其实就是一个结构体,是一个__CFRunLoop对象,具体结构如下:
      image.png
      从对象的结构中可以得出当前运行的 mode只会有一个,但是每个 runloop里面是存在多个 mode的同时也存在着多个 item也就是事务(source、timer、observer

    • 获取 RunLoop以及进一步验证RunLoop和线程之间的关系
      • 获取主线程runloop: CFRunLoopGetMain()
        image.png发现底层就是调用 _CFRunLoopGet0方法获取 runloop,入参是主线程
      • 获取当前线程runloop: CFRunLoopGetCurrent()
        image.png
        同样的是调用了 _CFRunLoopGet0方法获取 runloop此时的入参是当前线程
      • 获取 runloop: _CFRunLoopGet0
        CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
           if (pthread_equal(t, kNilPthreadT)) {
               t = pthread_main_thread_np();
           }
           __CFSpinLock(&loopsLock);
           if (!__CFRunLoops) {
               //如果存储runloop的字典不存在则创建
               //只有第一次进来的时候才会走到这
               __CFSpinUnlock(&loopsLock);
        
        
               //创建一个临时字典
               CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        
               //创建主线程
               CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        
               // dict : key value
               //将线程和runloop以key-value的形式存到临时字典
               CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        
               //OSAtomicCompareAndSwapPtrBarrier  将临时的字典写入到__CFRunLoops
               if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
                   CFRelease(dict);
               }
        
               //释放mainLoop
               CFRelease(mainLoop);
               __CFSpinLock(&loopsLock);
           }
           //通过线程也就是key 到__CFRunLoops中寻找runloop
           CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
           __CFSpinUnlock(&loopsLock);
           if (!loop) {
               //如果没有找到则基于入参的线程创建一个runloop
               //这里只有可能是获取其他线程的runloop
               CFRunLoopRef newLoop = __CFRunLoopCreate(t);
               __CFSpinLock(&loopsLock);
               loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
               if (!loop) {
                   //此时将新创建的runloop存到__CFRunLoops中
                   CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
                   //赋值操作
                   loop = newLoop;
               }
               // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
               __CFSpinUnlock(&loopsLock);
               //释放新创建的runloop
               CFRelease(newLoop);
           }
           if (pthread_equal(t, pthread_self())) {
               //如果传入线程就是当前线程
               _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
               if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
                   //注册一个回调,当线程销毁时,销毁对应的RunLoop
                   //因为主线程是一直伴随着程序运行的所以不需要注册这个回调
                   _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
               }
           }
           return loop;
        }
        复制代码

        大致流程如下:

        1. 先判断存储 runloop的字典是否存在也就是__CFRunLoops是否存在,如果不存在说明是第一次获取 runloop此时创建主线程 runloop然后存储到 __CFRunLoops
        2. 通过 key也就是入参的线程在 __CFRunLoops中查找 runloop如果找到了给对应的 runloop注册一个回调然后返回,如果没有找到则基于入参的线程创建一个新的 runloop然后存到 __CFRunLoops,然后一样的注册一个回调然后返回

        总结:
        从上述的源码发现底层 runloop的存储使用的是字典结构,线程是 key,对应的 runloopvalue,从这里进一步的印证了线程和 runloop的关系是一一对应的

    • RunLoop 创建

      image.png
      就是开辟空间创建 runloop对象并配置对应参数

    • RunLoop Mode
      struct __CFRunLoopMode {
            CFRuntimeBase _base;
            pthread_mutex_t _lock;    /* must have the run loop locked before locking this */
            CFStringRef _name;
            Boolean _stopped; //mode是否被终止
            char _padding[3];
            //几种事件
            CFMutableSetRef _sources0;  //sources0 集合
            CFMutableSetRef _sources1;   //sources1 集合
            CFMutableArrayRef _observers; //observers 集合
            CFMutableArrayRef _timers;  //timers 集合
            CFMutableDictionaryRef _portToV1SourceMap;
            __CFPortSet _portSet;  //保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中
            CFIndex _observerMask;
        #if USE_DISPATCH_SOURCE_FOR_TIMERS
            dispatch_source_t _timerSource;
            dispatch_queue_t _queue;
            Boolean _timerFired; // set to true by the source when a timer has fired
            Boolean _dispatchTimerArmed;
        #endif
        #if USE_MK_TIMER_TOO
            mach_port_t _timerPort;
            Boolean _mkTimerArmed;
        #endif
        #if DEPLOYMENT_TARGET_WINDOWS
            DWORD _msgQMask;
            void (*_msgPump)(void);
        #endif
            uint64_t _timerSoftDeadline; /* TSR */
            uint64_t _timerHardDeadline; /* TSR */
        };
      
      复制代码

      __CFRunLoopMode的结构体当中我们可以得到的消息是每个 mode中有若干个 source0、source1、timer、observer和若干个端口所以事务的执行是由 mode控制的,而上文张分析 runloop的结构的时候又知道每个 runloop当中都含有多个 mode,但是当前的 mode只有一个,所以从这里可以总结出两点:

      1. runloop管理着 mode
      2. 每次 runloop 运行的时候都必须指定有且仅有一个 mode指定对应的事务,如果需要执行其他 mode的事务需要切换 mode重新进入 runloop
      3. 这里可以总结出 runloop mode 事务、三者的关系如下图

      未命名文件(40).png
      Mode的类型
      从官方文档中可以知道 mode的类型一共五种如下
      image.png
      其中mode在苹果文档中提及的有五个,而在iOS中公开暴露出来的只有 NSDefaultRunLoopModeNSRunLoopCommonModesNSRunLoopCommonModes 实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopModeNSEventTrackingRunLoopMode(当以模态方式跟踪事件(例如鼠标拖动循环)时,应将运行循环设置为此模式。)。可通过 CFRunLoopAddCommonMode添加到 runloop运行模式集合中

    • RunLoop Source

      Run Loop Source分为Source、Observer、Timer三种,也就是 ModeItem

      1. Source
        source又分为 source0source1表示可以唤醒RunLoop的一些事件,例如用户点击了屏幕,就会创建一个RunLoop

        • source0 表示非系统事件,即用户自定义的事件,不能自动唤醒 runloop需要先调用CFRunLoopSourceSignal(source)source置为待处理事件,然后再唤醒 runloop让其处理这个事件
        • source1 由RunLoop和内核管理,source1带有mach_port_t,可以接收内核消息并触发回调,以下是source1的结构体
      2. Observer 主要用于监听RunLoop的状态变化,并作出一定响应,主要有以下一些状态
        image.png
      3. Timer 就是是定时器,可以在设定的时间点抛出回调,

      综上所述得知
      Runloop 通过监控 Source 来决定有没有任务要做,除此之外,我们还可以用 Runloop Observer 来监控 Runloop 本身的状态。 Runloop Observer 可以监控上面的 Runloop 事件,具体流程如下图。
      image.png

    • 添加 mode
      • CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode) 添加一个 moderunloop的 commonmodes中
        image.png
      • CFStringRef CFRunLoopCopyCurrentMode(CFRunLoopRef rl) 返回当前运行的 mode的名称
        image.png
        从这里也可以证明当前运行的 mode是唯一的
      • CFArrayRef CFRunLoopCopyAllModes(CFRunLoopRef rl) 返回 runloop中所有的 mode
        image.png
    • 添加/移除 Source
      • void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode) 添加 source0或者是 source1
        void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {    /* DOES CALLOUT */
          CHECK_FOR_FORK();
          if (__CFRunLoopIsDeallocating(rl)) return;
          if (!__CFIsValid(rls)) return;
          Boolean doVer0Callout = false;
          __CFRunLoopLock(rl);
          if (modeName == kCFRunLoopCommonModes) {
              //如果是kCFRunLoopCommonModes
              //则将对应的source添加到_commonModeItems中
              CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
              if (NULL == rl->_commonModeItems) {
                  //如果没有则创建一个
                  rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
              }
              //
              CFSetAddValue(rl->_commonModeItems, rls);
              if (NULL != set) {
                  //如果set 存在则将source添加到每一个common-modes中去
                  //这里需要解释一下 应为kCFRunLoopCommonModes 是Mode的集合  所需需要在每个mode中都对应添加这个source
                  //这样才能体现kCFRunLoopCommonModes的灵活性 不管设置那个mode 只要改mode在kCFRunLoopCommonModes这个
                  //集合中source就能执行
                  CFTypeRef context[2] = {rl, rls};
                  /* add new item to all common-modes */
                  CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
                  CFRelease(set);
              }
          } else {
              //通过modeName 获取 mode
              CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);
              if (NULL != rlm && NULL == rlm->_sources0) {
                  //如果mode存在但是_sources0不存在则初始化_sources0、_sources1、_portToV1SourceMap
                  rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
                  rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
                  rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);
              }
              if (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {
                  //如果_sources0和_sources1的集合中都不存在source
                  if (0 == rls->_context.version0.version) {
                      //如果version == 0 则添加到_sources0
                      CFSetAddValue(rlm->_sources0, rls);
                  } else if (1 == rls->_context.version0.version) {
                      //如果version == 1 则添加到_sources1
                      CFSetAddValue(rlm->_sources1, rls);
                      __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
                      if (CFPORT_NULL != src_port) {
                          //此处只有在加到source1的时候才会把souce和一个mach_port_t对应起来
                          //可以理解为,source1可以通过内核向其端口发送消息来主动唤醒runloop
                          CFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);
                          __CFPortSetInsert(src_port, rlm->_portSet);
                      }
                  }
                  __CFRunLoopSourceLock(rls);
                  //把runloop加入到source的_runLoops中
                  if (NULL == rls->_runLoops) {
                      rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops!
                  }
                  CFBagAddValue(rls->_runLoops, rl);
                  __CFRunLoopSourceUnlock(rls);
                  if (0 == rls->_context.version0.version) {
                      if (NULL != rls->_context.version0.schedule) {
                          doVer0Callout = true;
                      }
                  }
              }
              if (NULL != rlm) {
                  __CFRunLoopModeUnlock(rlm);
              }
          }
          __CFRunLoopUnlock(rl);
          if (doVer0Callout) {
              // although it looses some protection for the source, we have no choice but
              // to do this after unlocking the run loop and mode locks, to avoid deadlocks
              // where the source wants to take a lock which is already held in another
              // thread which is itself waiting for a run loop/mode lock
              rls->_context.version0.schedule(rls->_context.version0.info, rl, modeName);    /* CALLOUT */
          }
        }
        复制代码

        添加 source步骤

        1. 先判断 mode如果是 commonMode则将 source添加到 _commonModeItems中,并且将 source添加到每一个 common-modes
        2. 如果不是 commonMode则先通过 mode名称获取对应 mode
        3. 判断 modesource0是否存在,不存在则初始化_sources0、_sources1、_portToV1SourceMap
        4. 判断 source是否已经在 source0或者是 source1的集合中
        5. 如果存在则啥都不干,不存在则判断 rls->_context.version0.version是0,则添加到 source0集合中
        6. 是1 则添加到 source1的集合中然后吧souce和一个mach_port_t对应起来
      • void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef mode)
        void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {    /* DOES CALLOUT */
           CHECK_FOR_FORK();
           Boolean doVer0Callout = false, doRLSRelease = false;
           __CFRunLoopLock(rl);
           //同样的是需要判断mode
           if (modeName == kCFRunLoopCommonModes) {
               if (NULL != rl->_commonModeItems && CFSetContainsValue(rl->_commonModeItems, rls)) {
                   CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
                   //_commonModeItems中删除对应source
                   CFSetRemoveValue(rl->_commonModeItems, rls);
                   if (NULL != set) {
                       CFTypeRef context[2] = {rl, rls};
                       /* remove new item from all common-modes */
                       CFSetApplyFunction(set, (__CFRunLoopRemoveItemFromCommonModes), (void *)context);
                       CFRelease(set);
                   }
               } else {
               }
           } else {
               CFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, false);
               if (NULL != rlm && ((NULL != rlm->_sources0 && CFSetContainsValue(rlm->_sources0, rls)) || (NULL != rlm->_sources1 && CFSetContainsValue(rlm->_sources1, rls)))) {
                   CFRetain(rls);
                   if (1 == rls->_context.version0.version) {
                       __CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);
                       if (CFPORT_NULL != src_port) {
                           CFDictionaryRemoveValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port);
                           __CFPortSetRemove(src_port, rlm->_portSet);
                       }
                   }
                   CFSetRemoveValue(rlm->_sources0, rls);
                   CFSetRemoveValue(rlm->_sources1, rls);
                   __CFRunLoopSourceLock(rls);
                   if (NULL != rls->_runLoops) {
                       CFBagRemoveValue(rls->_runLoops, rl);
                   }
                   __CFRunLoopSourceUnlock(rls);
                   if (0 == rls->_context.version0.version) {
                       if (NULL != rls->_context.version0.cancel) {
                           doVer0Callout = true;
                       }
                   }
                   doRLSRelease = true;
               }
               if (NULL != rlm) {
                   __CFRunLoopModeUnlock(rlm);
               }
           }
           __CFRunLoopUnlock(rl);
           if (doVer0Callout) {
               // although it looses some protection for the source, we have no choice but
               // to do this after unlocking the run loop and mode locks, to avoid deadlocks
               // where the source wants to take a lock which is already held in another
               // thread which is itself waiting for a run loop/mode lock
               rls->_context.version0.cancel(rls->_context.version0.info, rl, modeName);    /* CALLOUT */
           }
           if (doRLSRelease) CFRelease(rls);
        }
        复制代码

        理解了添加的操作,删除的操作就比较简单了,这里就不赘述了

      • void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode)
        image.png添加Observer和添加 source区别就在于 Observer如果已经添加到其他 runloop中去了则不能再被添加,从 __CFRunLoopObserver__CFRunLoopSource也可以看出差异点
        image.png
        image.png
        定义的时候__CFRunLoopObserver就是只能获取一个runloop__CFRunLoopSource是一个 runloop的集合
      • void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef * mode)
        CFRunLoopRemoveSource
      • void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
        CFRunLoopAddObserver
      • void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode)
        CFRunLoopRemoveSource
    • CFRunLoopRun

      image.png

    • CFRunLoopRunInMode

      指定模式下运行 runloop
      image.png
      再看CFRunLoopRunSpecific方法

      SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
        CHECK_FOR_FORK();
        if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
        __CFRunLoopLock(rl);
        //根据modeName找到本次运行的mode
        CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
        //如果没找到 || mode中没有注册任何事件,则就此停止,不进入循环
        if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
            Boolean did = false;
            if (currentMode) __CFRunLoopModeUnlock(currentMode);
            __CFRunLoopUnlock(rl);
            return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
        }
        volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
        //取上一次运行的mode
        CFRunLoopModeRef previousMode = rl->_currentMode;
        //传入的mode置为当前mode
        rl->_currentMode = currentMode;
        int32_t result = kCFRunLoopRunFinished;
      
        // 1.通知observer即将进入runloop
        if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
        result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
        //通知observer已退出runloop
        if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
      
        __CFRunLoopModeUnlock(currentMode);
        __CFRunLoopPopPerRunData(rl, previousPerRun);
        rl->_currentMode = previousMode;
        __CFRunLoopUnlock(rl);
        return result;
      }
      复制代码

      大致流程如下:

      1. 判断当前 runloop中是否含有 mode如果没有则退出
      2. 判断 mode中是否含有 itme没有则退出
      3. 将传入的 mode置为当前运行 mode
      4. 通知 Observer 进入 runloop
      5. 启动 runloop 这里调用的是 __CFRunLoopRun方法
      6. 通知 Observer 已退出 runloop
    • __CFRunLoopRun

      由于该源码太多这里就是用伪代码来代替

      //核心函数
        /* rl, rlm are locked on entrance and exit */
        static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode){
      
            //通过GCD开启一个定时器,然后开始跑圈
            dispatch_source_t timeout_timer = NULL;
            ...
            dispatch_resume(timeout_timer);
      
            int32_t retVal = 0;
      
            //处理事务,即处理items
            do {
      
                // 通知 Observers: 即将处理timer事件
                __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
      
                // 通知 Observers: 即将处理Source事件
                __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources)
      
                // 处理Blocks
                __CFRunLoopDoBlocks(rl, rlm);
      
                // 处理sources0
                Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
      
                // 处理sources0返回为YES
                if (sourceHandledThisLoop) {
                    // 处理Blocks
                    __CFRunLoopDoBlocks(rl, rlm);
                }
      
                // 判断有无端口消息(Source1)
                if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
                    // 处理消息
                    goto handle_msg;
                }
      
      
                // 通知 Observers: 即将进入休眠
                __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
                __CFRunLoopSetSleeping(rl);
      
                // 等待被唤醒
                __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
      
                // user callouts now OK again
                __CFRunLoopUnsetSleeping(rl);
      
                // 通知 Observers: 被唤醒,结束休眠
                __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
      
      
            handle_msg:
                if (被timer唤醒) {
                    // 处理Timers
                    __CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
                }else if (被GCD唤醒){
                    // 处理gcd
                    __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
                }else if (被source1唤醒){
                    // 被Source1唤醒,处理Source1
                    __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)
                }
      
                // 处理block
                __CFRunLoopDoBlocks(rl, rlm);
      
                if (sourceHandledThisLoop && stopAfterHandle) {
                    retVal = kCFRunLoopRunHandledSource;//处理源
                } else if (timeout_context->termTSR < mach_absolute_time()) {
                    retVal = kCFRunLoopRunTimedOut;//超时
                } else if (__CFRunLoopIsStopped(rl)) {
                    __CFRunLoopUnsetStopped(rl);
                    retVal = kCFRunLoopRunStopped;//停止
                } else if (rlm->_stopped) {
                    rlm->_stopped = false;
                    retVal = kCFRunLoopRunStopped;//停止
                } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
                    retVal = kCFRunLoopRunFinished;//结束
                }
      
      
      
            }while (0 == retVal);
      
            return retVal;
        }
      复制代码

      发现和上文中的 runloop流程图是一致的
      这里挑一个处理timers源码作讲解其他大同小异 __CFRunLoopDoTimers源码
      image.png
      image.png
      发现底层就是通过调用 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__方法来执行 timer的回调
      image.png
      这里也可以通过打印堆栈来验证
      image.png
      类似这种函数一共有6种:

      1. block应用:__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
      2. 调用timer:__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
      3. 响应source0: __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
      4. 响应source1: __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
      5. GCD主队列:__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
      6. observer源: __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
    • 总结

      通过源码探索也可以发现 runloop其实很简单,它是一个对象,并且和线程一一对应,主线程的 runloop是自动开启的,子线程的 runloop是需要手动开启的,每一个 runloop中会有多个 mode, 每一个 mode中又有多个 source,但是 runloop每次 run只能选择一种 mode(这也就造成了 timer和页面滑动之后的问题下文有详细讲解)。

      RunLoop运行的核心是一个do..while..循环,遍历所有需要处理的事件,如果有事件处理就让线程工作,没有事件处理则让线程休眠,同时等待事件到来。

  • RunLoop的应用及相关问题

    • NSTimer的应用

      NSTimer其实就是上文提到的 timer,底层 runloop会在每个时间点都注册一个事件,到了时间点回调时间,但是 runloop为了节省资源不会每次都很准确的回调事件,timer有个属性是宽容度 Tolerance标识了可以有的多大误差,这样的机制就导致了 timer不是特别准确。而且如果某个时间点错过了就不会再重复执行
      面试题:以+scheduledTimerWithTimeInterval:的方式触发的timer,在滑动页面上的列表时,timer会暂停回调, 为什么?如何解决?
      应为scheduledTimerWithTimeInterval添加方式默认的 modedefaultMode,而页面滑动时的 modeUITrackingRunLoopMode,滑动的时候 runloop会切换 mode,上文也提到过 runloop一次只能执行一个 mode中的事件,所以对应的 timer就会停止。
      **解决方法是:**将 timer添加到 NSRunLoopCommonModes中就好

    • GCD Timer的应用

      GCD的线程管理是通过系统来直接管理的。GCD Timer 是通过 dispatch portRunLoop 发送消息,来使 RunLoop 执行相应的 block,如果所在线程没有 RunLoop,那么 GCD 会临时创建一个线程去执行 block,执行完之后再销毁掉,因此 GCDTimer 是可以不依赖 RunLoop 的。
      至于这两个 Timer 的准确性问题,如果不在 RunLoop 的线程里面执行,那么只能使用 GCD Timer,由于 GCD Timer 是基于 MKTimer(mach kernel timer),已经很底层了,因此是很准确的。
      如果GCD TimerRunLoop的线程中执行那么可能出现的问题和 timer大同小异
      面试题:GCD Timer和 NSTimer那个更精确,为什么
      这个问题不能单单直接肯定的说是 GCD Timer上文也分析到了如果在有 RunLoop的线程中精确度和 NSTimer差不多,具体原因可以看上文的 NSTimer分析
      如果线程中没有 runloop首选 GCD timer,它相对于 NSTimer更加低层并且没有 runloop的影响所以更加精确

    • AutoreleasePool和RunLoop的关系
      1. App 启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。
      2. 第一个 Observer 监视的事件是 Entry(即将进入 Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是 -2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
      3. 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用 _objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出 Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observerorder 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

      总结一下自动释放池的创建、销毁时机:

      1. kCFRunLoopEntry 进入runloop之前,创建一个自动释放池
      2. kCFRunLoopBeforeWaiting 休眠之前,销毁自动释放池,创建一个新的自动释放池
      3. kCFRunLoopExit 退出runloop之前,销毁自动释放池
    • 事件响应

      苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。这个过程的详细情况可以参考这里。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

    • 手势识别

      当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。

    • GCD和RunLoop的关系

      在RunLoop的源代码中可以看到用到了GCD的相关内容,但是RunLoop本身和GCD并没有直接的关系。当调用了dispatch_async(dispatch_get_main_queue(), <#^(void)block#>)时libDispatch会向主线程RunLoop发送消息唤醒RunLoop,RunLoop从消息中获取block,并且在__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__回调里执行这个block。不过这个操作仅限于主线程,其他线程dispatch操作是全部由libDispatch驱动的。

    • 图片下载中RunLoop的应用

      由于图片渲染到屏幕需要消耗较多资源,为了提高用户体验,当用户滚动Tableview的时候,只在后台下载图片,但是不显示图片,当用户停下来的时候才显示图片。
      [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"imgName"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
      此方法即时此时正在滑动屏幕也不会出现卡顿的情况,应为滑动的过程中,runloopmodeNSEventTrackingRunLoopMode所以滑动的时候会切换 mode那么 NSDefaultRunLoopMode的事件也就停止了, 滑动完成之后才会切到 NSDefaultRunLoopMode然后继续事件,这样就不会造成卡顿现象

    • 线程常驻

      这个就很简单了其实就是开个线程将 runloop跑起来,但是单纯的跑 runloop也不行,如果事件执行完了 runloop就会消亡对应的线程也就会销毁。所以可以添加一个 timer或者是 NSPort 具体方法如下:

      1. [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
      2. [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
      3. [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];

      三种方法都能保证 runloop运行,线程常驻

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享