iOS-RunLoop究竟是怎么運作的

RunLoop簡介(Introduction)

  1. RunLoop是線程基礎架構的一部分。RunLoop存在的目的是讓線程在沒有任務處理的時候進入休眠,在有任務處理的時候運行。

  2. RunLoop不是完全自管理的,需要你在適當的時候啟動。

  3. Cocoa和Core Foundation框架都提供了RunLoop相關的API。

  4. 你不需要自己創建RunLoop對象。每個線程,包括主線程都有一個對應的RunLoop對象。

  5. 只有子線程的RunLoop需要手動啟動,主線程的RunLoop在App啟動調用Main函數時就已運行。

image

RunLoop的結構

image
  1. NSRunloop:最上層的NSRunloop層實際上是對C語言實現的CFRunloop的一個封裝,實際上它沒干什么事,比如CFRunloop有一個過期時間是double類型,NSRunloop把它變成了NSDate類型;
  2. CFRunloop:這是真正干事的一層,源代碼是開源的,并且是跨平臺的;
  3. 系統層:底層實現用到了GCD,mach kernel是蘋果的內核,比如runloop的睡眠和喚醒就是用mach kernel來實現的
    下面是跟Runloop有關的,我們平時用到的一些模塊,功能等等:
    1)NSTimer計時器;
    2)UIEvent事件;
    3)Autorelease機制;
    4)NSObject(NSDelayedPerforming):比如這些方法:performSelector:withObject:afterDelay:,performSelector:withObject:afterDelay:inModes:,cancelPreviousPerformRequestsWithTarget:selector:object:等方法都是和Runloop有關的;
    5)NSObject(NSThreadPerformAddition):比如這些方法:performSelectorInBackground:withObject:,performSelectorOnMainThread:withObject:waitUntilDone:,performSelector:onThread:withObject:waitUntilDone:等方法都是和Runloop有關的;
  4. Core Animation層的一些東西:CADisplayLink,CATransition,CAAnimation等;
  5. dispatch_get_main_queue();
  6. NSURLConnection;

從調用堆棧來看Runloop

image

從下往上一層層的看,最開始的start是dyld干的,然后是main函數,main函數接著調用UIApplicationMain,然后的GSEventRunModal是Graphics Services是處理硬件輸入,比如點擊,所有的UI事件都是它發出來的。緊接著的就是Runloop了,從圖中的可以看到從13到10的4調用都是Runloop相關的。再上面的就是事件隊列處理,以及UI層的事件分發了。

  • dyld(the dynamic link editor)是蘋果的動態鏈接器,是蘋果操作系統一個重要組成部分,在系統內核做好程序準備工作之后,交由dyld負責余下的工作。而且它是開源的,任何人可以通過蘋果官網下載它的源碼來閱讀理解它的運作方式,了解系統加載動態庫的細節。

幾乎所有線程的所有函數都是從下面六個函數之一調起:

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的構成

  • Core Foundation中的CFRunLoopRef
  • NSRunLoop是基于CFRunLoopRef的一層OC包裝,所以要了解RunLoop內部結構,需要多研究CFRunLoopRef層面的API(Core Foundation層面)
RunLoop結構
  1. Runloop與Thread是一一綁定的,但是并不是一個Thread只能起一個Runloop,它可以起很多,但是必須是嵌套結構,根Runloop只有一個;
  2. RunloopMode是指的一個事件循環必須在某種模式下跑,系統會預定義幾個模式。一個Runloop有多個Mode
  3. CFRunloopSource,CFRunloopTimer,CFRunloopObserver這些元素是在Mode里面的,Mode與這些元素的對應關系也是1對多的
    CFRunloopTimer:比如下面的方法都是CFRunloopTimer的封裝:

CFRunLoop 結構

typedef struct __CFRunLoop * CFRunLoopRef;

struct __CFRunLoop {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;          /* locked for accessing mode list */
    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes; // 字符串,記錄所有標記為common的mode
    CFMutableSetRef _commonModeItems; // 所有commonMode的item(source、timer、observer)
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes; // CFRunLoopModeRef set
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
}
  • CFRunLoop 里面包含了線程,若干個 mode。
  • CFRunLoop 和線程是一一對應的。
  • _blocks_head 是 perform block 加入到里面的

RunLoop 的 Mode

一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer。每次調用 RunLoop 的主函數時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響。

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
    ...
};

CFRunLoopMode

// 定義 CFRunLoopModeRef 為指向 __CFRunLoopMode 結構體的指針
typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0; // source0 set ,非基于Port的,接收點擊事件,觸摸事件等APP 內部事件
    CFMutableSetRef _sources1; // source1 set,基于Port的,通過內核和其他線程通信,接收,分發系統事件
    CFMutableArrayRef _observers; // observer 數組
    CFMutableArrayRef _timers; // timer 數組
    CFMutableDictionaryRef _portToV1SourceMap;// source1 對應的端口號
    __CFPortSet _portSet;
    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 */
};

CommonModes

