強烈推薦
ibireme
大神的文章深刻理解RunLoopc++Runloop源碼地址objective-c
關於 Runloop
,儘管早就知道它的本質實現是一個循環,但筆者仍是一直很困惑它的做用是什麼 ,不過最近整理相關知識總算是理解了。bash
代碼的執行邏輯是自上而下的,若是沒有 Runloop
,代碼執行完畢後,程序就退出了,對應到實際場景就是 APP
一打開立馬就退出了。app
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"程序執行中...");
}
return 0;
}
// log
程序執行中...
Program ended with exit code: 0
複製代碼
例如上面的代碼,代碼執行完畢後,main
函數返回,而後程序退出。函數
爲何工做中,好像沒有編寫 Runloop
相關的代碼,程序仍是可以穩定持續運行呢?oop
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
複製代碼
這是由於程序自動幫咱們在 UIApplicationMain…
中作了這個事情。性能
下面來看看 Runloop
的簡化的僞代碼,主要來自 sunnyxx 大神的一次視頻分享:spa
function loop() {
do {
有事幹了 = 我睡覺了沒事別找我();
if (搬磚) {
搬磚();
} else if (吃飯) {
吃飯();
}
} while (活着)
}
複製代碼
這個僞代碼看着仍是有一點抽象,須要瞭解的一個知識點是線程和 RunLoop
之間是一一對應的,這裏的睡覺了能夠理解爲線程休眠 [NSThread sleepUntilDate:...]]
,也就是說當應用沒有任何事件觸發時,就會停在睡覺那行代碼不執行,這樣就節約了 CPU
的運算資源,提升程序性能,直到有事件喚醒應用爲止。例如上面的搬磚事件,吃飯事件。處理完後,又會進入睡覺狀態直到下次喚醒,反覆循環,這樣就保證了程序能隨時處理各類事件並可以穩定運行。線程
實際上觸摸事件、屏幕 UI
刷新、延遲迴調等等都是 Runloop
實現的。指針
先來看看 Runloop
的結構源碼:
struct __CFRunLoop {
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
// ...
};
複製代碼
這裏包含一個線程的成員變量 _pthread
,能夠看出 Runloop
確實和線程是息息相關的。還能看到 Runloop
擁有不少關於 Model
的成員變量,再來看看 Model
的結構:
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
// ...
};
複製代碼
先無論這些東西是幹什麼的,至少咱們如今可以得出以下圖所示的理解:
一個 Runloop
中包含若干個 Model
,每一個 Mode
又包含若干個 Source/Timer/Observer
。
Model
表明 Runloop
的運行模式,Runloop
每次只能指定一個 Model
做爲 _currentMode
,若是須要切換 Mode
,只能退出當前 Loop
,再從新選擇一個 Mode
進入。主線程的 Runloop
這裏有兩個預置的模式 ,而且這也是系統公開的兩個 Model
:
kCFRunLoopDefaultMode:APP
的普通狀態,一般主線程是在這個Mode下運行,已被標記爲 Common
。
UITrackingRunLoopMode:App
追蹤觸摸 ScrollView
滑動時的狀態,保證界面滑動時不受其餘 Mode
影響,已被標記爲 Common
。
注意 Runloop
的結構中有一個 _commonModes
。這裏是由於一個 Mode
能夠將本身標記爲 Common
(經過將其 ModeName
添加到 RunLoop
的 commonModes
中 ),標記爲 Common
的 Model
均可以處理事件,能夠理解爲變相的實現了多個 Model
同時運行。同時系統也提供了一個操做 Common
標記的字符串->kCFRunLoopCommonModes
。若是咱們想要上面兩種模式下都能處理事件,就可使用這個字符串。
Source/Timer/Observer
被統稱爲 mode item,不一樣 Model
的 Source0/Source1/Timer/Observer
被分隔開來,互不影響,若是 Mode
裏沒有任何Source0/Source1/Timer/Observer
,RunLoop
會立馬退出。
Source
是事件產生的的地方,它對應的類爲 CFRunLoopSourceRef
。Source
有兩個版本:Source0
和 Source1
。
Source0
只包含了一個回調(函數指針),它並不能主動觸發事件。Source1
包含了一個 mach_port
和一個回調(函數指針),被用於經過內核和其餘線程相互發送消息。這種 Source
能主動喚醒 RunLoop
的線程。例如屏幕觸摸、鎖屏和搖晃等。Timer
對應的類是 CFRunLoopTimerRef
,它其實就是 NSTimer
,當其加入到 RunLoop 時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回調。
Observer
對應的類是 CFRunLoopObserverRef
,當 RunLoop 的狀態發生變化時,觀察者就能經過回調接受到這個變化。能夠觀測的時間點有如下幾個:
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
的源碼,面對衆多代碼,讓人毫無頭緒,可是前文中已經講到,屏幕的觸摸事件是 Runloop
來處理的。因而打個斷點,來查看程序的函數調用棧:
從圖中能看到,Runloop
是從 11
開始的,因而從源碼中搜索 CFRunLoopRunSpecific
函數,這裏只探究內部主要邏輯,其餘細節不看,下面是精簡後的函數:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
// 根據 modeName 獲取currentMode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
// 設置 Runloop 的 Model
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
// 通知 Observers: 即將進入 RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 進入 runloop
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知 Observers: RunLoop 即將退出
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
複製代碼
而後再進入 __CFRunLoopRun(...)
函數查看內部精簡後的主要邏輯源碼:
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
int32_t retVal = 0;
do {
// 通知 Observers: 即將處理 Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 Observers: 即將處理 Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 處理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 處理 Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 處理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
// 判斷有無 Sources1
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
// 跳轉到 handle_msg 處理 Sources1soso
goto handle_msg;
}
// 通知 Observers: 即將休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 開始休眠
__CFRunLoopSetSleeping(rl);
// 等待消息喚醒當前線程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
// 結束休眠
__CFRunLoopUnsetSleeping(rl);
// 通知 Observers: 結束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 處理
handle_msg:;
// 被 timer 喚醒
if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
// 處理 timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
}
// 被 gcd 喚醒
else if (livePort == dispatchPort) {
// 處理 gcd
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
// 被source1喚醒
} else {
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
}
// 處理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 設置返回值
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
複製代碼
能夠看到 Runloop
內部確實是一個循環,而且,喚醒 RunLoop
的方式有 mach port
、Timer
和 dispatch
。筆者最初在疑惑一個問題,上面的函數調用棧是一個點擊屏幕後的響應事件,能夠看出這裏是 sources0
,明明是一個觸摸事件爲何不是 sources1
呢,筆者猜想 sources1
這裏喚醒了 Runloop
,由於 sources0
是沒法喚醒 runloop
的,而後再在 sources0
的回調中處理的點擊事件。
這裏因爲目前筆者水平有限,只可以理解到 mach port
是一個能夠控制硬件和接受硬件反饋的一個系統,而後能夠經過它未來自硬件的操做轉化成熟知的 UIEvent
事件等等。
這篇文章主要講解了 Runloop
究竟是一個什麼東西,固然 Runloop
的知識不只僅只有這篇文章這點。例如實際用處中的線程保活(AFNetworking 2.x 版本中),滑動時 Timer
怎麼不被中止,自動釋放池的實現等等都用到了 Runloop
。