iOS全解2:Runloop

引用

一、基本概念
自我理解:RunLoop 就是線程資源管理器。

它的本質(zhì)是一個(gè)事件循環(huán),用于管理線程的生命周期和事件處理。
不斷地從事件源中接收事件,并處理事件(觸摸、定時(shí)器、網(wǎng)絡(luò)等),沒(méi)有事件時(shí)讓線程休眠。

  • 線程相關(guān):每個(gè)線程都有一個(gè)對(duì)應(yīng)的 RunLoop,只有主線程的 RunLoop 是自動(dòng)創(chuàng)建并運(yùn)行的,子線程的 RunLoop 需要手動(dòng)啟動(dòng)。
  • 模式(Mode):RunLoop 運(yùn)行在不同的模式下,每種模式下可以注冊(cè)不同的事件源(如輸入源、定時(shí)器等)。常見(jiàn)的模式包括 NSDefaultRunLoopMode、UITrackingRunLoopMode 等。
二、RunLoop 的結(jié)構(gòu),組成部分
  • 輸入源:用于接收來(lái)自其他線程或應(yīng)用的事件。分為兩類:
    • 基于端口的輸入源:系統(tǒng)自動(dòng)觸發(fā)。
    • 自定義輸入源:開(kāi)發(fā)者可以自定義的事件(如觸摸事件)。
  • 定時(shí)器源:用于處理定時(shí)器事件。NSTimer 就是基于 RunLoop 的定時(shí)器。
  • 觀察者Observers:用于監(jiān)聽(tīng) RunLoop 的狀態(tài)變化。例如 RunLoop 即將進(jìn)入休眠、即將處理事件等。

三、 RunLoop 的運(yùn)行流程

RunLoop 的運(yùn)行流程可以簡(jiǎn)化為以下幾個(gè)步驟:

  • 通知觀察者:RunLoop 即將啟動(dòng)。
  • 處理定時(shí)器:檢查并處理即將觸發(fā)的定時(shí)器。
  • 處理輸入源:處理已準(zhǔn)備好的輸入源。
  • 進(jìn)入休眠:如果沒(méi)有事件需要處理,RunLoop 進(jìn)入休眠狀態(tài),等待事件喚醒。
  • 喚醒:當(dāng)有事件到達(dá)時(shí),RunLoop 被喚醒。
  • 處理事件:處理喚醒 RunLoop 的事件。
  • 通知觀察者:RunLoop 即將退出。

四、 RunLoop 的模式

RunLoop 可以運(yùn)行在不同的模式下,常見(jiàn)的模式有:

  • NSDefaultRunLoopMode:默認(rèn)模式,通常用于處理大多數(shù)事件。
  • UITrackingRunLoopMode:用于跟蹤用戶交互事件(如滑動(dòng) UIScrollView)。
  • NSRunLoopCommonModes:一個(gè)集合模式,包含 NSDefaultRunLoopMode 和 UITrackingRunLoopMode。

五、 RunLoop 的常見(jiàn)問(wèn)題和使用場(chǎng)景

每個(gè)線程都有且只有一個(gè) RunLoop,但只有主線程的 RunLoop 是默認(rèn)開(kāi)啟的。

線程類型 RunLoop 狀態(tài) 說(shuō)明
主線程 自動(dòng)開(kāi)啟 UIApplicationMain() 中自動(dòng)啟動(dòng)
子線程 默認(rèn)關(guān)閉 需要手動(dòng)獲取并運(yùn)行
方面 說(shuō)明
定義 事件循環(huán),管理線程生命周期
線程關(guān)系 一對(duì)一,主線程自動(dòng)開(kāi)啟
核心組件 Mode、Source、Timer、Observer
主要作用 保持線程存活、處理事件、節(jié)省 CPU
常見(jiàn)應(yīng)用 定時(shí)器、延遲執(zhí)行、卡頓檢測(cè)、保持子線程
  • 定時(shí)器不準(zhǔn)確:NSTimer 的觸發(fā)依賴于 RunLoop,如果 RunLoop 沒(méi)有運(yùn)行 或 正在處理其他事件,定時(shí)器的觸發(fā)可能會(huì)被延遲。
  • RunLoop 阻塞:如果在 RunLoop 中執(zhí)行了耗時(shí)操作,會(huì)導(dǎo)致 RunLoop 無(wú)法及時(shí)處理其他事件,造成界面卡頓。
  • 線程?;?常駐:通過(guò)啟動(dòng) RunLoop 可以讓線程常駐,但需要注意在適當(dāng)?shù)臅r(shí)候停止 RunLoop,避免資源浪費(fèi)。
  1. 定時(shí)器的 Mode 切換問(wèn)題:UIScrollView 滑動(dòng)時(shí),定時(shí)器會(huì)暫停工作。
  2. 監(jiān)控界面卡頓:檢測(cè)主線程卡頓,用于性能監(jiān)控
  3. 創(chuàng)建常駐線程:需要長(zhǎng)時(shí)間在后臺(tái)執(zhí)行任務(wù),避免頻繁創(chuàng)建銷毀線程。