這里有個概念叫 “CommonModes”:一個 Mode 可以將自己標記為”Common”屬性(通過將其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每當 RunLoop 的內容發生變化時,RunLoop 都會自動將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 標記的所有Mode里。

Mode列表

  • NSDefaultRunLoopMode:App的默認Mode,通常主線程是在這個Mode下運行
  • UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
  • UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用
  • GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到
  • NSRunLoopCommonModes: 這是一個占位用的Mode,不是一種真正的Mode commonModes:
  • 一個Mode 可以將自己標記成”Common”屬性(通過將其ModelName 添加到RunLoop的"commonModes" 中)。每當 RunLoop 的內容發生變化時,RunLoop 都會自動將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 標記的所有Mode里。
  • 應用場景舉例:主線程的 RunLoop 里有兩個預置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個 Mode 都已經被標記為"Common"屬性。DefaultMode 是 App 平時所處的狀態,TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態。當你創建一個 Timer 并加到 DefaultMode 時,Timer 會得到重復回調,但此時滑動一個TableView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回調,并且也不會影響到滑動操作。
  • 有時你需要一個 Timer,在兩個 Mode 中都能得到回調,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到commonMode 中。那么所有被標記為commonMode的mode(defaultMode和TrackingMode)都會執行該timer。這樣你在滑動界面的時候也能夠調用time

可以用表格來說明不同的mode:

mode name description
Default NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) 用的最多的模式,大多數情況下應該使用該模式開始 RunLoop并配置 input source
Connection NSConnectionReplyMode (Cocoa) Cocoa用這個模式結合 NSConnection 對象監測回應,我們應該很少使用這種模式
Modal NSModalPanelRunLoopMode (Cocoa) Cocoa用此模式來標識用于模態面板的事件
Event tracking NSEventTrackingRunLoopMode (Cocoa) Cocoa使用此模式在鼠標拖動loop和其它用戶界面跟蹤 loop期間限制傳入事件
Common modes NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) 這是一組可配置的常用模式。將輸入源與些模式相關聯會與組中的每個模式相關聯。Cocoa applications 里面此集包括Default、Modal和Event tracking。Core Foundation只包括默認模式,你可以自己把自定義mode用CFRunLoopAddCommonMode函數加入到集合中.

CFRunLoopSourceRef

CFRunLoopSourceRef 是事件產生的地方。Source有兩個版本:Source0 和 Source1。
? Source0 只包含了一個回調(函數指針),它并不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
? Source1 包含了一個 mach_port 和一個回調(函數指針),被用于通過內核和其他線程相互發送消息。這種 Source 能主動喚醒 RunLoop 的線程,其原理在下面會講到。

  1. Source0:處理App內部事件,App自己負責管理(觸發),如UIEvent,CFSocket;
  2. Source1:由Runloop和內核管理,mach port驅動,如CFMachPort(輕量級的進程間通信的方式,NSPort就是對它的封裝,還有Runloop的睡眠和喚醒就是通過它來做的),CFMessagePort;

CFRunLoopTimerRef

CFRunLoopTimerRef 是基于時間的觸發器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一個時間長度和一個回調(函數指針)。當其加入到 RunLoop 時,RunLoop會注冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調。

RunLoopObserver

CFRunLoopObserver 是觀察者,可以觀察RunLoop的各種狀態,并拋出回調。可以監聽得狀態如下:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
 kCFRunLoopEntry = (1UL << 0), //即將進入run loop
 kCFRunLoopBeforeTimers = (1UL << 1), //即將處理timer
 kCFRunLoopBeforeSources = (1UL << 2), //即將處理source
 kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠
 kCFRunLoopAfterWaiting = (1UL << 6), //被喚醒但是還沒開始處理事件
 kCFRunLoopExit = (1UL << 7), //run loop已經退出
 kCFRunLoopAllActivities = 0x0FFFFFFFU
};
  • Source0:非基于Port的。只包含了一個回調(函數指針),它并不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。
  • Source1:基于Port的,通過內核和其他線程通信,接收、分發系統事件。這種 Source 能主動喚醒 RunLoop 的線程。后面講到的創建常駐線程就是在線程中添加一個NSport來實現的。

上面的 Source/Timer/Observer 被統稱為 mode item,一個 item 可以被同時加入多個 mode。但一個 item 被重復加入同一個 mode 時是不會有效果的。如果一個 mode 中一個 item 都沒有,則 RunLoop 會直接退出,不進入循環。

RunLoop的內部邏輯

應用場景舉例:主線程的 RunLoop 里有兩個預置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個 Mode 都已經被標記為”Common”屬性。DefaultMode 是 App 平時所處的狀態,TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態。當你創建一個 Timer 并加到 DefaultMode 時,Timer 會得到重復回調,但此時滑動一個TableView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回調,并且也不會影響到滑動操作。

有時你需要一個 Timer,在兩個 Mode 中都能得到回調,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到頂層的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自動更新到所有具有”Common”屬性的 Mode 里去。

Runloop一次loop執行過程

執行過程大致描述如下:

  • 通知 observers 即將進入 run loop
  • 通知 observes 即將開始處理 timer source
  • 通知 observes 即將開始處理 source0 事件
  • 執行 source0 事件
  • 如果處于主線程有 dispatchPort 消息,跳轉到第9步
  • 通知 observes 線程即將進入休眠
  • 內循環阻塞等待接收系統消息,包括:
  • 收到內核發送過來的消息 (source1消息)
  • 定時器事件需要執行
  • run loop 的超時時間到了
  • 手動喚醒 run loop
  • 通知 observes 線程被喚醒
  • 處理通過端口收到的消息:
  • 如果自定義的 timer 被 fire,那么執行該 timer 事件并重新開始循環,完成后跳轉到第2步
  • 如果 input source 被 fire,則處理該事件
  • 如果 run loop 被手動喚醒,并且沒有超時,完成后跳轉到第2步
  • 通知 observes run loop 已經退出
