iOS-RunLoop究竟是怎么運作的

RunLoop簡介(Introduction)

  1. RunLoop是線程基礎(chǔ)架構(gòu)的一部分鲤竹。RunLoop存在的目的是讓線程在沒有任務(wù)處理的時候進入休眠攒驰,在有任務(wù)處理的時候運行吝岭。

  2. RunLoop不是完全自管理的,需要你在適當(dāng)?shù)臅r候啟動威蕉。

  3. Cocoa和Core Foundation框架都提供了RunLoop相關(guān)的API刁俭。

  4. 你不需要自己創(chuàng)建RunLoop對象。每個線程韧涨,包括主線程都有一個對應(yīng)的RunLoop對象牍戚。

  5. 只有子線程的RunLoop需要手動啟動,主線程的RunLoop在App啟動調(diào)用Main函數(shù)時就已運行虑粥。

image

RunLoop的結(jié)構(gòu)

image
  1. NSRunloop:最上層的NSRunloop層實際上是對C語言實現(xiàn)的CFRunloop的一個封裝如孝,實際上它沒干什么事,比如CFRunloop有一個過期時間是double類型娩贷,NSRunloop把它變成了NSDate類型第晰;
  2. CFRunloop:這是真正干事的一層,源代碼是開源的彬祖,并且是跨平臺的茁瘦;
  3. 系統(tǒng)層:底層實現(xiàn)用到了GCD,mach kernel是蘋果的內(nèi)核储笑,比如runloop的睡眠和喚醒就是用mach kernel來實現(xiàn)的甜熔。
    下面是跟Runloop有關(guān)的,我們平時用到的一些模塊突倍,功能等等:
    1)NSTimer計時器腔稀;
    2)UIEvent事件;
    3)Autorelease機制羽历;
    4)NSObject(NSDelayedPerforming):比如這些方法:performSelector:withObject:afterDelay:焊虏,performSelector:withObject:afterDelay:inModes:,cancelPreviousPerformRequestsWithTarget:selector:object:等方法都是和Runloop有關(guān)的秕磷;
    5)NSObject(NSThreadPerformAddition):比如這些方法:performSelectorInBackground:withObject:诵闭,performSelectorOnMainThread:withObject:waitUntilDone:,performSelector:onThread:withObject:waitUntilDone:等方法都是和Runloop有關(guān)的跳夭;
  4. Core Animation層的一些東西:CADisplayLink涂圆,CATransition们镜,CAAnimation等;
  5. dispatch_get_main_queue()润歉;
  6. NSURLConnection模狭;

從調(diào)用堆棧來看Runloop

image

從下往上一層層的看,最開始的start是dyld干的踩衩,然后是main函數(shù)嚼鹉,main函數(shù)接著調(diào)用UIApplicationMain,然后的GSEventRunModal是Graphics Services是處理硬件輸入驱富,比如點擊锚赤,所有的UI事件都是它發(fā)出來的。緊接著的就是Runloop了褐鸥,從圖中的可以看到從13到10的4調(diào)用都是Runloop相關(guān)的线脚。再上面的就是事件隊列處理,以及UI層的事件分發(fā)了叫榕。

  • dyld(the dynamic link editor)是蘋果的動態(tài)鏈接器浑侥,是蘋果操作系統(tǒng)一個重要組成部分,在系統(tǒng)內(nèi)核做好程序準備工作之后晰绎,交由dyld負責(zé)余下的工作寓落。而且它是開源的,任何人可以通過蘋果官網(wǎng)下載它的源碼來閱讀理解它的運作方式荞下,了解系統(tǒng)加載動態(tài)庫的細節(jié)伶选。

幾乎所有線程的所有函數(shù)都是從下面六個函數(shù)之一調(diào)起:

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的構(gòu)成

  • Core Foundation中的CFRunLoopRef
  • NSRunLoop是基于CFRunLoopRef的一層OC包裝,所以要了解RunLoop內(nèi)部結(jié)構(gòu)尖昏,需要多研究CFRunLoopRef層面的API(Core Foundation層面)
RunLoop結(jié)構(gòu)
  1. Runloop與Thread是一一綁定的仰税,但是并不是一個Thread只能起一個Runloop,它可以起很多会宪,但是必須是嵌套結(jié)構(gòu)肖卧,根Runloop只有一個蚯窥;
  2. RunloopMode是指的一個事件循環(huán)必須在某種模式下跑掸鹅,系統(tǒng)會預(yù)定義幾個模式。一個Runloop有多個Mode拦赠;
  3. CFRunloopSource巍沙,CFRunloopTimer,CFRunloopObserver這些元素是在Mode里面的荷鼠,Mode與這些元素的對應(yīng)關(guān)系也是1對多的句携;
    CFRunloopTimer:比如下面的方法都是CFRunloopTimer的封裝:

CFRunLoop 結(jié)構(gòu)

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 和線程是一一對應(yīng)的蠢笋。
  • _blocks_head 是 perform block 加入到里面的

RunLoop 的 Mode

一個 RunLoop 包含若干個 Mode拨齐,每個 Mode 又包含若干個 Source/Timer/Observer。每次調(diào)用 RunLoop 的主函數(shù)時昨寞,只能指定其中一個 Mode瞻惋,這個Mode被稱作 CurrentMode。如果需要切換 Mode援岩,只能退出 Loop歼狼,再重新指定一個 Mode 進入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer享怀,讓其互不影響羽峰。

