iOS全解2:Runloop

面試系列:



Runloop官方文檔
Runloop官方課程講解

RunLoop概念

自我理解:RunLoop 就是線程資源管理器绊率。之后會(huì)介紹一下在 iOS 中憔足,蘋(píng)果是如何利用 RunLoop 實(shí)現(xiàn)一下功能:

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

一般來(lái)講,一個(gè)線程一次只能執(zhí)行一個(gè)任務(wù)齐饮,執(zhí)行完成后線程就會(huì)退出(銷毀)咳蔚。如果我們需要一個(gè)機(jī)制,讓線程能隨時(shí)處理事件但并不退出,通常的代碼邏輯是這樣的:

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

這種模型通常被稱作 Event Loop拥娄。 Event Loop 在很多系統(tǒng)和框架里都有實(shí)現(xiàn),比如 Node.js 的事件處理瞳筏,比如 Windows 程序的消息循環(huán)稚瘾,再比如 OSX/iOS 里的 RunLoop。實(shí)現(xiàn)這種模型的關(guān)鍵點(diǎn)在于:如何管理事件/消息姚炕,如何讓線程在沒(méi)有處理消息時(shí)休眠摊欠,以避免資源占用丢烘、在有消息到來(lái)時(shí)立刻被喚醒。

所以些椒,RunLoop 實(shí)際上就是一個(gè)對(duì)象播瞳,這個(gè)對(duì)象管理了其需要處理的事件和消息,并提供了一個(gè)入口函數(shù)來(lái)執(zhí)行上面 Event Loop 的邏輯免糕。線程執(zhí)行了這個(gè)函數(shù)后赢乓,就會(huì)一直處于這個(gè)函數(shù)內(nèi)部 “接受消息->等待->處理”的循環(huán)中,直到這個(gè)循環(huán)結(jié)束(比如傳入 quit 的消息)说墨,函數(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非沉墼桑基礎(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)的N淌凇拣播!
所以,我們要啟動(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

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í)器(但實(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。


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() 里執(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ū)ο蟮慕涌诤藓担珹FNetworking 工作于這一層辈毯。(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)系作者
  • 序言:七十年代末镐牺,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子魁莉,更是在濱河造成了極大的恐慌睬涧,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,817評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件旗唁,死亡現(xiàn)場(chǎng)離奇詭異畦浓,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)检疫,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門讶请,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人屎媳,你說(shuō)我怎么就攤上這事夺溢。” “怎么了烛谊?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,354評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵风响,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我丹禀,道長(zhǎng)状勤,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,498評(píng)論 1 284
  • 正文 為了忘掉前任双泪,我火速辦了婚禮持搜,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘焙矛。我一直安慰自己葫盼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布村斟。 她就那樣靜靜地躺著贫导,像睡著了一般。 火紅的嫁衣襯著肌膚如雪邓梅。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 49,829評(píng)論 1 290
  • 那天邑滨,我揣著相機(jī)與錄音日缨,去河邊找鬼。 笑死掖看,一個(gè)胖子當(dāng)著我的面吹牛匣距,可吹牛的內(nèi)容都是我干的面哥。 我是一名探鬼主播,決...
    沈念sama閱讀 38,979評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼毅待,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼尚卫!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起尸红,我...
    開(kāi)封第一講書(shū)人閱讀 37,722評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤吱涉,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后外里,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體怎爵,經(jīng)...
    沈念sama閱讀 44,189評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評(píng)論 2 327
  • 正文 我和宋清朗相戀三年盅蝗,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鳖链。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,654評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡墩莫,死狀恐怖芙委,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情狂秦,我是刑警寧澤灌侣,帶...
    沈念sama閱讀 34,329評(píng)論 4 330
  • 正文 年R本政府宣布,位于F島的核電站故痊,受9級(jí)特大地震影響顶瞳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜愕秫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評(píng)論 3 313
  • 文/蒙蒙 一慨菱、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧戴甩,春花似錦符喝、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,762評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至缴川,卻和暖如春茉稠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背把夸。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,993評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工而线, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,382評(píng)論 2 360
  • 正文 我出身青樓膀篮,卻偏偏與公主長(zhǎng)得像嘹狞,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子誓竿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評(píng)論 2 349

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

  • 本文源連接??點(diǎn)擊即可跳轉(zhuǎn)到本文源鏈接 推薦視頻:RunLoop 1.RunLoop的概念 一般一個(gè)線程一次只能執(zhí)行...
    veraTuku閱讀 830評(píng)論 0 51
  • 轉(zhuǎn)自http://blog.ibireme.com/2015/05/18/runloop[http://blog....
    Leoeoo閱讀 249評(píng)論 0 1
  • RunLoop 是 iOS 和 OSX 開(kāi)發(fā)中非嘲跬基礎(chǔ)的一個(gè)概念,這篇文章將從 CFRunLoop 的源碼入手筷屡,介...
    DeadRabbit閱讀 434評(píng)論 0 4
  • RunLoop 是 iOS 和 OSX 開(kāi)發(fā)中非辰担基礎(chǔ)的一個(gè)概念,這篇文章將從 CFRunLoop 的源碼入手速蕊,介...
    柳大官人閱讀 394評(píng)論 0 3
  • 前言 RunLoop是iOS和OSX開(kāi)發(fā)中非成┍基礎(chǔ)的一個(gè)概念,這篇文章將從CFRunLoop的源碼入手规哲,介紹Run...
    暮年古稀ZC閱讀 2,235評(píng)論 1 19