image

注意:

CFRunLoopDoBlocks 是執行 perform block 中的 block
綠色的是RunLoopRun()
第一次循環 CFRunLoopServiceMachPort 是不走的
handle_msg 處理 timer 事件,處理 main queue block 事件,處理 source1 事件
中間的紅色CFRunLoopServiceMachPort是監聽 GCD 的端口事件,只監聽一個端口,左下角的CFRunLoopServiceMachPort是堅挺 source1,timer 的,是一個 MutableSet

image

NSTimer 與 GCD Timer

  • NSTimer 是通過 RunLoop 的 RunLoopTimer 把時間加入到 RunLoopMode 里面。官方文檔里面也有說 CFRunLoopTimer 和 NSTimer 是可以互相轉換的。由于 NSTimer 的這種機制,因此 NSTimer 的執行必須依賴于 RunLoop,如果沒有 RunLoop,NSTimer 是不會執行的。

  • GCD 則不同,GCD 的線程管理是通過系統來直接管理的。GCD Timer 是通過 dispatch port 給 RunLoop 發送消息,來使 RunLoop 執行相應的 block,如果所在線程沒有 RunLoop,那么 GCD 會臨時創建一個線程去執行 block,執行完之后再銷毀掉,因此 GCD 的 Timer 是不依賴 RunLoop 的。

  • 至于這兩個 Timer 的準確性問題,如果不再 RunLoop 的線程里面執行,那么只能使用 GCD Timer,由于 GCD Timer 是基于 MKTimer(mach kernel timer),已經很底層了,因此是很準確的。

  • 異步的回調如果存在延時操作,那么就要放到有 RunLoop 的線程里面,否則回調沒有著陸點無法執行

  • NSTimer 必須得在有 RunLoop 的線程里面才能執行,另外,使用 NSTimer 的時候會出現滑動 TableView,Timer 停止的問題,是由于 RunLoopMode 切換的問題,只要把 NSTimer 加到 common mode 就好了。

  • 滾動過程中延遲加載,可以利用滾動時 RunLoopMode 切換到 NSEventTrackingRunLoopMode 模式下這個機制,在 Default mode 下添加加載圖片的方法,在滾動時就不會觸發。

  • 崩潰后處理 DSSignalHandlerDemo

RunLoop 與線程的關系

一般來講,一個線程一次只能執行一個任務,執行完成后線程就會退出。如果我們需要一個機制,讓線程能隨時處理事件但并不退出,通常的代碼邏輯是這樣的:

  • 每條線程都有唯一的一個與之對應的RunLoop對象
  • 主線程的RunLoop已經自動創建好了,子線程的RunLoop需要主動創建,只要調用currentRunLoop方法, 系統就會自動創建一個RunLoop, 添加到當前線程中
  • 線程剛創建時并沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的創建是發生在第一次獲取時,RunLoop 的銷毀是發生在線程結束時。你只能在一個線程的內部獲取其 RunLoop(主線程除外)
function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

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

這種模型通常被稱作 Event LoopEvent Loop 在很多系統和框架里都有實現,比如 Node.js 的事件處理,比如 Windows 程序的消息循環,再比如 OSX/iOS 里的 RunLoop。**實現這種模型的關鍵點在于:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以避免資源占用、在有消息到來時立刻被喚醒。
**
所以,RunLoop 實際上就是一個對象,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數來執行上面 Event Loop 的邏輯。線程執行了這個函數后,就會一直處于這個函數內部 “接受消息->等待->處理” 的循環中,直到這個循環結束(比如傳入 quit 的消息),函數返回。

首先,iOS 開發中能遇到兩個線程對象: pthread_t 和 NSThread。過去蘋果有份文檔標明了 NSThread 只是 pthread_t 的封裝,但那份文檔已經失效了,現在它們也有可能都是直接包裝自最底層的 mach thread。蘋果并沒有提供這兩個對象相互轉換的接口,但不管怎么樣,可以肯定的是 pthread_t 和 NSThread 是一一對應的。比如,你可以通過 pthread_main_thread_np() 或 [NSThread mainThread] 來獲取主線程;也可以通過 pthread_self() 或 [NSThread currentThread] 來獲取當前線程。CFRunLoop 是基于 pthread 來管理的。

蘋果不允許直接創建 RunLoop,它只提供了兩個自動獲取的函數:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()。 這兩個函數內部的邏輯大概是下面這樣:

AutoreleasePool & Runloop

自動釋放池的創建和釋放,銷毀的時機如下所示

  • kCFRunLoopEntry; // 進入runloop之前,創建一個自動釋放池
  • kCFRunLoopBeforeWaiting; // 休眠之前,銷毀自動釋放池,創建一個新的自動釋放池
  • kCFRunLoopExit; // 退出runloop之前,銷毀自動釋放池

事件響應

  • 蘋果注冊了一個 Source1 (基于 mach port 的) 用來接收系統事件,當一個硬件事件(觸摸/鎖屏/搖晃等)發生后,首先由 IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉發給需要的App進程。

  • 隨后蘋果注冊的那個 Source1 就會觸發回調,并調用 _UIApplicationHandleEventQueue() 進行應用內部的分發。

  • _UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理并包裝成 UIEvent 進行處理或分發,其中包括識別 UIGesture/處理屏幕旋轉/發送給 UIWindow 等。通常事件比如 UIButton 點擊、touchesBegin/Move/End/Cancel 事件都是在這個回調中完成的。