引用:CADisplayLink 與 Runloop Observer 都能監(jiān)控卡頓

iOS FPS 監(jiān)控詳解



定時(shí)器不準(zhǔn)確使用:dispatch_source_t、CADisplayLink、NSTimer添加到 CommonModes
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

原因 影響程度 解決方案
RunLoop 繁忙 ????? 使用 GCD Timer
Mode 切換 ???? 添加到 CommonModes
線程阻塞 ???? 避免阻塞,或用獨(dú)立線程
系統(tǒng)負(fù)載 ??? 優(yōu)化性能
時(shí)鐘變化 ?? 使用 CACurrentMediaTime()
誤差累積 ?? 使用 dispatch_source_t

最佳實(shí)踐:

  • 普通定時(shí)任務(wù)(秒級(jí)精度): NSTimer + CommonModes
  • 精確計(jì)時(shí)(毫秒級(jí)): dispatch_source_t
  • 動(dòng)畫(huà)幀同步 : CADisplayLink
  • 現(xiàn)代開(kāi)發(fā) : 考慮 async/await + Task.sleep


一般的使用功能:

-自動(dòng)釋放池
-延遲回調(diào)
-觸摸事件
-屏幕刷新

一般來(lái)講,一個(gè)線程一次只能執(zhí)行一個(gè)任務(wù),執(zhí)行完成后線程就會(huì)退出(銷毀)。如果我們需要一個(gè)機(jī)制,讓線程能隨時(shí)處理事件但并不退出。
實(shí)現(xiàn)這種模型的關(guān)鍵點(diǎn)在于:如何管理事件/消息,如何讓線程在沒(méi)有處理消息時(shí)休眠,以避免資源占用、在有消息到來(lái)時(shí)立刻被喚醒。

一個(gè)RunLoop就是一個(gè)事件處理循環(huán),用來(lái)不停的調(diào)配工作以及處理輸入事件。使用RunLoop的目的是使你的線程在有工作的時(shí)候工作,沒(méi)有的時(shí)候休眠。NSRunloop可以保持一個(gè)線程一直為活躍狀態(tài),不會(huì)馬上銷毀
在多線程中使用定時(shí)器必須開(kāi)啟Runloop,因?yàn)橹挥虚_(kāi)啟Runloop保持線程為活躍狀態(tài),定時(shí)器才能運(yùn)行正常。(即:一個(gè)線程對(duì)應(yīng)一個(gè) RunLoop)

# 定時(shí)器
- (void)viewDidLoad
{
    [super viewDidLoad];
    [self performSelectorInBackground:@selector(multithread) withObject:nil];
}
-(void)multithread {
    NSLog(@"HE");
    [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(timeAction) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] run];
}
-(void)timeAction {
    NSLog(@"Hello");
}

有兩種方法可以讓RunLoop處理事件之前退出:
1、給 runloop設(shè)置超時(shí)時(shí)間
2、通知runloop停止

OSX/iOS 系統(tǒng)中,提供了兩個(gè)這樣的對(duì)象:NSRunLoop 和 CFRunLoopRef。
1、CFRunLoopRef 是在 CoreFoundation 框架內(nèi)的(CF),它提供了純 C 函數(shù)的 API,所有這些 API 都是線程安全的。
2、NSRunLoop 是基于 CFRunLoopRef 的封裝,提供了面向?qū)ο蟮?API,但是這些 API 不是線程安全的。


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

首先,iOS 開(kāi)發(fā)中能遇到兩個(gè)線程對(duì)象: pthread_tNSThread。過(guò)去蘋(píng)果有份文檔標(biāo)明了 NSThread 只是對(duì) pthread_t 的封裝,但那份文檔已經(jīng)失效了,現(xiàn)在它們也有可能都是直接包裝自最底層的 mach thread。蘋(píng)果并沒(méi)有提供這兩個(gè)對(duì)象相互轉(zhuǎn)換的接口,但不管怎么樣,可以肯定的是 pthread_t 和 NSThread 是一一對(duì)應(yīng)的。比如,你可以通過(guò) pthread_main_thread_np()[NSThread mainThread]來(lái)獲取主線程;也可以通過(guò) pthread_self() 或 [NSThread currentThread] 來(lái)獲取當(dāng)前線程。CFRunLoop 是基于 pthread 來(lái)管理的

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

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
static CFSpinLock_t loopsLock;  /// 訪問(wèn) loopsDic 時(shí)的鎖(自旋鎖)
 
/// 獲取一個(gè) pthread 對(duì)應(yīng)的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    if (!loopsDic) {
        // 第一次進(jìn)入時(shí),初始化全局Dic,并先為主線程創(chuàng)建一個(gè) RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    /// 直接從 Dictionary 里獲取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    if (!loop) {
        /// 取不到時(shí),創(chuàng)建一個(gè)
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注冊(cè)一個(gè)回調(diào),當(dāng)線程銷毀時(shí),順便也銷毀其對(duì)應(yīng)的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    OSSpinLockUnLock(&loopsLock);
    return loop;
}

CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}


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

GCD 可以同時(shí)處理多個(gè)線程,一個(gè)線程可以處理多個(gè)事件,線程的 使用和睡眠 由RunLoop控制,避免浪費(fèi)系統(tǒng)資源。

RunLoop與線程 .png

一個(gè) RunLoop 包含若干個(gè) Mode,每個(gè) Mode 又包含若干個(gè) Source、Timer、Observer。每次調(diào)用 RunLoop 的主函數(shù)時(shí),只能指定其中一個(gè) Mode,這個(gè)Mode被稱作 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個(gè) Mode 進(jìn)入。這樣做主要是為了分隔開(kāi)不同組的 Source/Timer/Observer,讓其互不影響。
(即:每個(gè)事件都要注冊(cè) Observer 觀察者,切換 Mode就是切換事件源)

概念了解

  • 切換事件源:比如兩個(gè)事件 1.一個(gè)文件下載;2.數(shù)據(jù)請(qǐng)求。在同一條線程上,當(dāng)文件下載暫停時(shí),就切換到等待的事件2進(jìn)行處理

  • 切換上下文:CPU從一個(gè)進(jìn)程或線程切換到另一個(gè)進(jìn)程或線程。
    上下文切換對(duì)系統(tǒng)來(lái)說(shuō)意味著消耗大量的CPU時(shí)間。


RunLoop 對(duì)外的接口

