最近在學習一些OC底層的東西, 下面是學習了RunLoop
的一些總結(jié)和感受^^
首先,RunLoop的作用
從字面上看,RunLoop
是運行循環(huán)的意思.實際上,線程就是依靠RunLoop
來實現(xiàn)任務(wù)來了被喚醒,完成任務(wù)之后休眠的這一特性.
RunLoop的基本作用:
- 保持程序的持續(xù)運行
- 處理App中的各種事件(比如觸摸事件, 定時器事件, selector事件)
- 節(jié)約系統(tǒng)資源,提高程序性能(有事件要處理時被喚醒,沒事件時休眠)
RunLoop
實際上就是一個對象棱诱,這個對象管理了其需要處理的事件和消息,并提供了一個入口函數(shù)來執(zhí)行任務(wù)來了被喚醒,完成任務(wù)之后休眠的邏輯涝动。線程執(zhí)行了這個函數(shù)后迈勋,就會一直處于這個函數(shù)內(nèi)部 "接受消息->等待->處理" 的循環(huán)中,直到這個循環(huán)結(jié)束(比如傳入quit的消息)醋粟,函數(shù)返回靡菇。
OSX/iOS 系統(tǒng)中,提供了兩個這樣的對象:NSRunLoop
和CFRunLoopRef
米愿。
CFRunLoopRef
是在CoreFoundation
框架內(nèi)的厦凤,它提供了純 C 函數(shù)的 API,所有這些 API 都是線程安全的育苟。
NSRunLoop
是基于CFRunLoopRef
的封裝较鼓,提供了面向?qū)ο蟮?API,但是這些 API 不是線程安全的违柏。
RunLoop與線程的關(guān)系
蘋果不允許直接創(chuàng)建RunLoop
博烂,它只提供了兩個自動獲取的函數(shù):CFRunLoopGetMain()
和CFRunLoopGetCurrent()
。這兩個函數(shù)內(nèi)部的邏輯大概是下面這樣:
/// 全局的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());
}
從上面的代碼中,我們可以得到以下的一些結(jié)論:
- 每條線程都有唯一的一個與之對應(yīng)的
RunLoop
對象,并且以{pthread_t
:CFRunLoopRef
}的形式保存在一個全局的Dictionary中 - 主線程的
RunLoop
已經(jīng)自動創(chuàng)建好了,子線程的RunLoop
需要手動進行創(chuàng)建 -
RunLoop
在第一次獲取時通過懶加載創(chuàng)建,在線程結(jié)束時銷毀 - 你只能在一個線程內(nèi)部獲取屬于它自己的那個
RunLoop
(主線程除外)
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
RunLoop相關(guān)類
Core Foundation中關(guān)于RunLoop的5個類
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
他們的關(guān)系關(guān)系如圖:
一個 RunLoop
包含若干個 Mode
,每個 Mode
又包含若干個 Source/Timer/Observer
们颜。每次調(diào)用 RunLoop
的主函數(shù)時吕朵,只能指定其中一個 Mode
猎醇,這個Mode
被稱作 CurrentMode
。如果需要切換 Mode努溃,只能退出 Loop硫嘶,再重新指定一個 Mode 進入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer
梧税,讓其互不影響沦疾。
CFRunLoopSourceRef
CFRunLoopSourceRef
是事件源(輸入源)
以前的分法
- Port-Based Sources
- Custom Input Sources
- Cocoa Perform Selector Sources
現(xiàn)在的分法
- Source0:只包含了一個回調(diào)(函數(shù)指針),它并不能主動觸發(fā)事件第队。使用時哮塞,你需要先調(diào)用
CFRunLoopSourceSignal(source)
,將這個 Source 標記為待處理凳谦,然后手動調(diào)用CFRunLoopWakeUp(runloop)
來喚醒 RunLoop忆畅,讓其處理這個事件。 - Source1:包含了一個
mach_port
和一個回調(diào)(函數(shù)指針)尸执,被用于通過內(nèi)核和其他線程相互發(fā)送消息
可以以下代碼添加新的CFRunLoopSourceRef:
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
CFRunLoopTimerRef
CFRunLoopTimerRef
是基于時間的觸發(fā)器家凯,它和 NSTimer
是toll-free bridged
的,可以混用如失。其包含一個時間長度和一個回調(diào)(函數(shù)指針)绊诲。當其加入到 RunLoop
時,RunLoop
會注冊對應(yīng)的時間點褪贵,當時間點到時掂之,RunLoop
會被喚醒以執(zhí)行那個回調(diào)。
-
NSTimer
會受到RunLoop
的Mode的影響可能會不準確 - GCD的定時器不受
RunLoop
的Mode的影響
下面是兩種往RunLoop
中添加定時器的方法:
- (void)testRunloopTimerWithNSTimer
{
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"我是NSTimer");
}];
//默認模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//滑動模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
//能夠在默認模式或者滑動模式中回調(diào)這個NSTimer
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)testRunloopTimerWithGCDTimer
{
dispatch_queue_t mainQueue = dispatch_get_main_queue();
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, mainQueue);
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
NSLog(@"我是GCD Timer");
});
dispatch_resume(timer);
//需要要有一個指針指向Timer,不然這個函數(shù)結(jié)束Timer就被釋放掉了
self.timer = timer;
}
CFRunLoopObserverRef
CFRunLoopObserverRef
是觀察者,能夠監(jiān)聽RunLoop
狀態(tài)變化. 能夠觀測的時間點有以下幾個:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};
Runloop的Mode
上面的 Source/Timer/Observer
被統(tǒng)稱為 mode item
脆丁,一個item
可以被同時加入多個mode
世舰。但一個 item
被重復加入同一個mode
時是不會有效果的。如果一個 mode
中一個 item
都沒有偎快,則 RunLoop
會直接退出冯乘,不進入循環(huán)。
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
...
};
幾種常使用的Mode:
-
NSDefaultRunLoopMode
:這是App平時所處的狀態(tài) -
UITrackingRunLoopMode
:這是追蹤ScrollView
滑動時的狀態(tài) -
NSRunLoopCommonModes
:能夠?qū)?code>mode itemr添加到__CFRunLoop
里面的_commonModeItems
中,然后RunLoop會將會自動將這個mode item
同步到具有 "Common"標記的所有Mode(NSDefaultRunLoopMode和UITrackingRunLoopMode)
里晒夹。
RunLoop的內(nèi)部邏輯
其內(nèi)部代碼整理如下:
/// 用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
就是這樣一個函數(shù),其內(nèi)部是一個 do-while
循環(huán)名惩。當你調(diào)用CFRunLoopRun()
時澎胡,線程就會一直停留在這個循環(huán)里;直到超時或被手動停止娩鹉,該函數(shù)才會返回攻谁。
應(yīng)用
使用線程的定時器
Cocoa
中使用任何performSelector…
的方法自動釋放池
App啟動后,蘋果在主線程
RunLoop
里注冊了兩個Observer
弯予,其回調(diào)都是_wrapRunLoopWithAutoreleasePoolHandler()
戚宦。第一個
Observer
監(jiān)視的事件是 Entry(即將進入Loop),其回調(diào)內(nèi)會調(diào)用_objc_autoreleasePoolPush()
創(chuàng)建自動釋放池锈嫩。其 order 是-2147483647
受楼,優(yōu)先級最高,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前呼寸。
第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting
(準備進入休眠) 時調(diào)用_objc_autoreleasePoolPop()
和 _objc_autoreleasePoolPush()
釋放舊的池并創(chuàng)建新池艳汽;Exit(即將退出Loop) 時調(diào)用 _objc_autoreleasePoolPop()
來釋放自動釋放池。這個 Observer
的 order 是 2147483647
对雪,優(yōu)先級最低河狐,保證其釋放池子發(fā)生在其他所有回調(diào)之后。
在主線程執(zhí)行的代碼,通常是寫在諸如事件回調(diào)馋艺、Timer
回調(diào)內(nèi)的栅干。這些回調(diào)會被 RunLoop
創(chuàng)建好的 AutoreleasePool
環(huán)繞著,所以不會出現(xiàn)內(nèi)存泄漏捐祠,開發(fā)者也不必顯示創(chuàng)建 Pool
了碱鳞。
- 創(chuàng)建一個常駐線程
開啟一個常駐線程(讓一個子線程不進入消亡狀態(tài),等待其他線程發(fā)來消息,處理其他事件),可以在這個線程里面開啟一個定時器或者進行一些長期監(jiān)控
通過方法[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
在RunLoop中添加至少一個Source就能夠讓線程常駐
- 可以添加
Observer
監(jiān)聽RunLoop
的狀態(tài),比如監(jiān)聽點擊事件的處理(在所有點擊事件之前做一些事情)