RunLoop的一些學習與總結
最近在學習一些OC底層的東西, 下面是學習了RunLoop
的一些總結和感受^^
首先,RunLoop的作用
從字面上看,RunLoop
是執行迴圈的意思.實際上,執行緒就是依靠RunLoop
來實現任務來了被喚醒,完成任務之後休眠的這一特性.
RunLoop的基本作用:
- 保持程式的持續執行
- 處理App中的各種事件(比如觸控事件, 定時器事件, selector事件)
- 節約系統資源,提高程式效能(有事件要處理時被喚醒,沒事件時休眠)
RunLoop
實際上就是一個物件,這個物件管理了其需要處理的事件和訊息,並提供了一個入口函式來執行任務來了被喚醒,完成任務之後休眠的邏輯。執行緒執行了這個函式後,就會一直處於這個函式內部 "接受訊息->等待->處理" 的迴圈中,直到這個迴圈結束(比如傳入quit的訊息),函式返回。
OSX/iOS 系統中,提供了兩個這樣的物件:NSRunLoop
和CFRunLoopRef
。CFRunLoopRef
是在CoreFoundation
框架內的,它提供了純 C 函式的 API,所有這些 API 都是執行緒安全的。NSRunLoop
是基於CFRunLoopRef
的封裝,提供了物件導向的 API,但是這些 API 不是執行緒安全的。
RunLoop與執行緒的關係
蘋果不允許直接建立RunLoop
,它只提供了兩個自動獲取的函式:CFRunLoopGetMain()
和CFRunLoopGetCurrent()
。這兩個函式內部的邏輯大概是下面這樣:
/// 全域性的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
/// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次進入時,初始化全域性Dic,並先為主執行緒建立一個 RunLoop。
loopsDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
}
/// 直接從 Dictionary 裡獲取。
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
if (!loop) {
/// 取不到時,建立一個
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
/// 註冊一個回撥,當執行緒銷燬時,順便也銷燬其對應的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
從上面的程式碼中,我們可以得到以下的一些結論:
- 每條執行緒都有唯一的一個與之對應的
RunLoop
物件,並且以{pthread_t
:CFRunLoopRef
}的形式儲存在一個全域性的Dictionary中 - 主執行緒的
RunLoop
已經自動建立好了,子執行緒的RunLoop
需要手動進行建立 -
RunLoop
在第一次獲取時通過懶載入建立,線上程結束時銷燬 - 你只能在一個執行緒內部獲取屬於它自己的那個
RunLoop
(主執行緒除外)
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
RunLoop相關類
Core Foundation中關於RunLoop的5個類
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
他們的關係關係如圖:
一個 RunLoop
包含若干個 Mode
,每個 Mode
又包含若干個 Source/Timer/Observer
。每次呼叫 RunLoop
的主函式時,只能指定其中一個 Mode
,這個Mode
被稱作 CurrentMode
。如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer
,讓其互不影響。
CFRunLoopSourceRef
CFRunLoopSourceRef
是事件源(輸入源)
以前的分法
- Port-Based Sources
- Custom Input Sources
- Cocoa Perform Selector Sources
現在的分法
- Source0:只包含了一個回撥(函式指標),它並不能主動觸發事件。使用時,你需要先呼叫
CFRunLoopSourceSignal(source)
,將這個 Source 標記為待處理,然後手動呼叫CFRunLoopWakeUp(runloop)
來喚醒 RunLoop,讓其處理這個事件。 - Source1:包含了一個
mach_port
和一個回撥(函式指標),被用於通過核心和其他執行緒相互傳送訊息
可以以下程式碼新增新的CFRunLoopSourceRef:
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
CFRunLoopTimerRef
CFRunLoopTimerRef
是基於時間的觸發器,它和 NSTimer
是toll-free bridged
的,可以混用。其包含一個時間長度和一個回撥(函式指標)。當其加入到 RunLoop
時,RunLoop
會註冊對應的時間點,當時間點到時,RunLoop
會被喚醒以執行那個回撥。
-
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];
//能夠在預設模式或者滑動模式中回撥這個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,不然這個函式結束Timer就被釋放掉了
self.timer = timer;
}
CFRunLoopObserverRef
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的Mode
上面的 Source/Timer/Observer
被統稱為 mode item
,一個item
可以被同時加入多個mode
。但一個 item
被重複加入同一個mode
時是不會有效果的。如果一個 mode
中一個 item
都沒有,則 RunLoop
會直接退出,不進入迴圈。
CFRunLoopMode 和 CFRunLoop 的結構大致如下:
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平時所處的狀態 -
UITrackingRunLoopMode
:這是追蹤ScrollView
滑動時的狀態 -
NSRunLoopCommonModes
:能夠將mode item
r新增到__CFRunLoop
裡面的_commonModeItems
中,然後RunLoop會將會自動將這個mode item
同步到具有 "Common"標記的所有Mode(NSDefaultRunLoopMode和UITrackingRunLoopMode)
裡。
RunLoop的內部邏輯
其內部程式碼整理如下:
/// 用DefaultMode啟動
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}
/// 用指定的Mode啟動,允許設定RunLoop超時時間
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
/// RunLoop的實現
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
/// 首先根據modeName找到對應mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
/// 如果mode裡沒有source/timer/observer, 直接返回。
if (__CFRunLoopModeIsEmpty(currentMode)) return;
/// 1. 通知 Observers: RunLoop 即將進入 loop。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
/// 內部函式,進入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即將觸發 Timer 回撥。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即將觸發 Source0 (非port) 回撥。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 執行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 觸發 Source0 (非port) 回撥。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 執行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基於port) 處於 ready 狀態,直接處理這個 Source1 然後跳轉去處理訊息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的執行緒即將進入休眠(sleep)。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 呼叫 mach_msg 等待接受 mach_port 的訊息。執行緒將進入休眠, 直到被下面某一個事件喚醒。
/// • 一個基於 port 的Source 的事件。
/// • 一個 Timer 到時間了
/// • RunLoop 自身的超時時間到了
/// • 被其他什麼呼叫者手動喚醒
__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 到時間了,觸發這個Timer的回撥。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block,執行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一個 Source1 (基於port) 發出事件了,處理這個事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 執行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 進入loop時引數說處理完事件就返回。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出傳入引數標記的超時時間了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部呼叫者強制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一個都沒有了
retVal = kCFRunLoopRunFinished;
}
/// 如果沒超時,mode裡沒空,loop也沒被停止,那繼續loop。
} while (retVal == 0);
}
/// 10. 通知 Observers: RunLoop 即將退出。
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}
可以看到,實際上RunLoop
就是這樣一個函式,其內部是一個 do-while
迴圈。當你呼叫CFRunLoopRun()
時,執行緒就會一直停留在這個迴圈裡;直到超時或被手動停止,該函式才會返回。
應用
使用執行緒的定時器
Cocoa
中使用任何performSelector…
的方法自動釋放池
App啟動後,蘋果在主執行緒
RunLoop
裡註冊了兩個Observer
,其回撥都是_wrapRunLoopWithAutoreleasePoolHandler()
。第一個
Observer
監視的事件是 Entry(即將進入Loop),其回撥內會呼叫_objc_autoreleasePoolPush()
建立自動釋放池。其 order 是-2147483647
,優先順序最高,保證建立釋放池發生在其他所有回撥之前。
第二個 Observer 監視了兩個事件: BeforeWaiting
(準備進入休眠) 時呼叫_objc_autoreleasePoolPop()
和 _objc_autoreleasePoolPush()
釋放舊的池並建立新池;Exit(即將退出Loop) 時呼叫 _objc_autoreleasePoolPop()
來釋放自動釋放池。這個 Observer
的 order 是 2147483647
,優先順序最低,保證其釋放池子發生在其他所有回撥之後。
在主執行緒執行的程式碼,通常是寫在諸如事件回撥、Timer
回撥內的。這些回撥會被 RunLoop
建立好的 AutoreleasePool
環繞著,所以不會出現記憶體洩漏,開發者也不必顯示建立 Pool
了。
- 建立一個常駐執行緒
開啟一個常駐執行緒(讓一個子執行緒不進入消亡狀態,等待其他執行緒發來訊息,處理其他事件),可以在這個執行緒裡面開啟一個定時器或者進行一些長期監控
通過方法[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
在RunLoop中新增至少一個Source就能夠讓執行緒常駐
- 可以新增
Observer
監聽RunLoop
的狀態,比如監聽點選事件的處理(在所有點選事件之前做一些事情)
參考資料
相關文章
- RunLoop總結與面試OOP面試
- RunLoop的學習OOP
- 學習 CodeWhisperer 的一些總結
- sqlldr的學習與總結SQL
- 爬蟲學習中的一些總結爬蟲
- Elasticsearch 一些命令彙總 以及學習總結Elasticsearch
- 學習PHP程式語言的一些總結PHP
- 深入學習之連結與總結
- JS/JSP學習的一些問題總結JS
- iOS底層原理總結 – RunLoopiOSOOP
- iOS底層原理總結 - RunLoopiOSOOP
- iOS Runloop學習筆記iOSOOP筆記
- 關於學習 Linux 系統結構的一些總結Linux
- 繼續學習webpack4的一些配置總結Web
- Hadoop平臺學習過程的一些總結Hadoop
- 關於學習-Linux-系統結構的一些總結Linux
- JAVA基礎學習-數字與字串學習總結Java字串
- MongoDB的學習總結MongoDB
- docker的學習總結Docker
- pandas的學習總結
- 深度學習中卷積和池化的一些總結深度學習卷積
- 學習總結
- 一些Flex學習的連結Flex
- 學習前端近一年的反思與總結前端
- Solr學習總結(二)Solr的安裝與配置Solr
- Vue學習筆記 - 關於過渡效果的一些總結Vue筆記
- docker學習系列16使用過程的一些經驗總結Docker
- maven 學習總結(五)——聚合與繼承Maven繼承
- iOS學習之深入理解RunLoopiOSOOP
- mysqlimport學習總結MySqlImport
- Maven學習總結Maven
- MyBatis 學習總結MyBatis
- awk 學習總結
- JNI 學習總結
- tkinter學習總結
- SVG學習總結SVG
- vue學習總結Vue
- WorkFlow學習總結