在 CoreFoundation 里面關(guān)于 RunLoop 有5個(gè)類:

  • CFRunLoopRef
  • CFRunLoopModeRef(Mode
  • CFRunLoopSourceRef(Source
  • CFRunLoopTimerRef(Timer
  • CFRunLoopObserverRef(Observer

其中 CFRunLoopModeRef 類并沒(méi)有對(duì)外暴露,只是通過(guò) CFRunLoopRef 的接口進(jìn)行了封裝。他們的關(guān)系如下:

RunLoop與Mode.png

從上面的代碼可以看出,線程和 RunLoop 之間是一一對(duì)應(yīng)的,其關(guān)系是保存在一個(gè)全局的 Dictionary 里。線程剛創(chuàng)建時(shí)并沒(méi)有 RunLoop,如果你不主動(dòng)獲取,那它一直都不會(huì)有。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時(shí),RunLoop 的銷毀是發(fā)生在線程結(jié)束時(shí)。你只能在一個(gè)線程的內(nèi)部獲取其 RunLoop(主線程除外)。

1、基于非端口的源: Source0(待處理源)
??? ? 自定義輸入源
??? ? Cocoa執(zhí)行選擇器源
2、基于端口的源:? ? ? Source1(喚醒源)

CFRunLoopObserverRef,對(duì)應(yīng) observer,表示觀察者。每個(gè) Observer 都包含了一個(gè)回調(diào)(函數(shù)指針),當(dāng) RunLoop 的狀態(tài)發(fā)生變化時(shí),觀察者就能通過(guò)回調(diào)接受到這個(gè)變化??梢杂^測(cè)的時(shí)間點(diǎn)有以下幾個(gè):

  • kCFRunLoopEntry,即將進(jìn)入Loop
  • kCFRunLoopBeforeTimers,即將處理 Timer
  • kCFRunLoopBeforeSources,即將處理 Source
  • kCFRunLoopBeforeWaiting,即將進(jìn)入休眠
  • kCFRunLoopAfterWaiting,剛從休眠中喚醒
  • kCFRunLoopExit,即將退出Loop
RunLoop事件循環(huán)1.png
1、CFRunLoopSourceRef (Source:事件源

是事件產(chǎn)生的地方。Source有兩個(gè)版本:Source0Source1。
? Source0:待處理源 只包含了一個(gè)回調(diào)(函數(shù)指針),它并不能主動(dòng)觸發(fā)事件。使用時(shí),你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個(gè) Source 標(biāo)記為待處理,然后手動(dòng)調(diào)用 CFRunLoopWakeUp(runloop) 來(lái)喚醒 RunLoop,讓其處理這個(gè)事件。
? Source1:?jiǎn)拘言?/code> 包含了一個(gè) mach_port 和一個(gè)回調(diào)(函數(shù)指針),被用于通過(guò)內(nèi)核和其他線程相互發(fā)送消息。這種 Source 能主動(dòng)喚醒 RunLoop 的線程,其原理在下面會(huì)講到。

2、CFRunLoopTimerRef (Timer:回調(diào)定時(shí)器

是基于時(shí)間的觸發(fā)器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一個(gè)時(shí)間長(zhǎng)度和一個(gè)回調(diào)(函數(shù)指針)。當(dāng)其加入到 RunLoop 時(shí),RunLoop會(huì)注冊(cè)對(duì)應(yīng)的時(shí)間點(diǎn),當(dāng)時(shí)間點(diǎn)到時(shí),RunLoop會(huì)被喚醒以執(zhí)行那個(gè)回調(diào)。

3、CFRunLoopObserverRef (Observer:觀察者

是觀察者,每個(gè) Observer 都包含了一個(gè)回調(diào)(函數(shù)指針),當(dāng) RunLoop 的狀態(tài)發(fā)生變化時(shí),觀察者就能通過(guò)回調(diào)接受到這個(gè)變化。可以觀測(cè)的時(shí)間點(diǎn)有以下幾個(gè):

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0),  即將進(jìn)入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1),  即將處理 Timer
    kCFRunLoopBeforeSources = (1UL << 2),  即將處理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5),  即將進(jìn)入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6),  剛從休眠中喚醒
    kCFRunLoopExit          = (1UL << 7),  即將退出Loop
};

上面的 Source/Timer/Observer 被統(tǒng)稱為 mode item,一個(gè) item 可以被同時(shí)加入多個(gè) mode。但一個(gè) item 被重復(fù)加入同一個(gè) mode 時(shí)是不會(huì)有效果的。如果一個(gè) mode 中一個(gè) item 都沒(méi)有,則 RunLoop 會(huì)直接退出,不進(jìn)入循環(huán)。


RunLoop 的 Mode

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

這里有個(gè)概念叫 “CommonModes”:一個(gè) Mode 可以將自己標(biāo)記為”Common”屬性(通過(guò)將其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每當(dāng) RunLoop 的內(nèi)容發(fā)生變化時(shí),RunLoop 都會(huì)自動(dòng)將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 標(biāo)記的所有Mode里。

應(yīng)用場(chǎng)景舉例:

主線程的 RunLoop 里有兩個(gè)預(yù)置的 Mode:kCFRunLoopDefaultModeUITrackingRunLoopMode。這兩個(gè) Mode 都已經(jīng)被標(biāo)記為”Common”屬性。DefaultMode 是 App 平時(shí)所處的狀態(tài),TrackingRunLoopMode 是追蹤 ScrollView 滑動(dòng)時(shí)的狀態(tài)。當(dāng)你創(chuàng)建一個(gè) Timer 并加到 DefaultMode 時(shí),Timer 會(huì)得到重復(fù)回調(diào),但此時(shí)滑動(dòng)一個(gè)TableView時(shí),RunLoop 會(huì)將 mode 切換為 TrackingRunLoopMode,這時(shí) Timer 就不會(huì)被回調(diào),并且也不會(huì)影響到滑動(dòng)操作。

即:滑動(dòng) ScrollView/TableView時(shí), DefaultMode(Timer回調(diào)) —> TrackingMode(Timer不回調(diào))

為了解決這個(gè)問(wèn)題:使用NSRunLoopCommonModes模式。
NSRunLoopCommonModes 是runloop中的另一種模式,其作用等價(jià)于NSDefaultRunLoopMode與UITrackingRunLoopMode的結(jié)合,滑動(dòng)scrollview的時(shí)候等價(jià)于UITrackingRunLoopMode,停止滑動(dòng)的時(shí)候等價(jià)于NSDefaultRunLoopMode。


RunLoop 的內(nèi)部邏輯

任何事件觸發(fā),都先獲取runloop,然后注冊(cè)一個(gè) Observer。
根據(jù)蘋(píng)果在文檔里的說(shuō)明,RunLoop 內(nèi)部的邏輯大致如下:

RunLoop 內(nèi)部的邏輯.png

實(shí)際上 RunLoop 就是這樣一個(gè)函數(shù),其內(nèi)部是一個(gè) do-while 循環(huán)。當(dāng)你調(diào)用 CFRunLoopRun() 時(shí),線程就會(huì)一直停留在這個(gè)循環(huán)里;直到超時(shí)或被手動(dòng)停止,該函數(shù)才會(huì)返回。


RunLoop 的底層實(shí)現(xiàn)

從上面代碼可以看到,RunLoop 的核心是基于 mach port 的,其進(jìn)入休眠時(shí)調(diào)用的函數(shù)是 mach_msg()。為了解釋這個(gè)邏輯,下面稍微介紹一下 OSX/iOS 的系統(tǒng)架構(gòu)。

RunLoop_系統(tǒng)架構(gòu).png

蘋(píng)果官方將整個(gè)系統(tǒng)大致劃分為上述4個(gè)層次:
1、應(yīng)用層:包括用戶能接觸到的圖形應(yīng)用,例如 Spotlight、Aqua、SpringBoard 等。
2、應(yīng)用框架層:即開(kāi)發(fā)人員接觸到的 Cocoa 等框架。
3、核心框架層:包括各種核心框架、OpenGL 等內(nèi)容。
4、Darwin:即操作系統(tǒng)的核心,包括系統(tǒng)內(nèi)核、驅(qū)動(dòng)、Shell 等內(nèi)容,這一層是開(kāi)源的,其所有源碼都可以在 opensource.apple.com 里找到。

Mach

Mach 本身提供的 API 非常有限,而且蘋(píng)果也不鼓勵(lì)使用 Mach 的 API,但是這些API非?;A(chǔ),如果沒(méi)有這些API的話,其他任何工作都無(wú)法實(shí)施。在 Mach 中,所有的東西都是通過(guò)自己的對(duì)象實(shí)現(xiàn)的,進(jìn)程、線程和虛擬內(nèi)存都被稱為“對(duì)象”。和其他架構(gòu)不同, Mach 的對(duì)象間不能直接調(diào)用,只能通過(guò)消息傳遞的方式實(shí)現(xiàn)對(duì)象間的通信。“消息”是 Mach 中最基礎(chǔ)的概念,消息在兩個(gè)端口 (port) 之間傳遞,這就是 Mach 的 IPC (進(jìn)程間通信) 的核心。

一條 Mach 消息實(shí)際上就是一個(gè)二進(jìn)制數(shù)據(jù)包 (BLOB),其頭部定義了當(dāng)前端口local_port 和目標(biāo)端口 remote_port
發(fā)送和接受消息是通過(guò)同一個(gè) API 進(jìn)行的,其 option 標(biāo)記了消息傳遞的方向:

mach_msg_return_t mach_msg(
            mach_msg_header_t *msg,
            mach_msg_option_t option,
            mach_msg_size_t send_size,
            mach_msg_size_t rcv_size,
            mach_port_name_t rcv_name,
            mach_msg_timeout_t timeout,
            mach_port_name_t notify);

為了實(shí)現(xiàn)消息的發(fā)送和接收,mach_msg() 函數(shù)實(shí)際上是調(diào)用了一個(gè) Mach 陷阱 (trap),即函數(shù)mach_msg_trap(),陷阱這個(gè)概念在 Mach 中等同于系統(tǒng)調(diào)用。當(dāng)你在用戶態(tài)調(diào)用 mach_msg_trap() 時(shí)會(huì)觸發(fā)陷阱機(jī)制,切換到內(nèi)核態(tài);內(nèi)核態(tài)中內(nèi)核實(shí)現(xiàn)的 mach_msg() 函數(shù)會(huì)完成實(shí)際的工作,如下圖:

mach_msg().png

RunLoop 的核心就是一個(gè) mach_msg() (見(jiàn)上面代碼的第7步),RunLoop 調(diào)用這個(gè)函數(shù)去接收消息,如果沒(méi)有別人發(fā)送 port 消息過(guò)來(lái),內(nèi)核會(huì)將線程置于等待狀態(tài)。例如你在模擬器里跑起一個(gè) iOS 的 App,然后在 App 靜止時(shí)點(diǎn)擊暫停,你會(huì)看到主線程調(diào)用棧是停留在 mach_msg_trap() 這個(gè)地方。

系統(tǒng)默認(rèn)注冊(cè)了5個(gè)Mode:
  1. kCFRunLoopDefaultMode: App的默認(rèn) Mode,通常主線程是在這個(gè) Mode 下運(yùn)行的。
  2. UITrackingRunLoopMode: 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他 Mode 影響。
  3. UIInitializationRunLoopMode: 在剛啟動(dòng) App 時(shí)第進(jìn)入的第一個(gè) Mode,啟動(dòng)完成后就不再使用。
  4. GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到。
  5. kCFRunLoopCommonModes: 這是一個(gè)占位的 Mode,沒(méi)有實(shí)際作用。


AutoreleasePool

App啟動(dòng)后,蘋(píng)果在主線程 RunLoop 里注冊(cè)了兩個(gè) Observer,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler()。

第一個(gè) Observer 監(jiān)視的事件是 Entry(即將進(jìn)入Loop),其回調(diào)內(nèi)會(huì)調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動(dòng)釋放池。其 order 是-2147483647,優(yōu)先級(jí)最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。

第二個(gè) Observer 監(jiān)視了兩個(gè)事件: BeforeWaiting(準(zhǔn)備進(jìn)入休眠) 時(shí)調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時(shí)調(diào)用 _objc_autoreleasePoolPop() 來(lái)釋放自動(dòng)釋放池。這個(gè) Observer 的 order 是 2147483647,優(yōu)先級(jí)最低,保證其釋放池子發(fā)生在其他所有回調(diào)之后。

在主線程執(zhí)行的代碼,通常是寫(xiě)在諸如事件回調(diào)、Timer回調(diào)內(nèi)的。這些回調(diào)會(huì)被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著,所以不會(huì)出現(xiàn)內(nèi)存泄漏,開(kāi)發(fā)者也不必顯示創(chuàng)建 Pool 了。


事件響應(yīng)

蘋(píng)果注冊(cè)了一個(gè) Source1 (基于 mach port 的) 用來(lái)接收系統(tǒng)事件,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()。

當(dāng)一個(gè)硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 IOKit.framework 生成一個(gè) IOHIDEvent 事件并由 SpringBoard 接收。這個(gè)過(guò)程的詳細(xì)情況可以參考這里。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸,加速,接近傳感器等幾種 Event,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進(jìn)程。隨后蘋(píng)果注冊(cè)的那個(gè) Source1 就會(huì)觸發(fā)回調(diào),并調(diào)用 _UIApplicationHandleEventQueue() 進(jìn)行應(yīng)用內(nèi)部的分發(fā)。

RunLoop是負(fù)責(zé)監(jiān)聽(tīng)事件:觸摸,時(shí)鐘,網(wǎng)絡(luò)等的,在主線程中創(chuàng)建NSURLConnection,RunLoop 可以被啟動(dòng)。在 「子線程」中 RunLoop是默認(rèn)不啟動(dòng)的,此時(shí)的網(wǎng)絡(luò)事件是不監(jiān)聽(tīng)的??!
所以,我們要啟動(dòng)運(yùn)行循環(huán):[[NSRunLoop currentRunLoop] run];。

注意,一般我們啟動(dòng)RunLoop的時(shí)候不要使用run,使用run來(lái)啟動(dòng)一旦啟動(dòng),就沒(méi)辦法被回收了。這里我們僅僅用來(lái)演示用。

此時(shí),我們算是解決了這個(gè)問(wèn)題。但是這個(gè)子線程是沒(méi)辦法被回收的,所以不能用run,可以需要手動(dòng)的方式來(lái)使runloop啟動(dòng)起來(lái)。當(dāng)然這種方式比較令人不爽。。。


手勢(shì)識(shí)別:注冊(cè)一個(gè) Observer 監(jiān)聽(tīng)

當(dāng)上面的 _UIApplicationHandleEventQueue() 識(shí)別了一個(gè)手勢(shì)時(shí),其首先會(huì)調(diào)用 Cancel 將當(dāng)前的 touchesBegin/Move/End 系列回調(diào)打斷。隨后系統(tǒng)將對(duì)應(yīng)的 UIGestureRecognizer 標(biāo)記為待處理。(即:打斷其他手勢(shì)回調(diào)-> 觸發(fā)手勢(shì) 標(biāo)記為待處理

蘋(píng)果注冊(cè)了一個(gè) Observer 監(jiān)測(cè) BeforeWaiting (Loop即將進(jìn)入休眠) 事件,這個(gè)Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver(),其內(nèi)部會(huì)獲取所有剛被標(biāo)記為待處理的 GestureRecognizer,并執(zhí)行GestureRecognizer的回調(diào)。


界面更新:注冊(cè)一個(gè) Observer 監(jiān)聽(tīng):即將進(jìn)入休眠、即將退出Loop

當(dāng)在操作 UI 時(shí),比如改變了 Frame、更新了 UIView/CALayer 的層次時(shí),或者手動(dòng)調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個(gè) UIView/CALayer 就被標(biāo)記為待處理,并被提交到一個(gè)全局的容器去。

蘋(píng)果注冊(cè)了一個(gè) Observer 監(jiān)聽(tīng) BeforeWaiting(即將進(jìn)入休眠) 和 Exit (即將退出Loop) 事件,回調(diào)去執(zhí)行一個(gè)很長(zhǎng)的函數(shù):
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個(gè)函數(shù)里會(huì)遍歷所有待處理的 UIView/CAlayer 以執(zhí)行實(shí)際的繪制和調(diào)整,并更新 UI 界面。


定時(shí)器:

NSTimer

其實(shí)就是 CFRunLoopTimerRef,他們之間是 toll-free bridged 的。一個(gè) NSTimer 注冊(cè)到 RunLoop 后,RunLoop 會(huì)為其重復(fù)的時(shí)間點(diǎn)注冊(cè)好事件。例如 10:00, 10:10, 10:20 這幾個(gè)時(shí)間點(diǎn)。RunLoop為了節(jié)省資源,并不會(huì)在非常準(zhǔn)確的時(shí)間點(diǎn)回調(diào)這個(gè)Timer。Timer 有個(gè)屬性叫做 Tolerance (寬容度),標(biāo)示了當(dāng)時(shí)間點(diǎn)到后,容許有多少最大誤差。

CADisplayLink

是一個(gè)與屏幕刷新率同步的定時(shí)器,適合用于 UI 動(dòng)畫(huà)或需要與屏幕刷新同步的任務(wù)。(但實(shí)際實(shí)現(xiàn)原理更復(fù)雜,和 NSTimer 并不一樣,其內(nèi)部實(shí)際是操作了一個(gè) Source)。如果在兩次屏幕刷新之間執(zhí)行了一個(gè)長(zhǎng)任務(wù),那其中就會(huì)有一幀被跳過(guò)去(和 NSTimer 相似),造成界面卡頓的感覺(jué)。在快速滑動(dòng)TableView時(shí),即使一幀的卡頓也會(huì)讓用戶有所察覺(jué)。Facebook 開(kāi)源的 AsyncDisplayLink 就是為了解決界面卡頓的問(wèn)題,其內(nèi)部也用到了 RunLoop。

定時(shí)器類型 精度 適用場(chǎng)景
CADisplayLink 高(60 FPS) UI 動(dòng)畫(huà)、屏幕刷新同步任務(wù)
NSTimer 一般性定時(shí)任務(wù)
DispatchSourceTimer 后臺(tái)任務(wù)、高精度定時(shí)任務(wù)
NSDate + CFRunLoop 最高 需要極高精度的任務(wù)(如音視頻同步)
MTKView / CVDisplayLink 圖形渲染任務(wù)


TableViewCell添加計(jì)時(shí)器

最近項(xiàng)目中需要實(shí)現(xiàn)一個(gè)需求,需要在一個(gè)訂單列表中給訂單添加一個(gè)時(shí)間倒計(jì)時(shí),不是每一個(gè)都顯示,有的話顯示,沒(méi)有的話不顯示。
看到上面的需求,首先想到的是給需要展示的cell添加定時(shí)器,給不需要的cell不添加定時(shí)器,這樣會(huì)出現(xiàn)一些問(wèn)題。

問(wèn)題:
1.1當(dāng)cell復(fù)用,定時(shí)器的開(kāi)啟和銷毀怎么處理?定時(shí)器初值怎么處理?
1.2如果我們?cè)赾ell展示的時(shí)候開(kāi)啟定時(shí)器,那么對(duì)于沒(méi)有展示的cell(定時(shí)器也沒(méi)有開(kāi)啟),怎么保證前臺(tái)倒計(jì)時(shí)和后臺(tái)計(jì)時(shí)同步?
1.3當(dāng)列表中開(kāi)辟多個(gè)計(jì)時(shí)器,性能消耗很嚴(yán)重,這些怎么優(yōu)化?
1.4當(dāng)我們的列表有上拉加載下拉刷新操作的時(shí)候,定時(shí)器的銷毀開(kāi)啟怎么處理?
……
等等這些問(wèn)題,我們?cè)撛趺刺幚恚?br> 其實(shí)上面有些問(wèn)題,如果不存在復(fù)用的情況下是可以解決的,但1.3的問(wèn)題會(huì)很嚴(yán)重,會(huì)出現(xiàn)頁(yè)面卡頓,那么要解決卡頓問(wèn)題,很明顯我們需要減少定時(shí)器創(chuàng)建的數(shù)量,那么減少到多少個(gè)還能完成功能呢?(越少也好,一個(gè)足最好),經(jīng)過(guò)思考決定就只用一個(gè)定時(shí)器來(lái)實(shí)現(xiàn),那么怎么實(shí)現(xiàn)呢,看下面:

//使用viewModel + RAC屬性監(jiān)聽(tīng)
轉(zhuǎn)自:TableViewCell添加計(jì)時(shí)器功能,只需創(chuàng)建一個(gè)定時(shí)器


PerformSelecter

當(dāng)調(diào)用 NSObject 的 performSelecter: afterDelay:后,實(shí)際上其內(nèi)部會(huì)創(chuàng)建一個(gè) Timer 并添加到當(dāng)前線程的 RunLoop 中。所以如果當(dāng)前線程沒(méi)有 RunLoop,則這個(gè)方法會(huì)失效。

當(dāng)調(diào)用 performSelector: onThread:時(shí),實(shí)際上其會(huì)創(chuàng)建一個(gè) Timer 加到對(duì)應(yīng)的線程去,同樣的,如果對(duì)應(yīng)線程沒(méi)有 RunLoop 該方法也會(huì)失效。


關(guān)于GCD

實(shí)際上 RunLoop 底層也會(huì)用到 GCD 的東西,但同時(shí) GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。

當(dāng)調(diào)用 dispatch_async(dispatch_get_main_queue(), block) 時(shí),libDispatch 會(huì)向主線程的 RunLoop 發(fā)送消息,RunLoop會(huì)被喚醒,并從消息中取得這個(gè) block,并在回調(diào) CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE()
cfrunloop_is_servicing_the_main_dispatch_queue()
里執(zhí)行這個(gè) block。但這個(gè)邏輯僅限于 dispatch 到主線程,dispatch 到其他線程仍然是由 libDispatch 處理的。


關(guān)于網(wǎng)絡(luò)請(qǐng)求

iOS 中,關(guān)于網(wǎng)絡(luò)請(qǐng)求的接口自下至上有如下幾層:

CFSocket
CFNetwork       -> ASIHttpRequest
NSURLConnection -> AFNetworking
NSURLSession    -> AFNetworking2, Alamofire
  • CFSocket 是最底層的接口,只負(fù)責(zé) socket 通信。

  • CFNetwork 是基于 CFSocket 等接口的上層封裝,ASIHttpRequest 工作于這一層。

  • NSURLConnection 是基于 CFNetwork 的更高層的封裝,提供面向?qū)ο蟮慕涌?,AFNetworking 工作于這一層。(NSURLConnection會(huì)阻塞主線程,在iOS8后被棄用)