手勢識別 & Runloop

當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調用 Cancel 將當前的 touchesBegin/Move/End 系列回調打斷。隨后系統將對應的 UIGestureRecognizer 標記為待處理。蘋果注冊了一個 Observer 監測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調函數是 _UIGestureRecognizerUpdateObserver(),其內部會獲取所有剛被標記為待處理的 GestureRecognizer,并執行GestureRecognizer的回調。當有 UIGestureRecognizer 的變化(創建/銷毀/狀態改變)時,這個回調都會進行相應處理。

界面更新 & Runloop

當在操作 UI 時,比如改變了 Frame、更新了 UIView/CALayer 的層次時,或者手動調用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個 UIView/CALayer 就被標記為待處理,并被提交到一個全局的容器去。蘋果注冊了一個 Observer 監聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件,回調去執行一個很長的函數:_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個函數里會遍歷所有待處理的 UIView/CAlayer 以執行實際的繪制和調整,并更新 UI 界面。

定時器 & Runloop

NSTimer 其實就是 CFRunLoopTimerRef,他們之間是 toll-free bridged 的。一個 NSTimer 注冊到 RunLoop 后,RunLoop 會為其重復的時間點注冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop為了節省資源,并不會在非常準確的時間點回調這個Timer。Timer 有個屬性叫做 Tolerance (寬容度),標示了當時間點到后,容許有多少最大誤差。

如果某個時間點被錯過了,例如執行了一個很長的任務,則那個時間點的回調也會跳過去,不會延后執行。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。

CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現原理更復雜,和 NSTimer 并不一樣,其內部實際是操作了一個 Source)。如果在兩次屏幕刷新之間執行了一個長任務,那其中就會有一幀被跳過去(和 NSTimer 相似),造成界面卡頓的感覺。在快速滑動TableView時,即使一幀的卡頓也會讓用戶有所察覺。Facebook 開源的 AsyncDisplayLink 就是為了解決界面卡頓的問題,其內部也用到了 RunLoop

RunLoop剖析

Run Loop Modes

1. RunLoop mode中包含了sources、timers和observers。

  1. 每次啟動RunLoop都必須指定一個Mode。

  2. 在RunLoop運行過程中,只有當前Mode中的事件會被處理,其他Mode中的事件會被暫停,直到該RunLoop在該Mode下運行。

  3. 你可以通過name來標識Mode,name是一個字符串。

  4. 你可以用你喜歡的名字來自定義一個Mode,但是為了確保這個Mode生效,你要確保至少添加一個souce或者timer或者observer到該Mode中。

  5. 下表列出了Cocoa框架和Core Foundation框架中定義的標準的Mode,還有簡單的描述。你可以通過Name那一列的字符串找到對應的Mode(這里我做一些改動,原文檔表格中寫的都是OS X中RunLoop的Mode,我改為iOS中的Mode)。

Mode Name Description
Default NSDefaultRunLoopMode(Cocoa) kCFRunLoopDefaultMode (Core Foundation) 默認Mode,APP運行起來之后,主線程的RunLoop默認在該Mode下運行
GSEventReceiveRunLoopMode(Cocoa) 接收系統內部事件
App Init UIInitializationRunLoopMode(Cocoa) APP初始化的時候運行在該Mode下
Event tracking UITrackingRunLoopMode(Cocoa) 追蹤觸摸手勢,確保界面刷新不會卡頓,滑動tableview,scrollview等都運行在這個Mode下
Common modes NSRunLoopCommonModes(Cocoa) kCFRunLoopCommonModes (Core Foundation) commonModes很特殊,稍后在下一章閱讀源碼的時候細說

事件源(Input Sources)

1.輸入源分2種,一種是內核通過端口發送消息產生的(Port-Based Sources),另一種是開發者自定義的(Custom Input Sources)。這兩種時間只在標記方面有所區別,內核消息由內核自動標記,開發者自定義的事件由開發者手動標記。

基于內核端口的事件源(Port-Based Sources)

1.Cocoa和Core Foundation都提供了Port-Based source的支持,比如Cocoa中的NSPort,Core Foundation中的CFMachPortRef等。

自定義事件源(Custom Input Sources)

1. 使用Core Foundation中的CFRunLoopSourceRef來創建一個自定義的source。

Perform Selector接口(Cocoa Perform Selector Sources)

1.使用performSelector系列API往某個線程添加事件的時候,你必須要確保目標線程的RunLoop是運行的。否則該事件不會被執行,這里要格外注意一下,子線程的RunLoop不是默認啟動的。

定時器(Timer Sources)

1.這里的定時器事件并一定時間到了就會執行。就像input source一樣,timer需要被加入到指定的Mode中,并且RunLoop要運行在這個Mode下,timer才有效。

2.Runloop中的定時器也不是精準定時器。RunLoop是一個循環一直跑,在某次循環運行中途加入的定時器事件,只有等到下一次循環才會被執行。

觀察者(Run Loop Observers)

1.RunLoop在狀態改變的時候回發出通知,你可以監聽這些通知來做一些有用的事情。比如在線程運行前、要休眠之前等時候做一些準備工作。