CFRunLoopMode 和 CFRunLoop 的結(jié)構(gòu)大致如下:

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 結(jié)構(gòu)體的指針
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 內(nèi)部事件
    CFMutableSetRef _sources1; // source1 set,基于Port的仰坦,通過內(nèi)核和其他線程通信履植,接收,分發(fā)系統(tǒng)事件
    CFMutableArrayRef _observers; // observer 數(shù)組
    CFMutableArrayRef _timers; // timer 數(shù)組
    CFMutableDictionaryRef _portToV1SourceMap;// source1 對應(yīng)的端口號
    __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” 中)悄晃。每當(dāng) RunLoop 的內(nèi)容發(fā)生變化時玫霎,RunLoop 都會自動將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 標記的所有Mode里。

Mode列表

  • NSDefaultRunLoopMode:App的默認Mode妈橄,通常主線程是在這個Mode下運行
  • UITrackingRunLoopMode:界面跟蹤 Mode庶近,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
  • UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode眷蚓,啟動完成后就不再使用
  • GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode鼻种,通常用不到
  • NSRunLoopCommonModes: 這是一個占位用的Mode,不是一種真正的Mode commonModes:
  • 一個Mode 可以將自己標記成”Common”屬性(通過將其ModelName 添加到RunLoop的"commonModes" 中)沙热。每當(dāng) RunLoop 的內(nèi)容發(fā)生變化時叉钥,RunLoop 都會自動將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 標記的所有Mode里。
  • 應(yīng)用場景舉例:主線程的 RunLoop 里有兩個預(yù)置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode篙贸。這兩個 Mode 都已經(jīng)被標記為"Common"屬性投队。DefaultMode 是 App 平時所處的狀態(tài),TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態(tài)爵川。當(dāng)你創(chuàng)建一個 Timer 并加到 DefaultMode 時敷鸦,Timer 會得到重復(fù)回調(diào)皿伺,但此時滑動一個TableView時急鳄,RunLoop 會將 mode 切換為 TrackingRunLoopMode唬渗,這時 Timer 就不會被回調(diào)肯骇,并且也不會影響到滑動操作。
  • 有時你需要一個 Timer碟案,在兩個 Mode 中都能得到回調(diào)鳞滨,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式蟆淀,就是將 Timer 加入到commonMode 中拯啦。那么所有被標記為commonMode的mode(defaultMode和TrackingMode)都會執(zhí)行該timer。這樣你在滑動界面的時候也能夠調(diào)用time熔任。

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

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

CFRunLoopSourceRef

CFRunLoopSourceRef 是事件產(chǎn)生的地方。Source有兩個版本:Source0 和 Source1薪贫。
? Source0 只包含了一個回調(diào)(函數(shù)指針)恍箭,它并不能主動觸發(fā)事件。使用時瞧省,你需要先調(diào)用 CFRunLoopSourceSignal(source)扯夭,將這個 Source 標記為待處理,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop鞍匾,讓其處理這個事件交洗。
? Source1 包含了一個 mach_port 和一個回調(diào)(函數(shù)指針),被用于通過內(nèi)核和其他線程相互發(fā)送消息橡淑。這種 Source 能主動喚醒 RunLoop 的線程构拳,其原理在下面會講到。

  1. Source0:處理App內(nèi)部事件梁棠,App自己負責(zé)管理(觸發(fā))置森,如UIEvent,CFSocket掰茶;
  2. Source1:由Runloop和內(nèi)核管理暇藏,mach port驅(qū)動,如CFMachPort(輕量級的進程間通信的方式濒蒋,NSPort就是對它的封裝,還有Runloop的睡眠和喚醒就是通過它來做的),CFMessagePort沪伙;

CFRunLoopTimerRef

CFRunLoopTimerRef 是基于時間的觸發(fā)器瓮顽,它和 NSTimer 是toll-free bridged 的,可以混用围橡。其包含一個時間長度和一個回調(diào)(函數(shù)指針)暖混。當(dāng)其加入到 RunLoop 時,RunLoop會注冊對應(yīng)的時間點翁授,當(dāng)時間點到時拣播,RunLoop會被喚醒以執(zhí)行那個回調(diào)。

RunLoopObserver

CFRunLoopObserver 是觀察者收擦,可以觀察RunLoop的各種狀態(tài)贮配,并拋出回調(diào)∪福可以監(jiān)聽得狀態(tài)如下:

