概述
RunLoop作為iOS中一個基礎(chǔ)組件和線程有著千絲萬縷的關(guān)系,同時也是很多常見技術(shù)的幕后功臣爆存。盡管在平時多數(shù)開發(fā)者很少直接使用RunLoop株扛,但是理解RunLoop可以幫助開發(fā)者更好的利用多線程編程模型,同時也可以幫助開發(fā)者解答日常開發(fā)中的一些疑惑诉探。本文將從RunLoop源碼著手日熬,結(jié)合RunLoop的實際應(yīng)用來逐步解開它的神秘面紗。
開源的RunloopRef
通常所說的RunLoop指的是NSRunloop或者CFRunloopRef肾胯,CFRunloopRef是純C的函數(shù)碍遍,而NSRunloop僅僅是CFRunloopRef的OC封裝,并未提供額外的其他功能阳液,因此下面主要分析CFRunloopRef怕敬,蘋果已經(jīng)開源了CoreFoundation源代碼,因此很容易找到CFRunloop源代碼帘皿。
從代碼可以看出CFRunloopRef其實就是__CFRunloop這個結(jié)構(gòu)體指針(按照OC的思路我們可以將RunLoop看成一個對象)东跪,這個對象的運行才是我們通常意義上說的運行循環(huán),核心方法是__CFRunloopRun()鹰溜,為了便于閱讀就不再直接貼源代碼虽填,放一段偽代碼方便大家閱讀:
int32_t __CFRunLoopRun()
{
// 通知即將進入runloop
__CFRunLoopDoObservers(KCFRunLoopEntry);
do
{
// 通知將要處理timer和source
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
// 處理非延遲的主線程調(diào)用
__CFRunLoopDoBlocks();
// 處理Source0事件
__CFRunLoopDoSource0();
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks();
}
/// 如果有 Source1 (基于port) 處于 ready 狀態(tài),直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息曹动。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort();
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)斋日。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
// GCD dispatch main queue
CheckIfExistMessagesInMainDispatchQueue();
// 即將進入休眠
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
// 等待內(nèi)核mach_msg事件
mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
// 等待。墓陈。恶守。
// 從等待中醒來
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// 處理因timer的喚醒
if (wakeUpPort == timerPort)
__CFRunLoopDoTimers();
// 處理異步方法喚醒,如dispatch_async
else if (wakeUpPort == mainDispatchQueuePort)
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
// 處理Source1
else
__CFRunLoopDoSource1();
// 再次確保是否有同步的方法需要調(diào)用
__CFRunLoopDoBlocks();
} while (!stop && !timeout);
// 通知即將退出runloop
__CFRunLoopDoObservers(CFRunLoopExit);
}
一、簡介
RunLoop是一個對象贡必,這個對象在循環(huán)中用來處理程序運行過程中出現(xiàn)的各種事件(比如說觸摸事件兔港、UI刷新事件、定時器事件仔拟、Selector事件)衫樊,從而保持程序的持續(xù)運行;而且在沒有事件處理的時候,會進入睡眠模式科侈,從而節(jié)省CPU資源载佳,提高程序性能。
RunLoop的代碼邏輯:
詳細解釋請看這里
// 用DefaultMode啟動
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
- 這種模型通常被稱作 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ù)返回侮繁。
二、RunLoop的深入分析
1. 從程序入口main
函數(shù)開始
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
程序主線程一開始如孝,就會一直跑宪哩,那么猜想其內(nèi)部一定是開啟了一個和主線程對應(yīng)的
RunLoop
并且可以看出函數(shù)返回的是一個int
返回值的UIApplicationMain()
函數(shù)
2. 我們繼續(xù)深入UIApplicationMain
函數(shù)
UIKIT_EXTERN int UIApplicationMain
(int argc,
char *argv[],
NSString * __nullable principalClassName,
NSString * __nullable delegateClassName
);
我們發(fā)現(xiàn)它返回的是一個int類型的值,那么我們對main函數(shù)做一些修改:
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"開始");
int re = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"結(jié)束");
return re;
}
}
運行程序第晰,我們發(fā)現(xiàn)只會打印開始
锁孟,并不會打印結(jié)束
,這再次說明在UIApplicationMain
函數(shù)中茁瘦,開啟了一個和主線程相關(guān)的RunLoop
品抽,導致UIApplicationMain
不會返回,一直在運行中甜熔,也就保證了程序的持續(xù)運行圆恤。
3. 繼續(xù)學習CFRunLoopRef
RunLoop對象包括Fundation中的NSRunLoop對象和CoreFoundation中的CFRunLoopRef對象。因為Fundation框架是基于CoreFoundation的封裝腔稀,因此我們學習RunLoop還是要研究CFRunLoopRef 源碼盆昙。
獲取RunLoop對象
//Foundation
[NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
//Core Foundation
CFRunLoopGetCurrent(); // 獲得當前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象
1. 主線程獲取CFRunLoopRef源碼
// 創(chuàng)建字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 創(chuàng)建主線程 根據(jù)傳入的主線程創(chuàng)建主線程對應(yīng)的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主線程 將主線程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
2. 創(chuàng)建與子線程相關(guān)聯(lián)的CFRunLoopRe源碼
蘋果不允許直接創(chuàng)建 RunLoop,它只提供了兩個自動獲取的函數(shù):CFRunLoopGetMain() 和 CFRunLoopGetCurrent()烧颖。
// 全局的Dictionary弱左,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
// 獲取一個 pthread 對應(yīng)的 RunLoop炕淮。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次進入時,初始化全局Dic跳夭,并先為主線程創(chuàng)建一個 RunLoop涂圆。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
// 直接從 Dictionary 里獲取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
// 取不到時,創(chuàng)建一個
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
// 注冊一個回調(diào)绣檬,當線程銷毀時渠鸽,順便也銷毀其對應(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());
}
3. CFRunloopRef與線程之間的關(guān)系
- 首先踩衩,iOS 開發(fā)中能遇到兩個線程對象: pthread_t 和 NSThread嚼鹉。過去蘋果有份文檔標明了 NSThread 只是 pthread_t 的封裝,但那份文檔已經(jīng)失效了驱富,現(xiàn)在它們也有可能都是直接包裝自最底層的 mach thread锚赤。蘋果并沒有提供這兩個對象相互轉(zhuǎn)換的接口,但不管怎么樣褐鸥,可以肯定的是 pthread_t 和 NSThread 是一一對應(yīng)的线脚。比如,你可以通過 pthread_main_np() 或 [NSThread mainThread] 來獲取主線程叫榕;也可以通過 pthread_self() 或 [NSThread currentThread] 來獲取當前線程浑侥。CFRunLoop 是基于 pthread 來管理的。
- 從
CFRunLoopRef
源碼可以看出晰绎,線程和 RunLoop 之間是一一對應(yīng)的寓落,其關(guān)系是保存在一個全局的 Dictionary 里。線程剛創(chuàng)建時并沒有 RunLoop荞下,如果你不主動獲取伶选,那它一直都不會有。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時锄弱,RunLoop 的銷毀是發(fā)生在線程結(jié)束時考蕾。你只能在一個線程的內(nèi)部獲取其 RunLoop(主線程除外)。
[NSRunLoop currentRunLoop];方法調(diào)用時会宪,會先看一下字典里有沒有存子線程相對用的RunLoop肖卧,如果有則直接返回RunLoop,如果沒有則會創(chuàng)建一個掸鹅,并將與之對應(yīng)的子線程存入字典中塞帐。
. 總結(jié)來說. CFRunloopRef與線程之間的關(guān)系
- 線程在處理完自己的任務(wù)后一般會退出,為了實現(xiàn)線程不退出能夠隨時處理任務(wù)的機制被稱為EventLoop巍沙,node.js 的事件處理葵姥,windows程序的消息循環(huán),iOS句携、OSX的RunLoop都是這種機制榔幸。
- 線程和RunLoop是一一對應(yīng)的,關(guān)系保存在全局的字典里。
在主線程中削咆,程序啟動時牍疏,系統(tǒng)默認添加了有kCFRunLoopDefaultMode 和 UITrackingRunLoopMode兩個預(yù)置Mode的RunLoop,保證程序處于等待狀態(tài)拨齐,如果接收到來自觸摸事件等鳞陨,就會執(zhí)行任務(wù),否則處于休眠中瞻惋。 - 線程創(chuàng)建時并沒有RunLoop厦滤,(主線程除外),RunLoop不能創(chuàng)建歼狼,只能主動獲取才會有掏导。RunLoop的創(chuàng)建是在第一次獲取時,RunLoop的銷毀是發(fā)生在線程結(jié)束時蹂匹。只能在一個線程中獲取自己和主線程的RunLoop碘菜。
Core Foundation中關(guān)于RunLoop的5個類
CFRunLoopRef //獲得當前RunLoop和主RunLoop
CFRunLoopModeRef //運行模式,只能選擇一種限寞,在不同模式中做不同的操作
CFRunLoopSourceRef //事件源忍啸,輸入源
CFRunLoopTimerRef //定時器時間
CFRunLoopObserverRef //觀察者
** CFRunLoopModeRef**
詳細內(nèi)容請點擊這里
1.簡介:
每個CFRunLoopRef 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer履植。每次調(diào)用 RunLoop 的主函數(shù)時计雌,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode玫霎。如果需要切換 Mode凿滤,只能退出 Loop,再重新指定一個 Mode 進入庶近。這樣做主要是為了分隔開不同組的 Source/Timer/Observer翁脆,讓其互不影響。
CFRunLoopModeRef
類并沒有對外暴露鼻种,只是通過 CFRunLoopRef 的接口進行了封裝反番。他們的關(guān)系如下:
CFRunLoopRef獲取Mode的接口:
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
2.CFRunLoopMode的類型
- kCFRunLoopDefaultMode
App的默認Mode,通常主線程是在這個Mode下運行
- UITrackingRunLoopMode:
界面跟蹤 Mode叉钥,用于 ScrollView 追蹤觸摸滑動罢缸,保證界面滑動時不受其他 Mode 影響
- UIInitializationRunLoopMode:
在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用
- GSEventReceiveRunLoopMode:
接受系統(tǒng)事件的內(nèi)部 Mode投队,通常用不到
- kCFRunLoopCommonModes:
這是一個占位用的Mode枫疆,作為標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一種真正的Mode
3.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
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
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 的線程.
** CFRunLoopObserverRef**
CFRunLoopObserverRef是觀察者,能夠監(jiān)聽RunLoop的狀態(tài)改變熔任。
我們直接來看代碼褒链,給RunLoop添加監(jiān)聽者,監(jiān)聽其運行狀態(tài):
//創(chuàng)建監(jiān)聽者
/*
第一個參數(shù) CFAllocatorRef allocator:分配存儲空間 CFAllocatorGetDefault()默認分配
第二個參數(shù) CFOptionFlags activities:要監(jiān)聽的狀態(tài) kCFRunLoopAllActivities 監(jiān)聽所有狀態(tài)
第三個參數(shù) Boolean repeats:YES:持續(xù)監(jiān)聽 NO:不持續(xù)
第四個參數(shù) CFIndex order:優(yōu)先級疑苔,一般填0即可
第五個參數(shù) :回調(diào) 兩個參數(shù)observer:監(jiān)聽者 activity:監(jiān)聽的事件
*/
/*
所有事件
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6),// 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7),// 即將退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop進入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要處理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要處理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒來了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
// 給RunLoop添加監(jiān)聽者
/*
第一個參數(shù) CFRunLoopRef rl:要監(jiān)聽哪個RunLoop,這里監(jiān)聽的是主線程的RunLoop
第二個參數(shù) CFRunLoopObserverRef observer 監(jiān)聽者
第三個參數(shù) CFStringRef mode 要監(jiān)聽RunLoop在哪種運行模式下的狀態(tài)
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
/*
CF的內(nèi)存管理(Core Foundation)
凡是帶有Create甫匹、Copy、Retain等字眼的函數(shù)惦费,創(chuàng)建出來的對象兵迅,都需要在最后做一次release
GCD本來在iOS6.0之前也是需要我們釋放的,6.0之后GCD已經(jīng)納入到了ARC中薪贫,所以我們不需要管了
*/
CFRelease(observer);
七恍箭、RunLoop 可能的面試套路
什么是RunLoop?
從字面上看:運行循環(huán)、跑圈
其實它內(nèi)部就是do-while循環(huán),在這個循環(huán)內(nèi)部不斷的處理各種任務(wù)(比如Source瞧省、Timer扯夭、Observer)
一個線程對應(yīng)一個RunLoop,主線程的RunLoop默認已經(jīng)啟動,子線程的RunLoop需要手動啟動(調(diào)用run方法)
RunLoop只能選擇一個Mode啟動,如果當前Mode中沒有任何Soure、Timer鞍匾、Observer,那么就直接退出RunLoop
在開發(fā)中如何使用RunLoop?什么應(yīng)用場景?
開啟一個常駐線程(讓一個子線程不進入消亡狀態(tài),等待其他線程發(fā)來消息,處理其他事件)
在子線程中開啟一個定時器
在子線程中進行一些長期監(jiān)控
可以控制定時器在特定模式下執(zhí)行
可以讓某些事件(行為交洗、任務(wù))在特定模式下執(zhí)行
可以添加Observer監(jiān)聽RunLoop的狀態(tài),比如監(jiān)聽點擊事件的處理(在所有點擊事件之前做一些事情)
所有參考鏈接:
1、ibireme
2橡淑、百度大神
3构拳、視頻-runloop線下分享
5梳码、官方文檔
6隐圾、CFRunLoop.c
什么是RunLoop?
RunLoop是一個接收處理異步消息事件的循環(huán),一個循環(huán)中:等待事件發(fā)生掰茶,然后將這個事件送到能處理它的地方暇藏。
RunLoop實際上是一個對象,這個對象在循環(huán)中用來處理程序運行過程中出現(xiàn)的各種事件(比如說觸摸事件、UI刷新事件濒蒋、定時器事件盐碱、Selector事件)和消息,從而保持程序的持續(xù)運行;而且在沒有事件處理的時候,會進入睡眠模式,從而節(jié)省CPU資源,提高程序性能把兔。
Event Loop模型偽代碼
int main(int argc, char * argv[]) {
//程序一直運行狀態(tài)
while (AppIsRunning) {
//睡眠狀態(tài),等待喚醒事件
id whoWakesMe = SleepForWakingU p();
//得到喚醒事件
id event = GetEvent(whoWakesMe);
//開始處理事件
HandleEvent(event);
}
return 0;
}
- mach kernel屬于蘋果內(nèi)核瓮顽,RunLoop依靠它實現(xiàn)了休眠和喚醒而避免了CPU的空轉(zhuǎn)县好。
- Runloop是基于pthread進行管理的,pthread是基于c的跨平臺多線程操作底層API暖混。它是mach thread的上層封裝(可以參見Kernel Programming Guide)缕贡,和NSThread一一對應(yīng)(而NSThread是一套面向?qū)ο蟮腁PI,所以在iOS開發(fā)中我們也幾乎不用直接使用pthread)拣播。
RunLoop的組成
RunLoop構(gòu)成
CFRunLoop對象可以檢測某個task或者dispatch的輸入事件晾咪,當檢測到有輸入源事件,CFRunLoop將會將其加入到線程中進行處理贮配。比方說用戶輸入事件谍倦、網(wǎng)絡(luò)連接事件、周期性或者延時事件泪勒、異步的回調(diào)等昼蛀。
RunLoop可以檢測的事件類型一共有3種,分別是CFRunLoopSource圆存、CFRunLoopTimer叼旋、CFRunLoopObserver×删纾可以通過CFRunLoopAddSource, CFRunLoopAddTimer或者CFRunLoopAddObserver添加相應(yīng)的事件類型送淆。
要讓一個RunLoop跑起來還需要run loop modes,每一個source, timer和observer添加到RunLoop中時必須要與一個模式(CFRunLoopMode)相關(guān)聯(lián)才可以運行怕轿。
上面是對于CFRunLoop官方文檔的解釋
RunLoop的主要組成
RunLoop共包含5個類偷崩,但公開的只有Source、Timer撞羽、Observer相關(guān)的三個類阐斜。
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
CFRunLoopSourceRef
source是RunLoop的數(shù)據(jù)源(輸入源)的抽象類(protocol),Source有兩個版本:Source0 和 Source1
- source0:只包含了一個回調(diào)(函數(shù)指針),使用時诀紊,你需要先調(diào)用 CFRunLoopSourceSignal(source)谒出,將這個 Source 標記為待處理,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop邻奠,讓其處理這個事件笤喳。處理App內(nèi)部事件,App自己負責管理(出發(fā)),如UIEvent(Touch事件等,GS發(fā)起到RunLoop運行再到事件回調(diào)到UI)碌宴、CFSocketRef杀狡。
- Source1:由RunLoop和內(nèi)核管理,由mach_port驅(qū)動(特指port-based事件)贰镣,如CFMachPort呜象、CFMessagePort膳凝、NSSocketPort。特別要注意一下Mach port的概念恭陡,它是一個輕量級的進程間通訊的方式蹬音,可以理解為它是一個通訊通道,假如同時有幾個進程都掛在這個通道上休玩,那么其它進程向這個通道發(fā)送消息后著淆,這些掛在這個通道上的進程都可以收到相應(yīng)的消息。這個Port的概念非常重要哥捕,因為它是RunLoop休眠和被喚醒的關(guān)鍵牧抽,它是RunLoop與系統(tǒng)內(nèi)核進行消息通訊的窗口。
CFRunLoopTimerRef 是基于時間的觸發(fā)器遥赚,它和 NSTimer 是toll-free bridged 的,可以混用(底層基于使用mk_timer實現(xiàn))阐肤。它受RunLoop的Mode影響(GCD的定時器不受RunLoop的Mode影響)凫佛,當其加入到 RunLoop 時,RunLoop會注冊對應(yīng)的時間點孕惜,當時間點到時愧薛,RunLoop會被喚醒以執(zhí)行那個回調(diào)。如果線程阻塞或者不在這個Mode下衫画,觸發(fā)點將不會執(zhí)行毫炉,一直等到下一個周期時間點觸發(fā)。
CFRunLoopObserverRef 是觀察者削罩,每個 Observer 都包含了一個回調(diào)(函數(shù)指針)瞄勾,當 RunLoop 的狀態(tài)發(fā)生變化時,觀察者就能通過回調(diào)接受到這個變化弥激〗福可以觀測的時間點有以下幾個
enum CFRunLoopActivity {
kCFRunLoopEntry = (1 << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1 << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1 << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1 << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1 << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1 << 7), // 即將退出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 包含上面所有狀態(tài)
};
typedef enum CFRunLoopActivity CFRunLoopActivity;
這里要提一句的是,timer和source1(也就是基于port的source)可以反復使用微服,比如timer設(shè)置為repeat趾疚,port可以持續(xù)接收消息,而source0在一次觸發(fā)后就會被runloop移除以蕴。
上面的 Source/Timer/Observer 被統(tǒng)稱為 mode item糙麦,一個 item 可以被同時加入多個 mode。但一個 item 被重復加入同一個 mode 時是不會有效果的丛肮。如果一個 mode 中一個 item 都沒有赡磅,則 RunLoop 會直接退出,不進入循環(huán)腾供。
RunLoop主要處理以下6類事件
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的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
...
};
一個RunLoop包含了多個Mode仆邓,每個Mode又包含了若干個Source/Timer/Observer鲜滩。每次調(diào)用 RunLoop的主函數(shù)時,只能指定其中一個Mode节值,這個Mode被稱作CurrentMode徙硅。如果需要切換 Mode,只能退出Loop搞疗,再重新指定一個Mode進入嗓蘑。這樣做主要是為了分隔開不同Mode中的Source/Timer/Observer,讓其互不影響匿乃。下面是5種Mode
- kCFDefaultRunLoopMode App的默認Mode桩皿,通常主線程是在這個Mode下運行
- UITrackingRunLoopMode 界面跟蹤Mode,用于ScrollView追蹤觸摸滑動幢炸,保證界面滑動時不受其他Mode影響
- UIInitializationRunLoopMode 在剛啟動App時第進入的第一個Mode泄隔,啟動完成后就不再使用
- GSEventReceiveRunLoopMode 接受系統(tǒng)事件的內(nèi)部Mode,通常用不到
- kCFRunLoopCommonModes 這是一個占位用的Mode宛徊,不是一種真正的Mode
其中kCFDefaultRunLoopMode佛嬉、UITrackingRunLoopMode是蘋果公開的,其余的mode都是無法添加的闸天。那為何我們又可以這么用呢
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
什么是CommonModes暖呕?
一個 Mode 可以將自己標記為”Common”屬性(通過將其 ModeName 添加到 RunLoop 的 “commonModes” 中)。每當 RunLoop 的內(nèi)容發(fā)生變化時苞氮,RunLoop 都會自動將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 標記的所有Mode里
主線程的 RunLoop 里有 kCFRunLoopDefaultMode 和 UITrackingRunLoopMode湾揽,這兩個Mode都已經(jīng)被標記為”Common”屬性。當你創(chuàng)建一個Timer并加到DefaultMode時笼吟,Timer會得到重復回調(diào)库物,但此時滑動一個 scrollView 時,RunLoop 會將 mode 切換為TrackingRunLoopMode赞厕,這時Timer就不會被回調(diào)艳狐,并且也不會影響到滑動操作。
如果想讓scrollView滑動時Timer可以正常調(diào)用皿桑,一種辦法就是手動將這個 Timer 分別加入這兩個 Mode毫目。另一種方法就是將 Timer 加入到CommonMode 中。
怎么將事件加入到CommonMode诲侮?
我們調(diào)用上面的代碼將 Timer 加入到CommonMode 時镀虐,但實際并沒有 CommonMode,其實系統(tǒng)將這個 Timer 加入到頂層的 RunLoop 的 commonModeItems 中沟绪。commonModeItems 會被 RunLoop 自動更新到所有具有”Common”屬性的 Mode 里去刮便。
這一步其實是系統(tǒng)幫我們將Timer加到了kCFRunLoopDefaultMode和UITrackingRunLoopMode中。
在項目中最常用的就是設(shè)置NSTimer的Mode绽慈,比較簡單這里就不說了恨旱。
RunLoop運行機制
當你調(diào)用 CFRunLoopRun() 時辈毯,線程就會一直停留在這個循環(huán)里;直到超時或被手動停止搜贤,該函數(shù)才會返回谆沃。每次線程運行RunLoop都會自動處理之前未處理的消息,并且將消息發(fā)送給觀察者仪芒,讓事件得到執(zhí)行唁影。RunLoop運行時首先根據(jù)modeName找到對應(yīng)mode,如果mode里沒有source/timer/observer掂名,直接返回据沈。
/// 用DefaultMode啟動
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode啟動,允許設(shè)置RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的實現(xiàn)
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根據(jù)modeName找到對應(yīng)mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode里沒有source/timer/observer, 直接返回饺蔑。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1\. 通知 Observers: RunLoop 即將進入 loop锌介。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 內(nèi)部函數(shù),進入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2\. 通知 Observers: RunLoop 即將觸發(fā) Timer 回調(diào)膀钠。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3\. 通知 Observers: RunLoop 即將觸發(fā) Source0 (非port) 回調(diào)掏湾。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 執(zhí)行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4\. RunLoop 觸發(fā) Source0 (非port) 回調(diào)。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 執(zhí)行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5\. 如果有 Source1 (基于port) 處于 ready 狀態(tài)肿嘲,直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)筑公。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7\. 調(diào)用 mach_msg 等待接受 mach_port 的消息雳窟。線程將進入休眠, 直到被下面某一個事件喚醒。
/// ? 一個基于 port 的Source 的事件匣屡。
/// ? 一個 Timer 到時間了
/// ? RunLoop 自身的超時時間到了
/// ? 被其他什么調(diào)用者手動喚醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8\. 通知 Observers: RunLoop 的線程剛剛被喚醒了封救。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,處理消息捣作。
handle_msg:
/// 9.1 如果一個 Timer 到時間了誉结,觸發(fā)這個Timer的回調(diào)。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block券躁,執(zhí)行block惩坑。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一個 Source1 (基于port) 發(fā)出事件了,處理這個事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 執(zhí)行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 進入loop時參數(shù)說處理完事件就返回也拜。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出傳入?yún)?shù)標記的超時時間了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部調(diào)用者強制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一個都沒有了
retVal = kCFRunLoopRunFinished;
}
/// 如果沒超時以舒,mode里沒空,loop也沒被停止慢哈,那繼續(xù)loop蔓钟。
} while (retVal == 0);
}
/// 10\. 通知 Observers: RunLoop 即將退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
RunLoop的掛起和喚醒
RunLoop的掛起
RunLoop的掛起是通過_CFRunLoopServiceMachPort —call—> mach_msg —call—> mach_msg_trap這個調(diào)用順序來告訴內(nèi)核RunLoop監(jiān)聽哪個mach_port(上面提到的消息通道)卵贱,然后等待事件的發(fā)生(等待與InputSource滥沫、Timer描述內(nèi)容相關(guān)的事件)侣集,這樣內(nèi)核就把RunLoop掛起了,即RunLoop休眠了兰绣。
RunLoop的喚醒
這接種情況下會被喚醒
- 存在Source0被標記為待處理世分,系統(tǒng)調(diào)用CFRunLoopWakeUp喚醒線程處理事件
- 定時器時間到了
- RunLoop自身的超時時間到了
- RunLoop外部調(diào)用者喚醒
當RunLoop被掛起后,如果之前監(jiān)聽的事件發(fā)生了狭魂,由另一個線程(或另一個進程中的某個線程)向內(nèi)核發(fā)送這個mach_port的msg后罚攀,trap狀態(tài)被喚醒,RunLoop繼續(xù)運行
處理事件
- 如果一個 Timer 到時間了雌澄,觸發(fā)這個Timer的回調(diào)
- 如果有dispatch到main_queue的block斋泄,執(zhí)行block
- 如果一個 Source1 發(fā)出事件了,處理這個事件
事件處理完成進行判斷
- 進入loop時傳入?yún)?shù)指明處理完事件就返回(stopAfterHandle)
- 超出傳入?yún)?shù)標記的超時時間(timeout)
- 被外部調(diào)用者強制停止__CFRunLoopIsStopped(runloop)
- source/timer/observer 全都空了__CFRunLoopModeIsEmpty(runloop, currentMode)
RunLoop 的底層實現(xiàn)
關(guān)于這個大家可以看ibireme的深入理解RunLoop一文镐牺,我這里選擇一些覺得比較重要又不是那么難懂的炫掐。
Mach消息發(fā)送機制看這篇文章Mach消息發(fā)送機制
為了實現(xiàn)消息的發(fā)送和接收,mach_msg() 函數(shù)實際上是調(diào)用了一個 Mach 陷阱 (trap)睬涧,即函數(shù)mach_msg_trap()募胃,陷阱這個概念在 Mach 中等同于系統(tǒng)調(diào)用。當你在用戶態(tài)調(diào)用 mach_msg_trap() 時會觸發(fā)陷阱機制畦浓,切換到內(nèi)核態(tài)痹束;內(nèi)核態(tài)中內(nèi)核實現(xiàn)的 mach_msg() 函數(shù)會完成實際的工作,如下圖:
RunLoop 的核心就是一個 mach_msg() (見上面代碼的第7步)讶请,RunLoop 調(diào)用這個函數(shù)去接收消息祷嘶,如果沒有別人發(fā)送 port 消息過來,內(nèi)核會將線程置于等待狀態(tài)夺溢。例如你在模擬器里跑起一個 iOS 的 App论巍,然后在 App 靜止時點擊暫停,你會看到主線程調(diào)用棧是停留在 mach_msg_trap() 這個地方风响。
RunLoop和線程
RunLoop和線程是息息相關(guān)的,我們知道線程的作用是用來執(zhí)行特定的一個或多個任務(wù),但是在默認情況下,線程執(zhí)行完之后就會退出,就不能再執(zhí)行任務(wù)了嘉汰。這時我們就需要采用一種方式來讓線程能夠處理任務(wù),并不退出。所以,我們就有了RunLoop状勤。
iOS開發(fā)中能遇到兩個線程對象: pthread_t和NSThread鞋怀,pthread_t和NSThread 是一一對應(yīng)的。比如荧降,你可以通過 pthread_main_thread_np()或 [NSThread mainThread]來獲取主線程接箫;也可以通過pthread_self()或[NSThread currentThread]來獲取當前線程。CFRunLoop 是基于 pthread 來管理的朵诫。
線程與RunLoop是一一對應(yīng)的關(guān)系(對應(yīng)關(guān)系保存在一個全局的Dictionary里)辛友,線程創(chuàng)建之后是沒有RunLoop的(主線程除外),RunLoop的創(chuàng)建是發(fā)生在第一次獲取時,銷毀則是在線程結(jié)束的時候。只能在當前線程中操作當前線程的RunLoop,而不能去操作其他線程的RunLoop废累。
蘋果不允許直接創(chuàng)建RunLoop邓梅,但是可以通過[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()來獲取(如果沒有就會自動創(chuàng)建一個)邑滨。
/// 全局的Dictionary日缨,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
/// 獲取一個 pthread 對應(yīng)的 RunLoop掖看。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次進入時匣距,初始化全局Dic,并先為主線程創(chuàng)建一個 RunLoop哎壳。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接從 Dictionary 里獲取毅待。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到時,創(chuàng)建一個
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 注冊一個回調(diào)归榕,當線程銷毀時尸红,順便也銷毀其對應(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());
}
開發(fā)過程中需要RunLoop時刹泄,則需要手動創(chuàng)建和運行RunLoop(尤其是在子線程中, 主線程中的Main RunLoop除外)外里,我看到別人舉了這么個例子,很有意思
調(diào)用[NSTimer scheduledTimerWithTimeInterval:target:selector:userInfo:repeats:]帶有schedule的方法簇來啟動Timer.
此方法會創(chuàng)建Timer并把Timer放到當前線程的RunLoop中特石,隨后RunLoop會在Timer設(shè)定的時間點回調(diào)Timer綁定的selector或Invocation盅蝗。但是,在主線程和子線程中調(diào)用此方法的效果是有差異的姆蘸,即在主線程中調(diào)用scheduledTimer方法時timer可以在設(shè)定的時間點觸發(fā)风科,但是在子線程里則不能觸發(fā)。這是因為子線程中沒有創(chuàng)建RunLoop且更沒有啟動RunLoop乞旦,而主線程中的RunLoop默認是創(chuàng)建好的且一直運行著。所以题山,子線程中需要像下面這樣調(diào)用兰粉。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
[[NSRunLoop currentRunLoop] run];
});
那為什么下面這樣調(diào)用同樣不會觸發(fā)Timer呢?
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[[NSRunLoop currentRunLoop] run];
[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimer) userInfo:nil repeats:NO];
});
我的分析是:scheduledTimerWithTimeInterval內(nèi)部在向RunLoop傳遞Timer時是調(diào)用與線程實例相關(guān)的單例方法[NSRunLoop currentRunLoop]來獲取RunLoop實例的顶瞳,即RunLoop實例不存在就創(chuàng)建一個與當前線程相關(guān)的RunLoop并把Timer傳遞到RunLoop中玖姑,存在則直接傳Timer到RunLoop中即可。而在RunLoop開始運行后再向其傳遞Timer時慨菱,由于dispatch_async代碼塊里的兩行代碼是順序執(zhí)行焰络,[[NSRunLoop currentRunLoop] run]是一個沒有結(jié)束時間的RunLoop,無法執(zhí)行到“[NSTimer scheduledTimerWithTimeInterval:…”這一行代碼符喝,Timer也就沒有被加到當前RunLoop中闪彼,所以更不會觸發(fā)Timer了斧拍。
蘋果用 RunLoop 實現(xiàn)的功能
AutoreleasePool
App啟動之后惯殊,系統(tǒng)啟動主線程并創(chuàng)建了RunLoop,在main thread中注冊了兩個observer,回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler()
第一個Observer監(jiān)視的事件
- 即將進入Loop(kCFRunLoopEntry)村象,其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池。其order是-2147483647营勤,優(yōu)先級最高旁舰,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。
第二個Observer監(jiān)視了兩個事件
準備進入休眠(kCFRunLoopBeforeWaiting)铭污,此時調(diào)用 _objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 來釋放舊的池并創(chuàng)建新的池恋日。
即將退出Loop(kCFRunLoopExit)此時調(diào)用 _objc_autoreleasePoolPop()釋放自動釋放池。這個 Observer的order是2147483647嘹狞,確保池子釋放在所有回調(diào)之后岂膳。
我們知道AutoRelease對象是被AutoReleasePool管理的,那么AutoRelease對象在什么時候被回收呢刁绒?
第一種情況:在我們自己寫的for循環(huán)或線程體里闷营,我們都習慣用AutoReleasePool來管理一些臨時變量的autorelease,使得在for循環(huán)或線程結(jié)束后回收AutoReleasePool的時候來回收AutoRelease臨時變量知市。
另一種情況:我們在主線程里創(chuàng)建了一些AutoRelease對象傻盟,這些對象可不能指望在回收Main AutoReleasePool時才被回收,因為App一直運行的過程中Main AutoReleasePool是不會被回收的嫂丙。那么這種AutoRelease對象的回收就依賴Main RunLoop的運行狀態(tài)娘赴,Main RunLoop的Observer會在Main RunLoop結(jié)束休眠被喚醒時(kCFRunLoopAfterWaiting狀態(tài))通知UIKit,UIKit收到這一通知后就會調(diào)用_CFAutorleasePoolPop方法來回收主線程中的所有AutoRelease對象跟啤。
在主線程中執(zhí)行代碼一般都是寫在事件回調(diào)或Timer回調(diào)中的诽表,這些回調(diào)都被加入了main thread的自動釋放池中,所以在ARC模式下我們不用關(guān)心對象什么時候釋放隅肥,也不用去創(chuàng)建和管理pool竿奏。(如果事件不在主線程中要注意創(chuàng)建自動釋放池,否則可能會出現(xiàn)內(nèi)存泄漏)腥放。
NSTimer(timer觸發(fā))
上文說到了CFRunLoopTimerRef泛啸,其實NSTimer的原型就是CFRunLoopTimerRef。一個Timer注冊 RunLoop 之后秃症,RunLoop 會為這個Timer的重復時間點注冊好事件候址。有兩點需要注意:
- 但是需要注意的是RunLoop為了節(jié)省資源,并不會在非常準確的時間點回調(diào)這個Timer种柑。Timer 有個屬性叫做 Tolerance (寬容度)岗仑,標示了當時間點到后,容許有多少最大誤差聚请。這個誤差默認為0荠雕,我們可以手動設(shè)置這個誤差。文檔最后還強調(diào)了,為了防止時間點偏移舞虱,系統(tǒng)有權(quán)力給這個屬性設(shè)置一個值無論你設(shè)置的值是多少欢际,即使RunLoop 模式正確,當前線程并不阻塞矾兜,系統(tǒng)依然可能會在 NSTimer 上加上很小的的容差损趋。
- 我們在哪個線程調(diào)用 NSTimer 就必須在哪個線程終止
在RunLoop的Mode中也有說到,NSTimer使用的時候注意Mode,比如我之前開發(fā)時候用NSTimer寫一個Banner圖片輪播框架椅寺,如果不設(shè)置Timer的Mode為commonModes那么在滑動TableView的時候Banner就停止輪播
DispatchQueue.global().async {
// 非主線程不能使用 Timer.scheduledTimer進行初始化
// self.timer = Timer.scheduledTimer(timeInterval: 6.0, target: self, selector: #selector(TurnPlayerView.didTurnPlay), userInfo: nil, repeats: false)
if #available(iOS 10.0, *) {
self.timer = Timer(timeInterval: 6.0, repeats: true, block: { (timer) in
self.setContentOffset(CGPoint(x: self.frame.width*2, y: self.contentOffset.y), animated: true)
})
} else {
// Fallback on earlier versions
}
RunLoop.main.add(self.timer!, forMode: RunLoopMode.commonModes)
}
和GCD的關(guān)系
- RunLoop底層用到GCD
- RunLoop與GCD并沒有直接關(guān)系浑槽,但當GCD使用到main_queue時才有關(guān)系,如下:
//實驗GCD Timer 與 Runloop的關(guān)系返帕,只有當dispatch_get_main_queue時才與RunLoop有關(guān)系
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"GCD Timer...");
});
當調(diào)用 dispatch_async(dispatch_get_main_queue(), block) 時桐玻,libDispatch 會向主線程的 RunLoop 發(fā)送消息,RunLoop會被喚醒荆萤,并從消息中取得這個 block镊靴,并在回調(diào) CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里執(zhí)行這個 block。但這個邏輯僅限于 dispatch 到主線程链韭,dispatch 到其他線程仍然是由 libDispatch 處理的偏竟。同理,GCD的dispatch_after在dispatch到main_queue時的timer機制才與RunLoop相關(guān)敞峭。
PerformSelecter
NSObject的performSelecter:afterDelay: 實際上其內(nèi)部會創(chuàng)建一個 Timer 并添加到當前線程的 RunLoop 中踊谋。所以如果當前線程沒有 RunLoop,則這個方法會失效旋讹。
NSObject的performSelector:onThread: 實際上其會創(chuàng)建一個 Timer 加到對應(yīng)的線程去殖蚕,同樣的,如果對應(yīng)線程沒有 RunLoop 該方法也會失效沉迹。
其實這種方式有種說法也叫創(chuàng)建常駐線程(內(nèi)存)睦疫,AFNetworking也用到這種技法。舉個例子鞭呕,如果把RunLoop去掉笼痛,那么test方法就不會執(zhí)行。
class SecondViewController: UIViewController {
var thread: Thread!
override func viewDidLoad() {
super.viewDidLoad()
self.view.backgroundColor = UIColor.red
thread = Thread.init(target: self, selector: #selector(SecondViewController.run), object: nil)
thread.start()
}
@objc func run() {
print("run -- ")
RunLoop.current.add(Port(), forMode: .defaultRunLoopMode)
RunLoop.current.run()
}
@objc func test() {
print("test -- \(Thread.current)")
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
// self.test()
self.perform(#selector(SecondViewController.test), on: thread, with: nil, waitUntilDone: false)
}
}
網(wǎng)絡(luò)請求
iOS中的網(wǎng)絡(luò)請求接口自下而上有這么幾層
其中CFSocket和CFNetwork偏底層琅拌,早些時候比較知名的網(wǎng)絡(luò)框架AFNetworking是基于NSURLConnection編寫的,iOS7之后新增了NSURLSession摘刑,NSURLSession的底層仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 線程)进宝,之后AFNetworking和Alamofire就是基于它封裝的了。
通常使用 NSURLConnection 時枷恕,會傳入一個 Delegate党晋,當調(diào)用了 [connection start] 后,這個 Delegate 就會不停收到事件回調(diào)。實際上未玻,start 這個函數(shù)的內(nèi)部會獲取 CurrentRunLoop灾而,然后在其中的 DefaultMode 添加了4個 Source0 (即需要手動觸發(fā)的Source)。CFMultiplexerSource 是負責各種 Delegate 回調(diào)的扳剿,CFHTTPCookieStorage 是處理各種 Cookie 的旁趟。
開始網(wǎng)絡(luò)傳輸時,NSURLConnection 創(chuàng)建了兩個新線程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private庇绽。
其中 CFSocket 線程是處理底層 socket 連接的锡搜,NSURLConnectionLoader中的RunLoop通過一些基于mach port的Source1接收來自底層CFSocket的通知。當收到通知后瞧掺,其會在合適的時機向CFMultiplexerSource等Source0發(fā)送通知耕餐,同時喚醒Delegate線程的RunLoop來讓其處理這些通知。CFMultiplexerSource會在Delegate線程的RunLoop對Delegate執(zhí)行實際的回調(diào)辟狈。
事件響應(yīng)
蘋果注冊了一個 Source1 (基于 mach port 的) 用來接收系統(tǒng)事件肠缔,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()。
當一個硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后哼转,首先由 IOKit.framework 生成一個 IOHIDEvent 事件并由 SpringBoard 接收明未。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸释簿,加速亚隅,接近傳感器等幾種 Event,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進程庶溶。
觸摸事件其實是Source1接收系統(tǒng)事件后在回調(diào) __IOHIDEventSystemClientQueueCallback()內(nèi)觸發(fā)的 Source0煮纵,Source0 再觸發(fā)的 _UIApplicationHandleEventQueue()。source0一定是要喚醒runloop及時響應(yīng)并執(zhí)行的偏螺,如果runloop此時在休眠等待系統(tǒng)的 mach_msg事件行疏,那么就會通過source1來喚醒runloop執(zhí)行。
_UIApplicationHandleEventQueue() 會把 IOHIDEvent 處理并包裝成 UIEvent 進行處理或分發(fā)套像,其中包括識別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等酿联。
手勢識別
當上面的 _UIApplicationHandleEventQueue() 識別了一個手勢時,其首先會調(diào)用 Cancel 將當前的 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)。
當有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時美澳,這個回調(diào)都會進行相應(yīng)處理销部。
UI更新
Core Animation 在 RunLoop 中注冊了一個 Observer 監(jiān)聽 BeforeWaiting(即將進入休眠) 和 Exit (即將退出Loop) 事件 摸航。當在操作 UI 時,比如改變了 Frame舅桩、更新了 UIView/CALayer 的層次時酱虎,或者手動調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,這個 UIView/CALayer 就被標記為待處理擂涛,并被提交到一個全局的容器去读串。當Oberver監(jiān)聽的事件到來時,回調(diào)執(zhí)行函數(shù)中會遍歷所有待處理的UIView/CAlayer 以執(zhí)行實際的繪制和調(diào)整歼指,并更新 UI 界面爹土。
如果此處有動畫,通過 DisplayLink 穩(wěn)定的刷新機制會不斷的喚醒runloop踩身,使得不斷的有機會觸發(fā)observer回調(diào)胀茵,從而根據(jù)時間來不斷更新這個動畫的屬性值并繪制出來。
函數(shù)內(nèi)部的調(diào)用棧
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];
繪圖和動畫有兩種處理的方式:CPU(中央處理器)和GPU(圖形處理器)
CPU: CPU 中計算顯示內(nèi)容挟阻,比如視圖的創(chuàng)建琼娘、布局計算、圖片解碼附鸽、文本繪制等
GPU: GPU 進行變換脱拼、合成、渲染.
關(guān)于CADisplayLink的描述有兩種
CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現(xiàn)原理更復雜坷备,和 NSTimer 并不一樣熄浓,其內(nèi)部實際是操作了一個 Source)。如果在兩次屏幕刷新之間執(zhí)行了一個長任務(wù)省撑,那其中就會有一幀被跳過去(和 NSTimer 相似)赌蔑,造成界面卡頓的感覺。在快速滑動TableView時竟秫,即使一幀的卡頓也會讓用戶有所察覺娃惯。
CADisplayLink是一個執(zhí)行頻率(fps)和屏幕刷新相同(可以修改preferredFramesPerSecond改變刷新頻率)的定時器,它也需要加入到RunLoop才能執(zhí)行肥败。與NSTimer類似趾浅,CADisplayLink同樣是基于CFRunloopTimerRef實現(xiàn),底層使用mk_timer(可以比較加入到RunLoop前后RunLoop中timer的變化)馒稍。和NSTimer相比它精度更高(盡管NSTimer也可以修改精度)皿哨,不過和NStimer類似的是如果遇到大任務(wù)它仍然存在丟幀現(xiàn)象。通常情況下CADisaplayLink用于構(gòu)建幀動畫纽谒,看起來相對更加流暢往史,而NSTimer則有更廣泛的用處。
不管怎么樣CADisplayLink和NSTimer是有很大不同的佛舱,詳情可以參考這篇文章CADisplayLink
ibireme根據(jù)CADisplayLink的特性寫了個FPS指示器YYFPSLabel椎例,代碼非常少
原理是這樣的:既然CADisplayLink可以以屏幕刷新的頻率調(diào)用指定selector,而且iOS系統(tǒng)中正常的屏幕刷新率為60Hz(60次每秒)请祖,所以使用 CADisplayLink 的 timestamp 屬性订歪,配合 timer 的執(zhí)行次數(shù)計算得出FPS數(shù)
- (void)viewDidLoad {
[super viewDidLoad];
_thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil];
[_thread start];
}
- (void)threadTest
{
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(TimerTest) userInfo:nil repeats:YES];
while (1) {
[[NSRunLoop currentRunLoop] run];
NSLog(@"++++++++");
}
}
- (void)TimerTest
{
NSLog(@"----%@-----", [NSThread currentThread]);
}
打印線程,不打印 NSLog(@"++++++++");
只在滾動的時候監(jiān)聽定制器啟動
NSTimer*timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(find) userInfo:nil repeats:YES];// 在UITrackingRunLoopMode模式下定時器才會運行
[[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
參考文章
深入理解RunLoop
iOS 事件處理機制與圖像渲染過程
RunLoop學習筆記(一) 基本原理介紹
iOS刨根問底-深入理解RunLoop
【iOS程序啟動與運轉(zhuǎn)】- RunLoop個人小結(jié)
RunLoop的前世今生
Runloop知識樹
RunLoop入門 看我就夠了
RunLoop已入門肆捕?不來應(yīng)用一下刷晋?