2.可以監聽的RunLoop狀態有這些:

  • 即將進入RunLoop
  • RunLoop即將處理timer source
  • RunLoop即將處理input source
  • RunLoop即將進入休眠
  • RunLoop被喚醒,但還沒開始處理事件
  • RunLoop退出

3.你可以通過Core Foundation框架中的CFRunLoopObserverRef相關API來操作observer

The Run Loop Sequence of Events

1.RunLoop每次循環的執行步驟大概如下:

  1. 通知observers 已經進入RunLoop
  2. 通知observes 即將開始處理timer source
  3. 通知observes 即將開始處理input sources(不包括port-based source)
  4. 開始處理input source(不包括port-based source)
  5. 如果有port-based source待處理,則開始處理port-based source,跳轉到第9步
  6. 通知observes線程即將進入休眠
  7. 讓線程進入休眠狀態,直到有以下事件發生:
    • 收到內核發送過來的消息
    • 定時器事件需要執行
    • RunLoop的超時時間到了
    • 手動喚醒RunLoop
  8. 通知observes 線程被喚醒
  9. 處理待處理的事件:
    • 如果自定義的timer被fire,那么執行該timer事件并重新開始循環,跳轉到第2步
    • 如果input source被fire,則處理該事件
    • 如果RunLoop被手動喚醒,并且沒有超時,那么重新開始循環,跳轉到第2步
  10. 通知observes RunLoop已經退出

2.RunLoop可以被手動喚醒。你可以在增加一個input source之后喚醒RunLoop以確保input source可以被立即執行,而不用等到RunLoop被其他事件喚醒。

RunLoop 作用

總結下來,RunLoop 的作用主要體現在三方面:

  • 保持程序的持續運行
  • 處理App中的各種事件(比如觸摸事件、定時器事件、Selector事件)
  • 節省CPU資源,提高程序性能:該做事的時候做事,該休息的時候休息
  • 就是說,如果沒有 RunLoop 程序一運行就結束了,你根本不可能看到持續運行的 app。

iOS中有2套API訪問和使用RunLoop

Foundation:NSRunLoop
Core Foundation: CFRunLoopRef
NSRunLoop是基于CFRunLoopRef的一層OC包裝,因此我們需要研究CFRunLoopRef層面的API(Core Foundation層面)

什么時候使用RunLoop?(When Would You Use a Run Loop?)

1.只有在創建一個子線程的時候,開發者才必要顯式的手動的把RunLoop run起來。主線程的RunLoop是自動啟動的,不需要手動run。

2.你需要考量,是否有必要開啟子線程的RunLoop,可以參考以下幾種情況

  • 需要和其他線程通信時
  • 需要在子線程中使用timer
  • 使用了performSelector…系列API(比如線程初始化了之后,使用了performSelector: onThread: withObject:..讓子線程執行某個任務,子線程RunLoop并沒有被開啟,所以檢測不到該input source,這個任務就一直不會被執行)
  • 需要線程持續執行某個周期性的任務

操作RunLoop對象(Using Run Loop Objects)

1.每個線程都對應一個RunLoop。一個RunLoop對象提供了添加、運行各種source的接口。

2.在Cocoa框架中,RunLoop對象是NSRunLoop類的實例,在Core Foundation框架中是指向CFRunLoopRef的指針。NSRunLoop是基于CFRunLoopRef的封裝。

3.可以使用以下方法來獲取當前線程的RunLoop:

  • 使用NSRunLoop中的currentRunLoop函數
  • 使用CFRunLoopGetCurrent函數

4.NSRunLoop和CFRunLoopRef不是toll-free的,但是NSRunLoop提供了一個getCFRunLoop函數來獲取CFRunLoopRef。

5.在你打算啟動一個子線程的RunLoop之前,你一定要增加至少一個input source或者timer到RunLoop中。如果RunLoop中沒有任何source,它會馬上退出。

6.此外,添加一個observes也可以讓RunLoop不至于馬上退出。你可以使用CFRunLoopObserverRef來創建observe,并使用CFRunLoopAddObserver函數來添加到RunLoop中

7.是否是線程安全的取決于你用什么API來操作RunLoop。Core Foundation框架中CFRunLoop相關的API都是線程安全的,并且可以在任何線程中調用。Cocoa框架中的NSRunLoop相關的API不是線程安全的。你最好在某個線程中只操作該線程的RunLoop。如果你在線程1中用NSRunLoop的API向線程2的RunLoop添加source,可能會引起crash。

配置各種事件源(Configuring Run Loop Sources)

這里主要是一些如何配置input source、Port-based input source的示例代碼。具體想看可以直接看文檔,代碼里面都帶有注釋。

  • 使程序一直運行并接受用戶輸入:我們的app必然不能像命令式執行一樣,執行完就退出了,我們需要app在我們不殺死它的時候一直運行著,并在由用戶事件的時候能夠響應,比如網絡輸入,用戶點擊等等,這是Runloop的首要任務;
  • 決定程序在何時應該處理哪些事件:實際上程序會有很多事件,Runloop會有一定的機制來管理時間的處理時機等
  • 調用解耦(Message Queue):比方說手指點擊滑動會產生UIEvent事件,對于主調方來說,我不可能等到這個事件被執行了才去產生下一個事件,也就是主調方不能被被調方卡住。那么在實際實現中,被調方會有一個消息隊列,主調方會把消息扔到消息隊列中,然后不管了,消息的處理是由被調方不斷去從消息隊列中取消息,然后執行的。這樣的好處是主調方不需要知道消息是具體是怎么執行的,只要產生消息即可,從而實現了解耦;
