什麼是runLoop
runLoop實際上是一個物件,這個物件管理了需要處理的事件和訊息,並提供了一個入口函式來執行Event Loop的邏輯。執行緒執行了這個入口函式後,函式內部會一直處在: 接收訊息 -> 等待 -> 處理,知道迴圈結束<傳去quit等>,函式返回
Event Loop
function loop() {
initialize();
do {
var message = get_next_message();
process_message(message);
} while (message != quit);
}
複製程式碼
Event Loop的關鍵點在於:如何管理事件、訊息,如何讓執行緒在沒有出來訊息時休眠來減少資源佔用,在訊息到來時立即喚醒。
runLoop和執行緒的關係
蘋果不允許直接建立RunLoop,只提供了函式: CFRunLoopGenMain()、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對應一個執行緒,只有在主動獲取runloop的時候才會線上程中建立它,線上程銷燬時一起銷燬。
RunLoop的對外介面
在CoreFoundation中RunLoop有5個類:
CFRunLoopRef
CFRunLoopModeRef<未對外暴露,通過CFRunLoopRef封裝>
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserveRef
/*
他們關係:RunLoop之下包含多個Mode,每個Mode中都包含若干個Source,Timer,Observe。呼叫RunLoop的主函式時,只能指定一個Mode,這個Mode被稱作CurrentMode,若想切換Mode只能退出Loop再重新指定一個Mode進入。
*/
複製程式碼
CFRunLoopSourceRef
CFRunLoopSourceRef:是事件產生的地方,有兩個版本:source0、source1.
- source0:只包含一個回撥(函式指標),他不能主動觸發事件。使用時,要先呼叫CFRunLoopSourceSignal(source)方法將source標記為待處理,後手動呼叫CFRunLoopSourcWeakUp(runloop)來喚醒Runloop來處理source。
- source1:包含一個mach_port和回撥。被用於通過核心和其他執行緒互相傳送訊息,能主動喚醒RunLoop的執行緒。
CFRunLoopTimerRef
CFRunLoopTimerRef:是基於事件的觸發器,他和NStimer是toll-free-bridged<能夠在Core Foundation和Foundation之間互換使用,這意味著同一資料型別即可以作為Core Foundation函式的引數,也可作為接收者向其傳送Objective-C訊息>。包含一個事件長度和回撥,當其加入到RunLoop時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回撥。
CFRunLoopObserveRef
CFRunLoopObserveRef:觀察者,包含了一個回撥,,當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
};
複製程式碼
CFRunLoopModeRef
CFRunLoopModeRef和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
...
};
複製程式碼
CommonModes:通過方法CFRunLoop對外暴露管理Mode介面:
// 新增mode<只能新增不能刪除>
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
// 用指定的Mode啟動,允許設定最大時間(假無線迴圈),執行完是否退出
CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle);
複製程式碼
Mode暴露管理的mode items介面:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
複製程式碼
蘋果提供的Mode:
/*
1. kCFRunLoopDefaultMode:App的預設Mode,通常主執行緒是在這個Mode下執行
2. UITrackingRunLoopMode:介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響
3. UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用
4. GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到
5. kCFRunLoopCommonModes: 這是一個佔位用的Mode,作為標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,並不是一種真正的Mode
*/
複製程式碼
/*使用*/
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
// 加入到RunLoop中才可以執行
// 1. 把定時器新增到RunLoop中,並且選擇預設執行模式NSDefaultRunLoopMode = kCFRunLoopDefaultMode
// [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 當textFiled滑動的時候,timer失效,停止滑動時,timer恢復
// 原因:當textFiled滑動的時候,RunLoop的Mode會自動切換成UITrackingRunLoopMode模式,因此timer失效,當停止滑動,RunLoop又會切換回NSDefaultRunLoopMode模式,因此timer又會重新啟動了
// 2. 當我們將timer新增到UITrackingRunLoopMode模式中,此時只有我們在滑動textField時timer才會執行
// [[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 3. 那個如何讓timer在兩個模式下都可以執行呢?
// 3.1 在兩個模式下都新增timer 是可以的,但是timer新增了兩次,並不是同一個timer
// 3.2 使用站位的執行模式 NSRunLoopCommonModes標記,凡是被打上NSRunLoopCommonModes標記的都可以執行,下面兩種模式被打上標籤
//0 : <CFString 0x10b7fe210 [0x10a8c7a40]>{contents = "UITrackingRunLoopMode"}
//2 : <CFString 0x10a8e85e0 [0x10a8c7a40]>{contents = "kCFRunLoopDefaultMode"}
// 因此也就是說如果我們使用NSRunLoopCommonModes,timer可以在UITrackingRunLoopMode,kCFRunLoopDefaultMode兩種模式下執行
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
NSLog(@"%@",[NSRunLoop mainRunLoop]);
}
複製程式碼
RunLoop的內部呼叫
1. 通知Observer:即將進入Loop
2. 通知Observe:將要處理Timer
3. 處理Source0
4. 有Source1,跳到第9步
6. 通知Observe:執行緒即將休眠
7. 休眠,等待喚醒-------------------------a.source0(port) b.timer c.外部
8. 通知Observer:執行緒剛被喚醒
9. 處理喚醒收到的訊息,跳回2
10. 通知Observer:即將退出loop
{
/// 1. 通知Observers,即將進入RunLoop
/// 此處有Observer會建立AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即將觸發 Timer 回撥。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即將觸發 Source (非基於port的,Source0) 回撥。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 觸發 Source0 (非基於port的) 回撥。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即將進入休眠
/// 此處有Observer釋放並新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,執行緒被喚醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer喚醒的,回撥Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch喚醒的,執行所有呼叫 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基於port的) 的事件喚醒了,處理這個事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即將退出RunLoop
/// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
複製程式碼
RunLoop 底層
RunLoop的核心是基於mach_port(),其休眠呼叫的函式是mach_msg()。
應用
事件響應
手勢識別
定時器
PerformSelector
GCD
GCD中呼叫到dispatch_get_main_queue()方法時會呼叫到RunLoop的方法