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、source一、timer、observer和若干個端口因此事務的執行是由 mode控制的,而上文張分析 runloop的結構的時候又知道每一個 runloop當中都含有多個 mode,可是當前的 mode只有一個,因此從這裏能夠總結出兩點:markdown

      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運行模式集合中app

    • RunLoop Source

      Run Loop Source分爲Source、Observer、Timer三種,也就是 ModeItemasync

      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.pngide

    • 添加 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、_sources一、_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、_sources一、_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;
      }
      複製代碼

      大體流程以下:oop

      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運行,線程常駐

相關文章
相關標籤/搜索