image

如果沒有RunLoop

image

有了RunLoop

image

RunLoop的應用

RunLoop在實際開發過程中的應用(二) - 簡書

  1. UIImageView延遲加載照片
  2. 線程保活
  3. 子線程中執行NSTimer
  4. performSelector
  5. 自動釋放池

讓UITableView、UICollectionView等延遲加載圖片。

下面就拿UITableView來舉例說明:

UITableView 的 cell 上顯示網絡圖片,一般需要兩步,第一步下載網絡圖片;第二步,將網絡圖片設置到UIImageView上。

  • 第一步,我們一般都是放在子線程中來做,這個不做贅述。
  • 第二步,一般是回到主線程去設置。有了前兩篇文章關于Mode的切換,想必你已經知道怎么做了。
    就是在為圖片視圖設置圖片時,在主線程設置,并調用performSelector:withObject:afterDelay:inModes:方法。最后一個參數,僅設置一個NSDefaultRunLoopMode。
UIImage *downloadedImage = ....;
[self.myImageView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

當然,即使是讀取沙盒或者bundle內的圖片,我們也可以運用這一點來改善視圖的滑動。但是如果UITableView上的圖片都是默認圖,似乎也不是很好,你需要自己來權衡了。

線程保活

可能你的項目中需要一個線程,一直在后臺做些耗時操作,但是不影響主線程,我們不要一直大量的創建和銷毀線程,因為這樣太浪費性能了,我們只要保留這個線程,只要對他進行“保活”就行

//繼承了一個NSTread 線程,然后使用vc中創建和執行某個任務,查看線程的情況
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    WXThread *thread = [[WXThread alloc] initWithTarget:self
                                               selector:@selector(doSomeThing)
                                                 object:nil];
    [thread start];
}
- (void)doSomeThing{
    NSLog(@"doSomeThing");
}

//每一次點擊屏幕的時候,線程執行完方法,直接釋放掉了,下一次創建了一個新的線程;
//子線程存活的時間很短,只要執行完畢任務,就會被釋放
2017-04-19 16:03:10.686 WXAllTest[14928:325108] doSomeThing
2017-04-19 16:03:10.688 WXAllTest[14928:325108] WXTread - dealloc - <WXThread: 0x600000276780>{number = 3, name = (null)}
2017-04-19 16:03:18.247 WXAllTest[14928:325194] doSomeThing
2017-04-19 16:03:18.249 WXAllTest[14928:325194] WXTread - dealloc - <WXThread: 0x608000271340>{number = 4, name = (null)}
2017-04-19 16:03:23.780 WXAllTest[14928:325236] doSomeThing
2017-04-19 16:03:23.781 WXAllTest[14928:325236] WXTread - dealloc - <WXThread: 0x608000270e00>{number = 5, name = (null)}

如果我每隔一段時間就像在線程中執行某個操作,好像現在不行
如果我們將線程對象強引用,也是不行的,會崩潰

1.成為基本屬性
/** 線程對象 */
@property(strong,nonatomic)  WXThread *thread;

2.創建線程之后,直接將入到RunLoop中
- (void)viewDidLoad {
    [super viewDidLoad];
    _thread = [[WXThread alloc] initWithTarget:self
                                      selector:@selector(doSomeThing)
                                        object:nil];
    [_thread start];
}

3.執行doSomeThing函數
- (void)doSomeThing{
    //一定要加入一個timer,port,或者是obervers,否則RunLoop啟動不起來
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}

4.在點擊屏幕的時候,執行一個方法,線程之間的數據通信

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self performSelector:@selector(test) onThread:_thread withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
}

5.將test方法寫清楚
- (void)test{
    NSLog(@"current thread - %@",[NSThread currentThread]);
}

//打印結果:同一個線程,線程保活成功
2017-04-19 18:21:07.660 WXAllTest[16145:382366] current thread - <WXThread: 0x60800007c180>{number = 3, name = (null)}
2017-04-19 18:21:07.843 WXAllTest[16145:382366] current thread - <WXThread: 0x60800007c180>{number = 3, name = (null)}
2017-04-19 18:21:08.015 WXAllTest[16145:382366] current thread - <WXThread: 0x60800007c180>{number = 3, name = (null)}
2017-04-19 18:21:08.194 WXAllTest[16145:382366] current thread - <WXThread: 0x60800007c180>{number = 3, name = (null)}
2017-04-19 18:21:08.398 WXAllTest[16145:382366] current thread - <WXThread: 0x60800007c180>{number = 3, name = (null)}
2017-04-19 18:21:08.598 WXAllTest[16145:382366] current thread - <WXThread: 0x60800007c180>{number = 3, name = (null)}
2017-04-19 18:21:08.770 WXAllTest[16145:382366] current thread - <WXThread: 0x60800007c180>{number = 3, name = (null)}

AFNetworking常駐線程保活

常說的AFNetworking常駐線程保活是什么原理?
我們知道,當子線程中的任務執行完畢之后就被銷毀了,那么如果我們需要開啟一個子線程,在程序運行過程中永遠都存在,那么我們就會面臨一個問題,如何讓子線程永遠活著,答案就是給子線程開啟一個RunLoop,下面是AFNetworking相關源碼:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (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;
}

