面試系列:
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_t
和 NSThread
琼讽。過(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)資源兼吓。
一個(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 之間是一一對(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
1庸毫、CFRunLoopSourceRef (Source:事件源
)
是事件產(chǎn)生的地方仔拟。Source有兩個(gè)版本:Source0
和 Source1
。
? 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:kCFRunLoopDefaultMode
和 UITrackingRunLoopMode
。這兩個(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)部的邏輯大致如下:
實(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)尖昏。
蘋(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í)際的工作反番,如下圖:
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:
-
kCFRunLoopDefaultMode
: App的默認(rèn) Mode,通常主線程是在這個(gè) Mode 下運(yùn)行的息楔。 -
UITrackingRunLoopMode
: 界面跟蹤 Mode寝贡,用于 ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他 Mode 影響值依。 -
UIInitializationRunLoopMode
: 在剛啟動(dòng) App 時(shí)第進(jìn)入的第一個(gè) Mode圃泡,啟動(dòng)完成后就不再使用。 -
GSEventReceiveRunLoopMode
: 接受系統(tǒng)事件的內(nèi)部 Mode愿险,通常用不到颇蜡。 -
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ì)被回收键俱。
引用:
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ī)制-原理篇