引用:iOS進(jìn)階_NSURLConnection被棄用的原因,Connection的缺點(diǎn)
1、沒(méi)有下載進(jìn)度,會(huì)影響用戶體驗(yàn)(計(jì)算總值和下載數(shù)據(jù))
2、內(nèi)存偏高,有一個(gè)最大的峰值
? ? 1.保存完成一次性寫(xiě)入磁盤(pán)
? ? 2.邊下載邊寫(xiě)入
3、會(huì)阻塞主線程

  • NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection并列的,但底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程),AFNetworking2 和 Alamofire 工作于這一層。


AsyncDisplayKit:(ASDK:異步渲染框架)

AsyncDisplayKit 是 Facebook 推出的用于保持界面流暢性的框架,其原理大致如下:

UI 線程中一旦出現(xiàn)繁重的任務(wù)就會(huì)導(dǎo)致界面卡頓,這類任務(wù)通常分為3類:排版、繪制、UI對(duì)象操作。

其中前兩類操作可以通過(guò)各種方法扔到后臺(tái)線程執(zhí)行,而最后一類操作只能在主線程完成,并且有時(shí)后面的操作需要依賴前面操作的結(jié)果 (例如TextView創(chuàng)建時(shí)可能需要提前計(jì)算出文本的大?。?。ASDK 所做的,就是盡量將能放入后臺(tái)的任務(wù)放入后臺(tái),不能的則盡量推遲 (例如視圖的創(chuàng)建、屬性的調(diào)整)。

為此,ASDK 創(chuàng)建了一個(gè)名為 ASDisplayNode 的對(duì)象。例如 frame、backgroundColor等,所有這些屬性都可以在后臺(tái)線程更改。

ASDK 仿照 QuartzCore/UIKit 框架的模式,實(shí)現(xiàn)了一套類似的界面更新的機(jī)制:即在主線程的 RunLoop 中添加一個(gè) Observer,監(jiān)聽(tīng)以下兩個(gè)事件:

  • kCFRunLoopBeforeWaiting:(即將進(jìn)入休眠)
  • kCFRunLoopExit:(即將退出Loop)
    在收到回調(diào)時(shí),遍歷所有之前放入隊(duì)列的待處理的任務(wù),然后一一執(zhí)行。

引用1:NSRunLoopCommonModes和NSDefaultRunLoopMode區(qū)別(Timer)
NSRunLoopCommonModes,這個(gè)模式等效于NSDefaultRunLoopMode和NSEventTrackingRunLoopMode的結(jié)合。

引用2:iOS深入理解定時(shí)器
NSTimer和CADisplayLink只會(huì)在創(chuàng)建timer的線程接收到時(shí)鐘回調(diào),線程在沒(méi)有任務(wù)的時(shí)候 10s 左右會(huì)被操作系統(tǒng)回收,注冊(cè)了NSTimer和CADisplayLink定時(shí)任務(wù)的線程,直到定時(shí)器銷毀后10秒左右才會(huì)被回收。



iOS全解14:事件的傳遞和響應(yīng)機(jī)制


引用:

CFRunLoopRef 的代碼是開(kāi)源的,你可以在這里 CoreFoundation 下載到整個(gè)源碼來(lái)查看。

(Update: Swift 開(kāi)源后,蘋(píng)果又維護(hù)了一個(gè)跨平臺(tái)的 CoreFoundation 版本,這個(gè)版本的源碼可能和現(xiàn)有 iOS 系統(tǒng)中的實(shí)現(xiàn)略不一樣,但更容易編譯,而且已經(jīng)適配了 Linux/Windows。)

參考:
深入理解RunLoop
runloop 的 mode 作用是什么?

iOS事件傳遞與響應(yīng)鏈
1、iOS事件響應(yīng)鏈&傳遞鏈
2、 史上最詳細(xì)的iOS事件的傳遞和響機(jī)制-原理篇



最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 本文源連接??點(diǎn)擊即可跳轉(zhuǎn)到本文源鏈接 推薦視頻:RunLoop 1.RunLoop的概念 一般一個(gè)線程一次只能執(zhí)行...
    veraTuku閱讀 881評(píng)論 0 51
  • 轉(zhuǎn)自http://blog.ibireme.com/2015/05/18/runloop[http://blog....
    Leoeoo閱讀 319評(píng)論 0 1
  • RunLoop 是 iOS 和 OSX 開(kāi)發(fā)中非?;A(chǔ)的一個(gè)概念,這篇文章將從 CFRunLoop 的源碼入手,介...
    DeadRabbit閱讀 460評(píng)論 0 4
  • RunLoop 是 iOS 和 OSX 開(kāi)發(fā)中非?;A(chǔ)的一個(gè)概念,這篇文章將從 CFRunLoop 的源碼入手,介...
    柳大官人閱讀 419評(píng)論 0 3
  • 前言 RunLoop是iOS和OSX開(kāi)發(fā)中非?;A(chǔ)的一個(gè)概念,這篇文章將從CFRunLoop的源碼入手,介紹Run...
    暮年古稀ZC閱讀 2,409評(píng)論 1 19

友情鏈接更多精彩內(nèi)容