RunLoop 實戰

滾動scrollview導致定時器失效

  • 產生的原因:因為當你滾動textview的時候,runloop會進入UITrackingRunLoopMode 模式,而定時器運行在defaultMode下面,系統一次只能處理一種模式的runloop,所以導致defaultMode下的定時器失效。

  • 解決辦法1:把定時器的runloop的model改為NSRunLoopCommonModes 模式,這個模式是一種占位mode,并不是真正可以運行的mode,它是用來標記一個mode的。默認情況下default和tracking這兩種mode 都會被標記上NSRunLoopCommonModes 標簽。改變定時器的mode為commonMode,可以讓定時器運行在defaultMode和trackingModel兩種模式下,不會出現滾動scrollview導致定時器失效的故障

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
  • 解決辦法2: 使用GCD創建定時器,GCD創建的定時器不會受runloop的影響
// 獲得隊列
dispatch_queue_t queue = dispatch_get_main_queue();

// 創建一個定時器(dispatch_source_t本質還是個OC對象)
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

// 設置定時器的各種屬性(幾時開始任務,每隔多長時間執行一次)
// GCD的時間參數,一般是納秒(1秒 == 10的9次方納秒)
// 比當前時間晚1秒開始執行
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));

//每隔一秒執行一次
uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
dispatch_source_set_timer(self.timer, start, interval, 0);

// 設置回調
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"------------%@", [NSThread currentThread]);

});

// 啟動定時器
dispatch_resume(self.timer);

圖片下載

由于圖片渲染到屏幕需要消耗較多資源,為了提高用戶體驗,當用戶滾動tableview的時候,只在后臺下載圖片,但是不顯示圖片,當用戶停下來的時候才顯示圖片