/* 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已經(jīng)退出
 kCFRunLoopAllActivities = 0x0FFFFFFFU
};
  • Source0:非基于Port的泪勒。只包含了一個回調(diào)(函數(shù)指針),它并不能主動觸發(fā)事件宴猾。使用時圆存,你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理仇哆,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop沦辙,讓其處理這個事件。
  • Source1:基于Port的讹剔,通過內(nèi)核和其他線程通信怕轿,接收、分發(fā)系統(tǒng)事件辟拷。這種 Source 能主動喚醒 RunLoop 的線程撞羽。后面講到的創(chuàng)建常駐線程就是在線程中添加一個NSport來實現(xiàn)的。

上面的 Source/Timer/Observer 被統(tǒng)稱為 mode item衫冻,一個 item 可以被同時加入多個 mode诀紊。但一個 item 被重復(fù)加入同一個 mode 時是不會有效果的。如果一個 mode 中一個 item 都沒有隅俘,則 RunLoop 會直接退出邻奠,不進入循環(huán)。

RunLoop的內(nèi)部邏輯

應(yīng)用場景舉例:主線程的 RunLoop 里有兩個預(yù)置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode为居。這兩個 Mode 都已經(jīng)被標記為”Common”屬性碌宴。DefaultMode 是 App 平時所處的狀態(tài),TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態(tài)蒙畴。當(dāng)你創(chuàng)建一個 Timer 并加到 DefaultMode 時贰镣,Timer 會得到重復(fù)回調(diào)呜象,但此時滑動一個TableView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode碑隆,這時 Timer 就不會被回調(diào)恭陡,并且也不會影響到滑動操作。

有時你需要一個 Timer上煤,在兩個 Mode 中都能得到回調(diào)休玩,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式劫狠,就是將 Timer 加入到頂層的 RunLoop 的 “commonModeItems” 中拴疤。”commonModeItems” 被 RunLoop 自動更新到所有具有”Common”屬性的 Mode 里去独泞。

Runloop一次loop執(zhí)行過程

執(zhí)行過程大致描述如下:

  • 通知 observers 即將進入 run loop
  • 通知 observes 即將開始處理 timer source
  • 通知 observes 即將開始處理 source0 事件
  • 執(zhí)行 source0 事件
  • 如果處于主線程有 dispatchPort 消息呐矾,跳轉(zhuǎn)到第9步
  • 通知 observes 線程即將進入休眠
  • 內(nèi)循環(huán)阻塞等待接收系統(tǒng)消息,包括:
  • 收到內(nèi)核發(fā)送過來的消息 (source1消息)
  • 定時器事件需要執(zhí)行
  • run loop 的超時時間到了
  • 手動喚醒 run loop
  • 通知 observes 線程被喚醒
  • 處理通過端口收到的消息:
  • 如果自定義的 timer 被 fire阐肤,那么執(zhí)行該 timer 事件并重新開始循環(huán)凫佛,完成后跳轉(zhuǎn)到第2步
  • 如果 input source 被 fire,則處理該事件
  • 如果 run loop 被手動喚醒孕惜,并且沒有超時愧薛,完成后跳轉(zhuǎn)到第2步
  • 通知 observes run loop 已經(jīng)退出
image

注意:

CFRunLoopDoBlocks 是執(zhí)行 perform block 中的 block
綠色的是RunLoopRun()
第一次循環(huán) CFRunLoopServiceMachPort 是不走的
handle_msg 處理 timer 事件,處理 main queue block 事件衫画,處理 source1 事件
中間的紅色CFRunLoopServiceMachPort是監(jiān)聽 GCD 的端口事件毫炉,只監(jiān)聽一個端口,左下角的CFRunLoopServiceMachPort是堅挺 source1,timer 的削罩,是一個 MutableSet

image

NSTimer 與 GCD Timer

  • NSTimer 是通過 RunLoop 的 RunLoopTimer 把時間加入到 RunLoopMode 里面瞄勾。官方文檔里面也有說 CFRunLoopTimer 和 NSTimer 是可以互相轉(zhuǎn)換的。由于 NSTimer 的這種機制弥激,因此 NSTimer 的執(zhí)行必須依賴于 RunLoop进陡,如果沒有 RunLoop,NSTimer 是不會執(zhí)行的微服。

  • GCD 則不同趾疚,GCD 的線程管理是通過系統(tǒng)來直接管理的。GCD Timer 是通過 dispatch port 給 RunLoop 發(fā)送消息以蕴,來使 RunLoop 執(zhí)行相應(yīng)的 block糙麦,如果所在線程沒有 RunLoop,那么 GCD 會臨時創(chuàng)建一個線程去執(zhí)行 block丛肮,執(zhí)行完之后再銷毀掉赡磅,因此 GCD 的 Timer 是不依賴 RunLoop 的。

  • 至于這兩個 Timer 的準確性問題宝与,如果不再 RunLoop 的線程里面執(zhí)行焚廊,那么只能使用 GCD Timer冶匹,由于 GCD Timer 是基于 MKTimer(mach kernel timer),已經(jīng)很底層了节值,因此是很準確的徙硅。

  • 異步的回調(diào)如果存在延時操作榜聂,那么就要放到有 RunLoop 的線程里面搞疗,否則回調(diào)沒有著陸點無法執(zhí)行

  • NSTimer 必須得在有 RunLoop 的線程里面才能執(zhí)行,另外须肆,使用 NSTimer 的時候會出現(xiàn)滑動 TableView匿乃,Timer 停止的問題,是由于 RunLoopMode 切換的問題豌汇,只要把 NSTimer 加到 common mode 就好了幢炸。

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

  • 崩潰后處理 DSSignalHandlerDemo

RunLoop 與線程的關(guān)系

一般來講逻澳,一個線程一次只能執(zhí)行一個任務(wù)闸天,執(zhí)行完成后線程就會退出。如果我們需要一個機制斜做,讓線程能隨時處理事件但并不退出苞氮,通常的代碼邏輯是這樣的:

  • 每條線程都有唯一的一個與之對應(yīng)的RunLoop對象
  • 主線程的RunLoop已經(jīng)自動創(chuàng)建好了,子線程的RunLoop需要主動創(chuàng)建瓤逼,只要調(diào)用currentRunLoop方法, 系統(tǒng)就會自動創(chuàng)建一個RunLoop, 添加到當(dāng)前線程中
  • 線程剛創(chuàng)建時并沒有 RunLoop笼吟,如果你不主動獲取,那它一直都不會有霸旗。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時贷帮,RunLoop 的銷毀是發(fā)生在線程結(jié)束時。你只能在一個線程的內(nèi)部獲取其 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 Loop诱告。 Event Loop 在很多系統(tǒng)和框架里都有實現(xiàn)撵枢,比如 Node.js 的事件處理,比如 Windows 程序的消息循環(huán)蔬啡,再比如 OSX/iOS 里的 RunLoop诲侮。**實現(xiàn)這種模型的關(guān)鍵點在于:如何管理事件/消息,如何讓線程在沒有處理消息時休眠以避免資源占用箱蟆、在有消息到來時立刻被喚醒沟绪。
**
所以,RunLoop 實際上就是一個對象空猜,這個對象管理了其需要處理的事件和消息绽慈,并提供了一個入口函數(shù)來執(zhí)行上面 Event Loop 的邏輯恨旱。線程執(zhí)行了這個函數(shù)后,就會一直處于這個函數(shù)內(nèi)部 “接受消息->等待->處理” 的循環(huán)中坝疼,直到這個循環(huán)結(jié)束(比如傳入 quit 的消息)搜贤,函數(shù)返回。

首先钝凶,iOS 開發(fā)中能遇到兩個線程對象: pthread_t 和 NSThread仪芒。過去蘋果有份文檔標明了 NSThread 只是 pthread_t 的封裝,但那份文檔已經(jīng)失效了耕陷,現(xiàn)在它們也有可能都是直接包裝自最底層的 mach thread掂名。蘋果并沒有提供這兩個對象相互轉(zhuǎn)換的接口,但不管怎么樣哟沫,可以肯定的是 pthread_t 和 NSThread 是一一對應(yīng)的饺蔑。比如,你可以通過 pthread_main_thread_np() 或 [NSThread mainThread] 來獲取主線程嗜诀;也可以通過 pthread_self() 或 [NSThread currentThread] 來獲取當(dāng)前線程猾警。CFRunLoop 是基于 pthread 來管理的。

蘋果不允許直接創(chuàng)建 RunLoop隆敢,它只提供了兩個自動獲取的函數(shù):CFRunLoopGetMain() 和 CFRunLoopGetCurrent()发皿。 這兩個函數(shù)內(nèi)部的邏輯大概是下面這樣:

AutoreleasePool & Runloop

自動釋放池的創(chuàng)建和釋放,銷毀的時機如下所示

  • kCFRunLoopEntry; // 進入runloop之前筑公,創(chuàng)建一個自動釋放池
  • kCFRunLoopBeforeWaiting; // 休眠之前雳窟,銷毀自動釋放池,創(chuàng)建一個新的自動釋放池
  • kCFRunLoopExit; // 退出runloop之前匣屡,銷毀自動釋放池

事件響應(yīng)

  • 蘋果注冊了一個 Source1 (基于 mach port 的) 用來接收系統(tǒng)事件封救,當(dāng)一個硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard 接收捣作。SpringBoard 只接收按鍵(鎖屏/靜音等)誉结,觸摸,加速券躁,接近傳感器等幾種 Event惩坑,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進程。

  • 隨后蘋果注冊的那個 Source1 就會觸發(fā)回調(diào)也拜,并調(diào)用 _UIApplicationHandleEventQueue() 進行應(yīng)用內(nèi)部的分發(fā)以舒。

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

手勢識別 & Runloop

當(dāng)上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時卵贱,其首先會調(diào)用 Cancel 將當(dāng)前的 touchesBegin/Move/End 系列回調(diào)打斷滥沫。隨后系統(tǒng)將對應(yīng)的 UIGestureRecognizer 標記為待處理侣集。蘋果注冊了一個 Observer 監(jiān)測 BeforeWaiting (Loop即將進入休眠) 事件,這個Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver()兰绣,其內(nèi)部會獲取所有剛被標記為待處理的 GestureRecognizer世分,并執(zhí)行GestureRecognizer的回調(diào)。當(dāng)有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時缀辩,這個回調(diào)都會進行相應(yīng)處理臭埋。

界面更新 & Runloop

當(dāng)在操作 UI 時,比如改變了 Frame雌澄、更新了 UIView/CALayer 的層次時斋泄,或者手動調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后杯瞻,這個 UIView/CALayer 就被標記為待處理镐牺,并被提交到一個全局的容器去。蘋果注冊了一個 Observer 監(jiān)聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件魁莉,回調(diào)去執(zhí)行一個很長的函數(shù):_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()睬涧。這個函數(shù)里會遍歷所有待處理的 UIView/CAlayer 以執(zhí)行實際的繪制和調(diào)整,并更新 UI 界面旗唁。

定時器 & Runloop

NSTimer 其實就是 CFRunLoopTimerRef畦浓,他們之間是 toll-free bridged 的。一個 NSTimer 注冊到 RunLoop 后检疫,RunLoop 會為其重復(fù)的時間點注冊好事件讶请。例如 10:00, 10:10, 10:20 這幾個時間點。RunLoop為了節(jié)省資源屎媳,并不會在非常準確的時間點回調(diào)這個Timer夺溢。Timer 有個屬性叫做 Tolerance (寬容度),標示了當(dāng)時間點到后烛谊,容許有多少最大誤差风响。

如果某個時間點被錯過了,例如執(zhí)行了一個很長的任務(wù)丹禀,則那個時間點的回調(diào)也會跳過去状勤,不會延后執(zhí)行。就比如等公交双泪,如果 10:10 時我忙著玩手機錯過了那個點的公交持搜,那我只能等 10:20 這一趟了。

CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現(xiàn)原理更復(fù)雜焙矛,和 NSTimer 并不一樣葫盼,其內(nèi)部實際是操作了一個 Source)。如果在兩次屏幕刷新之間執(zhí)行了一個長任務(wù)薄扁,那其中就會有一幀被跳過去(和 NSTimer 相似)剪返,造成界面卡頓的感覺废累。在快速滑動TableView時,即使一幀的卡頓也會讓用戶有所察覺脱盲。Facebook 開源的 AsyncDisplayLink 就是為了解決界面卡頓的問題邑滨,其內(nèi)部也用到了 RunLoop

RunLoop剖析

Run Loop Modes

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

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

  2. 在RunLoop運行過程中,只有當(dāng)前Mode中的事件會被處理面哥,其他Mode中的事件會被暫停哎壳,直到該RunLoop在該Mode下運行。

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

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

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

Mode Name Description
Default NSDefaultRunLoopMode(Cocoa) kCFRunLoopDefaultMode (Core Foundation) 默認Mode姆蘸,APP運行起來之后,主線程的RunLoop默認在該Mode下運行
GSEventReceiveRunLoopMode(Cocoa) 接收系統(tǒng)內(nèi)部事件
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種兰粉,一種是內(nèi)核通過端口發(fā)送消息產(chǎn)生的(Port-Based Sources),另一種是開發(fā)者自定義的(Custom Input Sources)顶瞳。這兩種時間只在標記方面有所區(qū)別玖姑,內(nèi)核消息由內(nèi)核自動標記,開發(fā)者自定義的事件由開發(fā)者手動標記慨菱。

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

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

自定義事件源(Custom Input Sources)

1. 使用Core Foundation中的CFRunLoopSourceRef來創(chuàng)建一個自定義的source闪彼。

Perform Selector接口(Cocoa Perform Selector Sources)

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

定時器(Timer Sources)

1.這里的定時器事件并一定時間到了就會執(zhí)行描馅。就像input source一樣把夸,timer需要被加入到指定的Mode中,并且RunLoop要運行在這個Mode下铭污,timer才有效恋日。

2.Runloop中的定時器也不是精準定時器。RunLoop是一個循環(huán)一直跑嘹狞,在某次循環(huán)運行中途加入的定時器事件岂膳,只有等到下一次循環(huán)才會被執(zhí)行。

觀察者(Run Loop Observers)

1.RunLoop在狀態(tài)改變的時候回發(fā)出通知磅网,你可以監(jiān)聽這些通知來做一些有用的事情谈截。比如在線程運行前、要休眠之前等時候做一些準備工作知市。

2.可以監(jiān)聽的RunLoop狀態(tài)有這些:

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

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

The Run Loop Sequence of Events

1.RunLoop每次循環(huán)的執(zhí)行步驟大概如下:

  1. 通知observers 已經(jīng)進入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嫂丙,跳轉(zhuǎn)到第9步
  6. 通知observes線程即將進入休眠
  7. 讓線程進入休眠狀態(tài),直到有以下事件發(fā)生:
    • 收到內(nèi)核發(fā)送過來的消息
    • 定時器事件需要執(zhí)行
    • RunLoop的超時時間到了
    • 手動喚醒RunLoop
  8. 通知observes 線程被喚醒
  9. 處理待處理的事件:
    • 如果自定義的timer被fire规哲,那么執(zhí)行該timer事件并重新開始循環(huán)跟啤,跳轉(zhuǎn)到第2步
    • 如果input source被fire,則處理該事件
    • 如果RunLoop被手動喚醒唉锌,并且沒有超時隅肥,那么重新開始循環(huán),跳轉(zhuǎn)到第2步
  10. 通知observes RunLoop已經(jīng)退出

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

RunLoop 作用

總結(jié)下來绿语,RunLoop 的作用主要體現(xiàn)在三方面:

  • 保持程序的持續(xù)運行
  • 處理App中的各種事件(比如觸摸事件秃症、定時器事件、Selector事件)
  • 節(jié)省CPU資源吕粹,提高程序性能:該做事的時候做事种柑,該休息的時候休息
  • 就是說,如果沒有 RunLoop 程序一運行就結(jié)束了匹耕,你根本不可能看到持續(xù)運行的 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.只有在創(chuàng)建一個子線程的時候驶赏,開發(fā)者才必要顯式的手動的把RunLoop run起來炸卑。主線程的RunLoop是自動啟動的,不需要手動run煤傍。

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

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

操作RunLoop對象(Using Run Loop Objects)

1.每個線程都對應(yīng)一個RunLoop返帕。一個RunLoop對象提供了添加、運行各種source的接口篙挽。

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

3.可以使用以下方法來獲取當(dāng)前線程的RunLoop:

  • 使用NSRunLoop中的currentRunLoop函數(shù)
  • 使用CFRunLoopGetCurrent函數(shù)

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

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

6.此外,添加一個observes也可以讓RunLoop不至于馬上退出轿衔。你可以使用CFRunLoopObserverRef來創(chuàng)建observe沉迹,并使用CFRunLoopAddObserver函數(shù)來添加到RunLoop中

7.是否是線程安全的取決于你用什么API來操作RunLoop。Core Foundation框架中CFRunLoop相關(guān)的API都是線程安全的害驹,并且可以在任何線程中調(diào)用鞭呕。Cocoa框架中的NSRunLoop相關(guān)的API不是線程安全的牢撼。你最好在某個線程中只操作該線程的RunLoop双揪。如果你在線程1中用NSRunLoop的API向線程2的RunLoop添加source英染,可能會引起crash蜂大。

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

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

  • 使程序一直運行并接受用戶輸入:我們的app必然不能像命令式執(zhí)行一樣枷恕,執(zhí)行完就退出了党晋,我們需要app在我們不殺死它的時候一直運行著,并在由用戶事件的時候能夠響應(yīng),比如網(wǎng)絡(luò)輸入未玻,用戶點擊等等灾而,這是Runloop的首要任務(wù);
  • 決定程序在何時應(yīng)該處理哪些事件:實際上程序會有很多事件扳剿,Runloop會有一定的機制來管理時間的處理時機等旁趟;
  • 調(diào)用解耦(Message Queue):比方說手指點擊滑動會產(chǎn)生UIEvent事件,對于主調(diào)方來說庇绽,我不可能等到這個事件被執(zhí)行了才去產(chǎn)生下一個事件锡搜,也就是主調(diào)方不能被被調(diào)方卡住。那么在實際實現(xiàn)中瞧掺,被調(diào)方會有一個消息隊列耕餐,主調(diào)方會把消息扔到消息隊列中,然后不管了辟狈,消息的處理是由被調(diào)方不斷去從消息隊列中取消息肠缔,然后執(zhí)行的。這樣的好處是主調(diào)方不需要知道消息是具體是怎么執(zhí)行的哼转,只要產(chǎn)生消息即可明未,從而實現(xiàn)了解耦;
image

如果沒有RunLoop

image

有了RunLoop

image

RunLoop的應(yīng)用

RunLoop在實際開發(fā)過程中的應(yīng)用(二) - 簡書

  1. UIImageView延遲加載照片
  2. 線程币悸活
  3. 子線程中執(zhí)行NSTimer
  4. performSelector
  5. 自動釋放池

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

下面就拿UITableView來舉例說明:

UITableView 的 cell 上顯示網(wǎng)絡(luò)圖片庶溶,一般需要兩步煮纵,第一步下載網(wǎng)絡(luò)圖片;第二步偏螺,將網(wǎng)絡(luò)圖片設(shè)置到UIImageView上。

  • 第一步匆光,我們一般都是放在子線程中來做套像,這個不做贅述。
  • 第二步终息,一般是回到主線程去設(shè)置夺巩。有了前兩篇文章關(guān)于Mode的切換,想必你已經(jīng)知道怎么做了周崭。
    就是在為圖片視圖設(shè)置圖片時柳譬,在主線程設(shè)置,并調(diào)用performSelector:withObject:afterDelay:inModes:方法续镇。最后一個參數(shù)美澳,僅設(shè)置一個NSDefaultRunLoopMode。
UIImage *downloadedImage = ....;
[self.myImageView performSelector:@selector(setImage:) withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

當(dāng)然,即使是讀取沙盒或者bundle內(nèi)的圖片制跟,我們也可以運用這一點來改善視圖的滑動舅桩。但是如果UITableView上的圖片都是默認圖,似乎也不是很好雨膨,你需要自己來權(quán)衡了擂涛。

線程保活

可能你的項目中需要一個線程聊记,一直在后臺做些耗時操作撒妈,但是不影響主線程,我們不要一直大量的創(chuàng)建和銷毀線程排监,因為這樣太浪費性能了狰右,我們只要保留這個線程,只要對他進行“鄙缏叮活”就行

//繼承了一個NSTread 線程挟阻,然后使用vc中創(chuàng)建和執(zhí)行某個任務(wù),查看線程的情況
- (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");
}

//每一次點擊屏幕的時候峭弟,線程執(zhí)行完方法附鸽,直接釋放掉了,下一次創(chuàng)建了一個新的線程瞒瘸;
//子線程存活的時間很短坷备,只要執(zhí)行完畢任務(wù),就會被釋放
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)}

如果我每隔一段時間就像在線程中執(zhí)行某個操作情臭,好像現(xiàn)在不行
如果我們將線程對象強引用省撑,也是不行的,會崩潰

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

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

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

4.在點擊屏幕的時候肥败,執(zhí)行一個方法,線程之間的數(shù)據(jù)通信

- (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]);
}

//打印結(jié)果:同一個線程愕提,線程甭裕活成功
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常駐線程鼻城龋活是什么原理纽谒?
我們知道,當(dāng)子線程中的任務(wù)執(zhí)行完畢之后就被銷毀了如输,那么如果我們需要開啟一個子線程鼓黔,在程序運行過程中永遠都存在央勒,那么我們就會面臨一個問題,如何讓子線程永遠活著请祖,答案就是給子線程開啟一個RunLoop订歪,下面是AFNetworking相關(guān)源碼:

+ (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 實戰(zhàn)

滾動scrollview導(dǎo)致定時器失效

  • 產(chǎn)生的原因:因為當(dāng)你滾動textview的時候,runloop會進入UITrackingRunLoopMode 模式肆捕,而定時器運行在defaultMode下面刷晋,系統(tǒng)一次只能處理一種模式的runloop,所以導(dǎo)致defaultMode下的定時器失效慎陵。

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

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

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

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

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

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

});

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

圖片下載

由于圖片渲染到屏幕需要消耗較多資源,為了提高用戶體驗舶赔,當(dāng)用戶滾動tableview的時候扫倡,只在后臺下載圖片,但是不顯示圖片竟纳,當(dāng)用戶停下來的時候才顯示圖片撵溃。

- (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]]; 
}
  • 上面的代碼可以達到如下效果:用戶點擊屏幕,在主線程中锥累,三秒之后顯示圖片缘挑,但是當(dāng)用戶點擊屏幕之后,如果此時用戶又開始滾動textview桶略,那么就算過了三秒卖哎,圖片也不會顯示出來,當(dāng)用戶停止了滾動删性,才會顯示圖片。

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

常駐線程

  • 需要創(chuàng)建一個在后臺一直存在的程序巴帮,來做一些需要頻繁處理的任務(wù)溯泣。比如檢測網(wǎng)絡(luò)狀態(tài)等。默認情況一個線程創(chuàng)建出來榕茧,運行完要做的事情垃沦,線程就會消亡。而程序啟動的時候用押,就創(chuàng)建的主線程已經(jīng)加入到runloop肢簿,所以主線程不會消亡。這個時候我們就需要把自己創(chuàng)建的線程加到runloop中來蜻拨,就可以實現(xiàn)線程常駐后臺池充。

  • 如果沒有實現(xiàn)添加NSPort或者NSTimer,會發(fā)現(xiàn)執(zhí)行完run方法缎讼,線程就會消亡收夸,后續(xù)再執(zhí)行touchbegan方法無效。我們必須保證線程不消亡血崭,才可以在后臺接受時間處理

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

可以發(fā)現(xiàn)執(zhí)行完了run方法忱详,這個時候再點擊屏幕,可以不斷執(zhí)行test方法跺涤,因為線程self.thread一直常駐后臺匈睁,等待事件加入其中,然后執(zhí)行桶错。

在所有UI相應(yīng)操作之前處理任務(wù)

比如我們點擊了一個按鈕航唆,在ui關(guān)聯(lián)的事件開始執(zhí)行之前,我們需要執(zhí)行一些其他任務(wù)院刁,可以在observer中實現(xiàn)

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

可以看到在按鈕點擊之前糯钙,先執(zhí)行的observe方法里面的代碼。這樣可以攔截事件退腥,讓我們的代碼先UI事件之前執(zhí)行任岸。

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相應(yīng)操作之前處理任務(wù)**********************
    func btnclick() {
    
      
        print("點擊了Btn")
        
    }
    
    
    func observer()  {
    
        let observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault,CFRunLoopActivity.AllActivities.rawValue , true, 0) { (observer, activity) in
            print("監(jiān)聽到RunLoop狀態(tài)發(fā)生變化----\(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())")
//     }
//    
    /*
     如果沒有實現(xiàn)添加NSPort或者NSTimer,會發(fā)現(xiàn)執(zhí)行完run方法狡刘,線程就會消亡享潜,后續(xù)再執(zhí)行touchbegan方法無效。
     
     我們必須保證線程不消亡嗅蔬,才可以在后臺接受時間處理
     
     RunLoop 啟動前內(nèi)部必須要有至少一個 Timer/Observer/Source剑按,所以在 [runLoop run] 之前先創(chuàng)建了一個新的 NSMachPort 添加進去了疾就。通常情況下,調(diào)用者需要持有這個 NSMachPort (mach_port) 并在外部線程通過這個 port 發(fā)送消息到 loop 內(nèi)艺蝴;但此處添加 port 只是為了讓 RunLoop 不至于退出猬腰,并沒有用于實際的發(fā)送消息。
     
     可以發(fā)現(xiàn)執(zhí)行完了run方法猜敢,這個時候再點擊屏幕姑荷,可以不斷執(zhí)行test方法,因為線程self.thread一直常駐后臺锣枝,等待事件加入其中厢拭,然后執(zhí)行。
 */
    
