面試題
- 講講 RunLoop,專案中有用到嗎?
- RunLoop內部實現邏輯?
- Runloop和執行緒的關係?
- timer 與 Runloop 的關係?
- 程式中新增每3秒響應一次的NSTimer,當拖動tableview時timer可能無法響應要怎麼解決?
- Runloop 是怎麼響應使用者操作的, 具體流程是什麼樣的?
- 說說RunLoop的幾種狀態?
- Runloop的mode作用是什麼?
一. RunLoop簡介
執行迴圈,在程式執行過程中迴圈做一些事情,如果沒有Runloop程式執行完畢就會立即退出,如果有Runloop程式會一直執行,並且時時刻刻在等待使用者的輸入操作。RunLoop可以在需要的時候自己跑起來執行,在沒有操作的時候就停下來休息。充分節省CPU資源,提高程式效能。
二. RunLoop基本作用:
- 保持程式持續執行,程式一啟動就會開一個主執行緒,主執行緒一開起來就會跑一個主執行緒對應的RunLoop,RunLoop保證主執行緒不會被銷燬,也就保證了程式的持續執行
- 處理App中的各種事件(比如:觸控事件,定時器事件,Selector事件等)
- 節省CPU資源,提高程式效能,程式執行起來時,當什麼操作都沒有做的時候,RunLoop就告訴CPU,現在沒有事情做,我要去休息,這時CPU就會將其資源釋放出來去做其他的事情,當有事情做的時候RunLoop就會立馬起來去做事情
我們先通過API內一張圖片來簡單看一下RunLoop內部執行原理通過圖片可以看出,RunLoop在跑圈過程中,當接收到Input sources 或者 Timer sources時就會交給對應的處理方去處理。當沒有事件訊息傳入的時候,RunLoop就休息了。這裡只是簡單的理解一下這張圖,接下來我們來了解RunLoop物件和其一些相關類,來更深入的理解RunLoop執行流程。
三. RunLoop在哪裡開啟
UIApplicationMain函式內啟動了Runloop,程式不會馬上退出,而是保持執行狀態。因此每一個應用必須要有一個runloop,
我們知道主執行緒一開起來,就會跑一個和主執行緒對應的RunLoop,那麼RunLoop一定是在程式的入口main函式中開啟。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
複製程式碼
進入UIApplicationMain
UIKIT_EXTERN int UIApplicationMain(int argc, char *argv[], NSString * __nullable principalClassName, NSString * __nullable delegateClassName);
複製程式碼
我們發現它返回的是一個int數,那麼我們對main函式做一些修改
int main(int argc, char * argv[]) {
@autoreleasepool {
NSLog(@"開始");
int re = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"結束");
return re;
}
}
複製程式碼
執行程式,我們發現只會列印開始,並不會列印結束,這說明在UIApplicationMain函式中,開啟了一個和主執行緒相關的RunLoop,導致UIApplicationMain不會返回,一直在執行中,也就保證了程式的持續執行。
我們來看到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);
}
複製程式碼
我們發現RunLoop確實是do while通過判斷result的值實現的。因此,我們可以把RunLoop看成一個死迴圈。如果沒有RunLoop,UIApplicationMain函式執行完畢之後將直接返回,也就沒有程式持續執行一說了。
四. RunLoop物件
Fundation框架 (基於CFRunLoopRef的封裝)
NSRunLoop物件
CoreFoundation
CFRunLoopRef物件
因為Fundation框架是基於CFRunLoopRef的一層OC封裝,這裡我們主要研究CFRunLoopRef原始碼
如何獲得RunLoop物件
Foundation
[NSRunLoop currentRunLoop]; // 獲得當前執行緒的RunLoop物件
[NSRunLoop mainRunLoop]; // 獲得主執行緒的RunLoop物件
Core Foundation
CFRunLoopGetCurrent(); // 獲得當前執行緒的RunLoop物件
CFRunLoopGetMain(); // 獲得主執行緒的RunLoop物件
複製程式碼
五. RunLoop和執行緒間的關係
- 每條執行緒都有唯一的一個與之對應的RunLoop物件
- RunLoop儲存在一個全域性的Dictionary裡,執行緒作為key,RunLoop作為value
- 主執行緒的RunLoop已經自動建立好了,子執行緒的RunLoop需要主動建立
- RunLoop在第一次獲取時建立,線上程結束時銷燬
通過原始碼檢視上述對應
// 拿到當前Runloop 呼叫_CFRunLoopGet0
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
// 檢視_CFRunLoopGet0方法內部
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 根據傳入的主執行緒獲取主執行緒對應的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 儲存主執行緒 將主執行緒-key和RunLoop-Value儲存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 從字典裡面拿,將執行緒作為key從字典裡獲取一個loop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果loop為空,則建立一個新的loop,所以runloop會在第一次獲取的時候建立
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 建立好之後,以執行緒為key runloop為value,一對一儲存在字典中,下次獲取的時候,則直接返回字典內的runloop
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don`t release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
複製程式碼
從上面的程式碼可以看出,執行緒和 RunLoop 之間是一一對應的,其關係是儲存在一個 Dictionary 裡。所以我們建立子執行緒RunLoop時,只需在子執行緒中獲取當前執行緒的RunLoop物件即可[NSRunLoop currentRunLoop];
如果不獲取,那子執行緒就不會建立與之相關聯的RunLoop,並且只能在一個執行緒的內部獲取其 RunLoop
[NSRunLoop currentRunLoop];
方法呼叫時,會先看一下字典裡有沒有存子執行緒相對用的RunLoop,如果有則直接返回RunLoop,如果沒有則會建立一個,並將與之對應的子執行緒存入字典中。當執行緒結束時,RunLoop會被銷燬。
六. RunLoop結構體
通過原始碼我們找到__CFRunLoop結構體
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
複製程式碼
除一些記錄性屬性外,主要來看一下以下兩個成員變數
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
複製程式碼
CFRunLoopModeRef 其實是指向__CFRunLoopMode結構體的指標,__CFRunLoopMode結構體原始碼如下
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
複製程式碼
主要檢視以下成員變數
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
複製程式碼
通過上面分析我們知道,CFRunLoopModeRef代表RunLoop的執行模式,一個RunLoop包含若干個Mode,每個Mode又包含若干個Source0/Source1/Timer/Observer,而RunLoop啟動時只能選擇其中一個Mode作為currentMode。
Source1/Source0/Timers/Observer分別代表什麼
1. Source1 : 基於Port的執行緒間通訊
2. Source0 : 觸控事件,PerformSelectors
我們通過程式碼驗證一下
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"點選了螢幕");
}
複製程式碼
打斷點之後列印堆疊資訊,當xcode工具區列印的堆疊資訊不全時,可以在控制檯通過“bt”指令列印完整的堆疊資訊,由堆疊資訊中可以發現,觸控事件確實是會觸發Source0事件。
同樣的方式驗證performSelector堆疊資訊
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
});
複製程式碼
可以發現PerformSelectors同樣是觸發Source0事件
其實,當我們觸發了事件(觸控/鎖屏/搖晃等)後,由IOKit.framework生成一個 IOHIDEvent事件,而IOKit是蘋果的硬體驅動框架,由它進行底層介面的抽象封裝與系統進行互動傳遞硬體感應的事件,並專門處理使用者互動裝置,由IOHIDServices和IOHIDDisplays兩部分組成,其中IOHIDServices是專門處理使用者互動的,它會將事件封裝成IOHIDEvents物件,接著用mach port轉發給需要的App程式,隨後 Source1就會接收IOHIDEvent,之後再回撥__IOHIDEventSystemClientQueueCallback(),__IOHIDEventSystemClientQueueCallback()內觸發Source0,Source0 再觸發 _UIApplicationHandleEventQueue()。所以觸控事件看到是在 Source0 內的。
3. Timers : 定時器,NSTimer
通過程式碼驗證
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"NSTimer ---- timer呼叫了");
}];
複製程式碼
列印完整堆疊資訊
4. Observer : 監聽器,用於監聽RunLoop的狀態
七. 詳解RunLoop相關類及作用
通過上面的分析,我們對RunLoop內部結構有了大致的瞭解,接下來來詳細分析RunLoop的相關類。以下為Core Foundation中關於RunLoop的5個類
CFRunLoopRef – 獲得當前RunLoop和主RunLoop
CFRunLoopModeRef – RunLoop 執行模式,只能選擇一種,在不同模式中做不同的操作
CFRunLoopSourceRef – 事件源,輸入源
CFRunLoopTimerRef – 定時器時間
CFRunLoopObserverRef – 觀察者
1. CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的執行模式
一個 RunLoop 包含若干個 Mode,每個Mode又包含若干個Source、Timer、Observer
每次RunLoop啟動時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode
如果需要切換Mode,只能退出RunLoop,再重新指定一個Mode進入,這樣做主要是為了分隔開不同組的Source、Timer、Observer,讓其互不影響。如果Mode裡沒有任何Source0/Source1/Timer/Observer,RunLoop會立馬退出
如圖所示:
注意:一種Mode中可以有多個Source(事件源,輸入源,基於埠事件源例鍵盤觸控等) Observer(觀察者,觀察當前RunLoop執行狀態) 和Timer(定時器事件源)。但是必須至少有一個Source或者Timer,因為如果Mode為空,RunLoop執行到空模式不會進行空轉,就會立刻退出。
系統預設註冊的5個Mode:
RunLoop 有五種執行模式,其中常見的有1.2兩種
1. kCFRunLoopDefaultMode:App的預設Mode,通常主執行緒是在這個Mode下執行
2. UITrackingRunLoopMode:介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響
3. UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用,會切換到kCFRunLoopDefaultMode
4. GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到
5. kCFRunLoopCommonModes: 這是一個佔位用的Mode,作為標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,並不是一種真正的Mode
複製程式碼
Mode間的切換
我們平時在開發中一定遇到過,當我們使用NSTimer每一段時間執行一些事情時滑動UIScrollView,NSTimer就會暫停,當我們停止滑動以後,NSTimer又會重新恢復的情況,我們通過一段程式碼來看一下
程式碼中的註釋也很重要,展示了我們探索的過程
-(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]);
}
-(void)show
{
NSLog(@"-------");
}
複製程式碼
由上述程式碼可以看出,NSTimer不管用是因為Mode的切換,因為如果我們在主執行緒使用定時器,此時RunLoop的Mode為kCFRunLoopDefaultMode,即定時器屬於kCFRunLoopDefaultMode,那麼此時我們滑動ScrollView時,RunLoop的Mode會切換到UITrackingRunLoopMode,因此在主執行緒的定時器就不在管用了,呼叫的方法也就不再執行了,當我們停止滑動時,RunLoop的Mode切換回kCFRunLoopDefaultMode,所以NSTimer就又管用了。
同樣道理的還有ImageView的顯示,我們直接來看程式碼,不再贅述了
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"%s",__func__);
// performSelector預設是在default模式下執行,因此在滑動ScrollView時,圖片不會載入
// [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 ];
// inModes: 傳入Mode陣列
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];
複製程式碼
使用GCD也可是建立計時器,而且更為精確我們來看一下程式碼
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//建立佇列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//1.建立一個GCD定時器
/*
第一個引數:表明建立的是一個定時器
第四個引數:佇列
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 需要對timer進行強引用,保證其不會被釋放掉,才會按時呼叫block塊
// 區域性變數,讓指標強引用
self.timer = timer;
//2.設定定時器的開始時間,間隔時間,精準度
/*
第1個引數:要給哪個定時器設定
第2個引數:開始時間
第3個引數:間隔時間
第4個引數:精準度 一般為0 在允許範圍內增加誤差可提高程式的效能
GCD的單位是納秒 所以要*NSEC_PER_SEC
*/
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
//3.設定定時器要執行的事情
dispatch_source_set_event_handler(timer, ^{
NSLog(@"---%@--",[NSThread currentThread]);
});
// 啟動
dispatch_resume(timer);
}
複製程式碼
2. CFRunLoopSourceRef事件源(輸入源)
Source分為兩種
Source0:非基於Port的 用於使用者主動觸發的事件(點選button 或點選螢幕)
Source1:基於Port的 通過核心和其他執行緒相互傳送訊息(與核心相關)
觸控事件及PerformSelectors會觸發Source0事件源在前文已經驗證過,這裡不在贅述
3. CFRunLoopObserverRef
CFRunLoopObserverRef是觀察者,能夠監聽RunLoop的狀態改變
我們直接來看程式碼,給RunLoop新增監聽者,監聽其執行狀態
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//建立監聽者
/*
第一個引數 CFAllocatorRef allocator:分配儲存空間 CFAllocatorGetDefault()預設分配
第二個引數 CFOptionFlags activities:要監聽的狀態 kCFRunLoopAllActivities 監聽所有狀態
第三個引數 Boolean repeats:YES:持續監聽 NO:不持續
第四個引數 CFIndex order:優先順序,一般填0即可
第五個引數 :回撥 兩個引數observer:監聽者 activity:監聽的事件
*/
/*
所有事件
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新增監聽者
/*
第一個引數 CFRunLoopRef rl:要監聽哪個RunLoop,這裡監聽的是主執行緒的RunLoop
第二個引數 CFRunLoopObserverRef observer 監聽者
第三個引數 CFStringRef mode 要監聽RunLoop在哪種執行模式下的狀態
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
/*
CF的記憶體管理(Core Foundation)
凡是帶有Create、Copy、Retain等字眼的函式,建立出來的物件,都需要在最後做一次release
GCD本來在iOS6.0之前也是需要我們釋放的,6.0之後GCD已經納入到了ARC中,所以我們不需要管了
*/
CFRelease(observer);
}
複製程式碼
我們來看一下輸出
以上可以看出,Observer確實用來監聽RunLoop的狀態,包括喚醒,休息,以及處理各種事件。
八. RunLoop處理邏輯
這時我們再來分析RunLoop的處理邏輯,就會簡單明瞭很多,現在回頭看官方文件RunLoop的處理邏輯,對RunLoop的處理邏輯有新的認識。
原始碼解析
下面原始碼僅保留了主流程程式碼
// 共外部呼叫的公開的CFRunLoopRun方法,其內部會呼叫CFRunLoopRunSpecific
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
// 經過精簡的 CFRunLoopRunSpecific 函式程式碼,其內部會呼叫__CFRunLoopRun函式
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
// 通知Observers : 進入Loop
// __CFRunLoopDoObservers內部會呼叫 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
函式
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 核心的Loop邏輯
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知Observers : 退出Loop
if (currentMode->_observerMask & kCFRunLoopExit ) __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,就跳轉到handle_msg標記處
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
// 通知Observers:即將休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 進入休眠,等待其他訊息喚醒
__CFRunLoopSetSleeping(rl);
__CFPortSetInsert(dispatchPort, waitSet);
do {
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
} while (1);
// 醒來
__CFPortSetRemove(dispatchPort, waitSet);
__CFRunLoopUnsetSleeping(rl);
// 通知Observers:已經喚醒
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg: // 看看是誰喚醒了RunLoop,進行相應的處理
if (被Timer喚醒的) {
// 處理Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
}
else if (被GCD喚醒的) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被Sources1喚醒的
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
}
// 執行Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 根據之前的執行結果,來決定怎麼做,為retVal賦相應的值
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;
}
複製程式碼
上述原始碼中,相應處理事件函式內部還會呼叫更底層的函式,內部呼叫才是真正處理事件的函式,通過上面bt列印全部堆疊資訊也可以得到驗證。
__CFRunLoopDoObservers 內部呼叫 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
__CFRunLoopDoBlocks 內部呼叫 __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRunLoopDoSources0 內部呼叫 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRunLoopDoTimers 內部呼叫 __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
GCD 呼叫 __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRunLoopDoSource1 內部呼叫 __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
RunLoop處理邏輯流程圖
此時我們按照原始碼重新整理一下RunLoop處理邏輯就會很清晰
九. RunLoop退出
- 主執行緒銷燬RunLoop退出
- Mode中有一些Timer 、Source、 Observer,這些保證Mode不為空時保證RunLoop沒有空轉並且是在執行的,當Mode中為空的時候,RunLoop會立刻退出
- 我們在啟動RunLoop的時候可以設定什麼時候停止
[NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>
複製程式碼
十. RunLoop應用
1. 常駐執行緒
常駐執行緒的作用:我們知道,當子執行緒中的任務執行完畢之後就被銷燬了,那麼如果我們需要開啟一個子執行緒,在程式執行過程中永遠都存在,那麼我們就會面臨一個問題,如何讓子執行緒永遠活著,這時就要用到常駐執行緒:給子執行緒開啟一個RunLoop
注意:子執行緒執行完操作之後就會立即釋放,即使我們使用強引用引用子執行緒使子執行緒不被釋放,也不能給子執行緒再次新增操作,或者再次開啟。
子執行緒開啟RunLoop的程式碼,先點選螢幕開啟子執行緒並開啟子執行緒RunLoop,然後點選button。
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic,strong)NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 建立子執行緒並開啟
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
self.thread = thread;
[thread start];
}
-(void)show
{
// 注意:列印方法一定要在RunLoop建立開始執行之前,如果在RunLoop跑起來之後列印,RunLoop先執行起來,已經在跑圈了就出不來了,進入死迴圈也就無法執行後面的操作了。
// 但是此時點選Button還是有操作的,因為Button是在RunLoop跑起來之後加入到子執行緒的,當Button加入到子執行緒RunLoop就會跑起來
NSLog(@"%s",__func__);
// 1.建立子執行緒相關的RunLoop,在子執行緒中建立即可,並且RunLoop中要至少有一個Timer 或 一個Source 保證RunLoop不會因為空轉而退出,因此在建立的時候直接加入
// 新增Source [NSMachPort port] 新增一個埠
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 新增一個Timer
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//建立監聽者
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新增監聽者
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 2.子執行緒需要開啟RunLoop
[[NSRunLoop currentRunLoop]run];
CFRelease(observer);
}
- (IBAction)btnClick:(id)sender {
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
-(void)test
{
NSLog(@"%@",[NSThread currentThread]);
}
@end
複製程式碼
注意:建立子執行緒相關的RunLoop,在子執行緒中建立即可,並且RunLoop中要至少有一個Timer 或 一個Source 保證RunLoop不會因為空轉而退出,因此在建立的時候直接加入,如果沒有加入Timer或者Source,或者只加入一個監聽者,執行程式會崩潰
2. 自動釋放池
Timer和Source也是一些變數,需要佔用一部分儲存空間,所以要釋放掉,如果不釋放掉,就會一直積累,佔用的記憶體也就越來越大,這顯然不是我們想要的。
那麼什麼時候釋放,怎麼釋放呢?
RunLoop內部有一個自動釋放池,當RunLoop開啟時,就會自動建立一個自動釋放池,當RunLoop在休息之前會釋放掉自動釋放池的東西,然後重新建立一個新的空的自動釋放池,當RunLoop被喚醒重新開始跑圈時,Timer,Source等新的事件就會放到新的自動釋放池中,當RunLoop退出的時候也會被釋放。
注意:只有主執行緒的RunLoop會預設啟動。也就意味著會自動建立自動釋放池,子執行緒需要線上程排程方法中手動新增自動釋放池。
@autorelease{
// 執行程式碼
}
複製程式碼
NSTimer、ImageView顯示、PerformSelector等在上面已經有過例子,這裡不再贅述。
最後檢驗一下自己
文章開頭的面試題,在文中都可以找到答案,這裡不在贅述了。
文獻資料
文中如果有不對的地方歡迎指出。我是xx_cc,一隻長大很久但還沒有二夠的傢伙。需要視訊一起探討學習的coder可以加我Q:2336684744