- (void)viewDidLoad { 
[super viewDidLoad];
 self.thread = [[XMGThread alloc] initWithTarget:self selector:@selector(run) object:nil][self.thread start]; 
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { 
[self performSelector:@selector(useImageView) onThread:self.thread withObject:nil waitUntilDone:NO]; }

- (void)useImageView { 
    // 只在NSDefaultRunLoopMode模式下顯示圖片 
    [self.imageView performSelector:@selector(setImage:) withObject:
    [UIImage imageNamed:@"placeholder"] afterDelay:3.0 
    inModes:@[NSDefaultRunLoopMode]]; 
}
  • 上面的代碼可以達到如下效果:用戶點擊屏幕,在主線程中,三秒之后顯示圖片,但是當用戶點擊屏幕之后,如果此時用戶又開始滾動textview,那么就算過了三秒,圖片也不會顯示出來,當用戶停止了滾動,才會顯示圖片。

  • 這是因為限定了方法setImage只能在NSDefaultRunLoopMode 模式下使用。而滾動textview的時候,程序運行在tracking模式下面,所以方法setImage不會執行。

常駐線程

  • 需要創建一個在后臺一直存在的程序,來做一些需要頻繁處理的任務。比如檢測網絡狀態等。默認情況一個線程創建出來,運行完要做的事情,線程就會消亡。而程序啟動的時候,就創建的主線程已經加入到runloop,所以主線程不會消亡。這個時候我們就需要把自己創建的線程加到runloop中來,就可以實現線程常駐后臺。

  • 如果沒有實現添加NSPort或者NSTimer,會發現執行完run方法,線程就會消亡,后續再執行touchbegan方法無效。我們必須保證線程不消亡,才可以在后臺接受時間處理

RunLoop 啟動前內部必須要有至少一個 Timer/Observer/Source,所以在 [runLoop run] 之前先創建了一個新的 NSMachPort 添加進去了。通常情況下,調用者需要持有這個 NSMachPort (mach_port) 并在外部線程通過這個 port 發送消息到 loop 內;但此處添加 port 只是為了讓 RunLoop 不至于退出,并沒有用于實際的發送消息。

可以發現執行完了run方法,這個時候再點擊屏幕,可以不斷執行test方法,因為線程self.thread一直常駐后臺,等待事件加入其中,然后執行。

在所有UI相應操作之前處理任務

比如我們點擊了一個按鈕,在ui關聯的事件開始執行之前,我們需要執行一些其他任務,可以在observer中實現

[圖片上傳失敗...(image-d1acc1-1550374500252)]

可以看到在按鈕點擊之前,先執行的observe方法里面的代碼。這樣可以攔截事件,讓我們的代碼先UI事件之前執行。

Demo代碼

//
//  ViewController.swift
//  RunLoop
//shui
//  Created by 邱淼 on 16/8/31.
//  Copyright ? 2016年 txcap. All rights reserved.
//
import UIKit
import Foundation
class ViewController: UIViewController {
    
     var thread :NSThread?
    let runTextView = UIScrollView()
    let btn = UIButton()
    
 
      override func viewDidLoad() {
              super.viewDidLoad()
        runTextView.frame = CGRectMake(100, 100, 200, 300)
        runTextView.backgroundColor = UIColor.redColor()
        self.view.addSubview(runTextView)
        btn.frame = CGRectMake(100, 0, 100, 100)
        btn.backgroundColor = UIColor.yellowColor()
        btn.addTarget(self, action: #selector(ViewController.btnclick), forControlEvents: .TouchUpInside)
        self.view.addSubview(btn)
        self.observer()
  
//        self.thread = NSThread.init(target: self, selector:#selector(ViewController.run), object: nil)
//         self.thread?.start()
      }
//*****************************************************在所有UI相應操作之前處理任務**********************
    func btnclick() {
    
      
        print("點擊了Btn")
        
    }
    
    
    func observer()  {
    
        let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,CFRunLoopActivity.AllActivities.rawValue , true, 0) { (observer, activity) in
            print("監聽到RunLoop狀態發生變化----\(activity)")
        }
        
        CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode)
        
    }
        
//*****************************************************常駐線程**********************
     func run() {
        
        print("run===========\(NSThread.currentThread())")
        //方法一
//        NSRunLoop.currentRunLoop().addPort(NSPort.init(), forMode:NSDefaultRunLoopMode)
        //方法二
//        NSRunLoop.currentRunLoop().runMode(NSDefaultRunLoopMode, beforeDate: NSDate.distantFuture())
        //方法三
//        NSRunLoop.currentRunLoop().runUntilDate(NSDate.distantFuture())
        //方法四 添加NSTimer
//        NSTimer.scheduledTimerWithTimeInterval(2.0, target: self, selector: #selector(ViewController.test), userInfo: nil, repeats: true)
//        
     
//        NSRunLoop.currentRunLoop().run()
    }
    
//    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
//        
//         self.performSelector(#selector(ViewController.test), onThread: self.thread!, withObject: nil, waitUntilDone: false)
//        
//    }
//    
//    func test()  {
//             print("test---------------\(NSThread.currentThread())")
//     }
//    
    /*
     如果沒有實現添加NSPort或者NSTimer,會發現執行完run方法,線程就會消亡,后續再執行touchbegan方法無效。
     
     我們必須保證線程不消亡,才可以在后臺接受時間處理
     
     RunLoop 啟動前內部必須要有至少一個 Timer/Observer/Source,所以在 [runLoop run] 之前先創建了一個新的 NSMachPort 添加進去了。通常情況下,調用者需要持有這個 NSMachPort (mach_port) 并在外部線程通過這個 port 發送消息到 loop 內;但此處添加 port 只是為了讓 RunLoop 不至于退出,并沒有用于實際的發送消息。
     
     可以發現執行完了run方法,這個時候再點擊屏幕,可以不斷執行test方法,因為線程self.thread一直常駐后臺,等待事件加入其中,然后執行。
 */
    
//*****************************************************常駐線程**********************
        
 //*****************************************************圖片下載**********************
 
//    
//    //由于圖片渲染到屏幕需要消耗較多資源,為了提高用戶體驗,當用戶滾動tableview的時候,只在后臺下載圖片,但是不顯示圖片,當用戶停下來的時候才顯示圖片。
//    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
//        
//        self.performSelector(#selector(ViewController.userImageView), onThread: self.thread!, withObject: nil, waitUntilDone: false)
//        
//    }
//    
//    func userImageView()  {
//    
   self.imageView.performSelector(#selector(ViewController.setImage), withObject: UIImage(named: "qiyerongzi"), afterDelay: 3, inModes:[NSDefaultRunLoopMode])
//        
//    }
//    
//    //設置圖片
//    func setImage()  {
 
//        self.imageView.image = UIImage(named: "tianxingjiangtang")
//        
//    }
//    
//    /*
//     上面的代碼可以達到如下效果:
//     
//     用戶點擊屏幕,在主線程中,三秒之后顯示圖片
//     
//     但是當用戶點擊屏幕之后,如果此時用戶又開始滾動textview,那么就算過了三秒,圖片也不會顯示出來,當用戶停止了滾動,才會顯示圖片。
//     
//     這是因為限定了方法setImage只能在NSDefaultRunLoopMode 模式下使用。而滾動textview的時候,程序運行在tracking模式下面,所以方法setImage不會執行。
// */
//    
//    
//*****************************************************圖片下載**********************
    
    
    /**
     解決滾動scrollView導致定時器失效
     */
    func scrollerTimer()  {
        //RunLoop 解決滾動scrollView導致定時器失效
        //原因:因為當你滾動textview的時候,runloop會進入UITrackingRunLoopMode 模式,而定時器運行在defaultMode下面,系統一次只能處理一種模式的runloop,所以導致defaultMode下的定時器失效。
        //解決辦法1:把定時器的runloop的model改為NSRunLoopCommonModes 模式,這個模式是一種占位mode,并不是真正可以運行的mode,它是用來標記一個mode的。默認情況下default和tracking這兩種mode 都會被標記上NSRunLoopCommonModes 標簽。改變定時器的mode為commonmodel,可以讓定時器運行在defaultMode和trackingModel兩種模式下,不會出現滾動scrollview導致定時器失效的故障
        //[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        
        //解決辦法2:使用GCD創建定時器,GCD創建的定時器不會受runloop的影響
    }
        override func didReceiveMemoryWarning() {
        super.didReceiveMemoryWarning()
        // Dispose of any resources that can be recreated.
    }

}

參考

  1. miaoqiu/RunLoop: 深入理解RunLoop
  2. 深入理解RunLoop | Garan no dou
  3. RunLoop運行循環機制 - 博客吧
  4. 深入理解 RunLoop | 獨 奏
  5. 孫源的Runloop視頻整理 - 簡書
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容