//*****************************************************常駐線程**********************
        
 //*****************************************************圖片下載**********************
 
//    
//    //由于圖片渲染到屏幕需要消耗較多資源撇叁,為了提高用戶體驗供鸠,當(dāng)用戶滾動tableview的時候,只在后臺下載圖片陨闹,但是不顯示圖片楞捂,當(dāng)用戶停下來的時候才顯示圖片。
//    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])
//        
//    }
//    
//    //設(shè)置圖片
//    func setImage()  {
 
//        self.imageView.image = UIImage(named: "tianxingjiangtang")
//        
//    }
//    
//    /*
//     上面的代碼可以達到如下效果:
//     
//     用戶點擊屏幕趋厉,在主線程中寨闹,三秒之后顯示圖片
//     
//     但是當(dāng)用戶點擊屏幕之后,如果此時用戶又開始滾動textview君账,那么就算過了三秒繁堡,圖片也不會顯示出來,當(dāng)用戶停止了滾動乡数,才會顯示圖片椭蹄。
//     
//     這是因為限定了方法setImage只能在NSDefaultRunLoopMode 模式下使用。而滾動textview的時候净赴,程序運行在tracking模式下面绳矩,所以方法setImage不會執(zhí)行。
// */
//    
//    
//*****************************************************圖片下載**********************
    
    
    /**
     解決滾動scrollView導(dǎo)致定時器失效
     */
    func scrollerTimer()  {
        //RunLoop 解決滾動scrollView導(dǎo)致定時器失效
        //原因:因為當(dāng)你滾動textview的時候玖翅,runloop會進入UITrackingRunLoopMode 模式翼馆,而定時器運行在defaultMode下面,系統(tǒng)一次只能處理一種模式的runloop金度,所以導(dǎo)致defaultMode下的定時器失效应媚。
        //解決辦法1:把定時器的runloop的model改為NSRunLoopCommonModes 模式,這個模式是一種占位mode猜极,并不是真正可以運行的mode珍特,它是用來標記一個mode的。默認情況下default和tracking這兩種mode 都會被標記上NSRunLoopCommonModes 標簽魔吐。改變定時器的mode為commonmodel扎筒,可以讓定時器運行在defaultMode和trackingModel兩種模式下,不會出現(xiàn)滾動scrollview導(dǎo)致定時器失效的故障
        //[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
        
        //解決辦法2:使用GCD創(chuàng)建定時器酬姆,GCD創(chuàng)建的定時器不會受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運行循環(huán)機制 - 博客吧
  4. 深入理解 RunLoop | 獨 奏
  5. 孫源的Runloop視頻整理 - 簡書
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嗜桌,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子辞色,更是在濱河造成了極大的恐慌骨宠,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,185評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件相满,死亡現(xiàn)場離奇詭異层亿,居然都是意外死亡,警方通過查閱死者的電腦和手機立美,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,652評論 3 393
  • 文/潘曉璐 我一進店門匿又,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人建蹄,你說我怎么就攤上這事碌更。” “怎么了洞慎?”我有些...
    開封第一講書人閱讀 163,524評論 0 353
  • 文/不壞的土叔 我叫張陵痛单,是天一觀的道長。 經(jīng)常有香客問我劲腿,道長旭绒,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,339評論 1 293
  • 正文 為了忘掉前任焦人,我火速辦了婚禮挥吵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘垃瞧。我一直安慰自己蔫劣,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,387評論 6 391
  • 文/花漫 我一把揭開白布个从。 她就那樣靜靜地躺著脉幢,像睡著了一般。 火紅的嫁衣襯著肌膚如雪嗦锐。 梳的紋絲不亂的頭發(fā)上嫌松,一...
    開封第一講書人閱讀 51,287評論 1 301
  • 那天,我揣著相機與錄音奕污,去河邊找鬼萎羔。 笑死,一個胖子當(dāng)著我的面吹牛碳默,可吹牛的內(nèi)容都是我干的贾陷。 我是一名探鬼主播缘眶,決...
    沈念sama閱讀 40,130評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼髓废!你這毒婦竟也來了巷懈?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,985評論 0 275
  • 序言:老撾萬榮一對情侶失蹤慌洪,失蹤者是張志新(化名)和其女友劉穎顶燕,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體冈爹,經(jīng)...
    沈念sama閱讀 45,420評論 1 313
  • 正文 獨居荒郊野嶺守林人離奇死亡涌攻,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,617評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了频伤。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片恳谎。...
    茶點故事閱讀 39,779評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖剂买,靈堂內(nèi)的尸體忽然破棺而出惠爽,到底是詐尸還是另有隱情,我是刑警寧澤瞬哼,帶...
    沈念sama閱讀 35,477評論 5 345
  • 正文 年R本政府宣布婚肆,位于F島的核電站,受9級特大地震影響坐慰,放射性物質(zhì)發(fā)生泄漏较性。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,088評論 3 328
  • 文/蒙蒙 一结胀、第九天 我趴在偏房一處隱蔽的房頂上張望赞咙。 院中可真熱鬧,春花似錦糟港、人聲如沸攀操。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,716評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽速和。三九已至,卻和暖如春剥汤,著一層夾襖步出監(jiān)牢的瞬間颠放,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,857評論 1 269
  • 我被黑心中介騙來泰國打工吭敢, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留碰凶,地道東北人。 一個月前我還...
    沈念sama閱讀 47,876評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像欲低,于是被迫代替她去往敵國和親辕宏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,700評論 2 354

推薦閱讀更多精彩內(nèi)容