今天同事問我RunLoop的知識。說他平時會用但是有些地方還是不清楚,所以我特意整理了一下,因為網(wǎng)上關于RunLoop的講解很多,這里我寫的是個人對RunLoop的理解,希望寫的易懂一些讓更多的人明白。
RunLoop基本作用
- 保持程序的持續(xù)運行
- 處理App中的各種事件(比如觸摸事件、定時器事件、Selector事件)
- 節(jié)省CPU資源,提高程序性能:該做事時做事,該休息時休息
程序一開始系統(tǒng)就為主線程創(chuàng)建了一個RunLoop,這也是一個app能一直運行的原因。
下面兩句話很重要
- 每條線程都有唯一的一個與之對應的RunLoop對象
- 主線程的RunLoop已經(jīng)自動創(chuàng)建好了,子線程的RunLoop需要主動創(chuàng)建
不管是主線程還是子線程都可以有個RunLoop 而且只能有一個。主線程的RunLoop在程序開始的時候已經(jīng)創(chuàng)建好了,子線程的RunLoop需要我們自己去創(chuàng)建。
獲取RunLoop的方式
//Foundation
[NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
//Core Foundation
CFRunLoopGetCurrent(); // 獲得當前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象
蘋果不允許直接創(chuàng)建 RunLoop,它只提供了兩個自動獲取的函數(shù):NSRunLoop currentRunLoop( CFRunLoopGetMain( ) )和 NSRunLoop mainRunLoop( CFRunLoopGetCurrent( ) )。這兩個函數(shù)內部的邏輯大概是下面這樣:
/// 全局的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,并先為主線程創(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);
/// 注冊一個回調,當線程銷毀時,順便也銷毀其對應的 RunLoop。
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
以上代碼來源網(wǎng)絡
子線程剛創(chuàng)建時并沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時,RunLoop 的銷毀是發(fā)生在線程結束時。
CFRunLoopModeRefbo(Mode)
RunLoop的Mode有五種:
- kCFRunLoopDefaultMode:App的默認Mode,通常主線程是在這個Mode下運行
- **UITrackingRunLoopMode **:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
- **UIInitializationRunLoopMode **: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用(系統(tǒng)用)
- **GSEventReceiveRunLoopMode **: 接受系統(tǒng)事件的內部 Mode,通常用不到(系統(tǒng)用)
- **kCFRunLoopCommonModes **: 這是一個占位用的Mode,不是一種真正的Mode(用來占位)
ps:其實kCFRunLoopCommonModes標記了UITrackingRunLoopMode和kCFRunLoopDefaultMode兩種模式,這里大家先了解一下后面會講到。

看上圖,這張圖描述的很清楚一個RunLoop可以包含多個Mode。
請務必記住下面的幾句話?。。。。?/h3>
CFRunLoopModeRef代表RunLoop的運行模式
一個 RunLoop 包含若干個 Mode,每個Mode又包含若干個Source/Timer/Observer
每次RunLoop啟動時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode
如果需要切換Mode,只能退出Loop,再重新指定一個Mode進入
這樣做主要是為了分隔開不同組的Source/Timer/Observer,讓其互不影響
CFRunLoopModeRef代表RunLoop的運行模式
一個 RunLoop 包含若干個 Mode,每個Mode又包含若干個Source/Timer/Observer
每次RunLoop啟動時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode
如果需要切換Mode,只能退出Loop,再重新指定一個Mode進入
這樣做主要是為了分隔開不同組的Source/Timer/Observer,讓其互不影響
下面我們來舉個例子
[NSTimer scheduledTimerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>];

這個方法是創(chuàng)建一個NSTimer,并把它加入到RunLoopDefaultMode 模式下的RunLoop中。
NSTimer *time=[NSTimer timerWithTimeInterval:<#(NSTimeInterval)#> target:<#(nonnull id)#> selector:<#(nonnull SEL)#> userInfo:<#(nullable id)#> repeats:<#(BOOL)#>]
而這個方法只是創(chuàng)建了一個NSTimer
我們可以把這個timer加到一個RunLoop中
[[NSRunLoop currentRunLoop] addTimer:time forMode:NSDefaultRunLoopMode];
這樣也可以實現(xiàn)上面代碼實現(xiàn)的目的:創(chuàng)建一個NSTimer,并把它加入到RunLoopDefaultMode 模式下的RunLoop中。
現(xiàn)在有這樣一個場景,添加一個定時器并把它加入到NSDefaultRunLoopMode,定時器正常運行,這時屏幕開始滑動,你會發(fā)現(xiàn)你的定時器不管用了,這個為什么呢?
這是因為在進行滑動的時候RunLoop進入了UITrackingRunLoopMode(忘記的請查看RunLoop的Mode)而你的time是在:NSDefaultRunLoopMode模式下的所以失效了當手一松開又進入了NSDefaultRunLoopMode,定時器又可以工作了現(xiàn)在該怎么辦呢?
接下來我們按住NSDefaultRunLoopMode進入頭文件看看發(fā)現(xiàn)
FOUNDATION_EXPORT NSString * const NSDefaultRunLoopMode;
FOUNDATION_EXPORT NSString * const NSRunLoopCommonModes NS_AVAILABLE(10_5, 2_0);
里面怎么就兩個Mode,而且有一個還是用來占位的,別急我們打印一下在NSRunLoopCommonModes的RunLoop看看它的Mode是什么
**common modes = <CFBasicHash 0x7f8009e035e0 [0x1095c5a40]>{type = mutable set, count = 2,**
**entries =>**
** 0 : <CFString 0x10a4fc210 [0x1095c5a40]>{contents = "UITrackingRunLoopMode"}**
** 2 : <CFString 0x1095e65e0 [0x1095c5a40]>{contents = "kCFRunLoopDefaultMode"}**
**}**
注意看**common modes **包含UITrackingRunLoopMode和kCFRunLoopDefaultMode
所以當你有一個定時器,能讓它在屏幕滑動的情況下和正常情況下都還能使用就應該使用NSRunLoopCommonModes。
如果只讓定時器在屏幕滑動時刻起作用該怎么辦呢?
加入到UITrackingRunLoopMode模式下不就行了,哈哈。
學到這里大家應該知道一個RunLoop可以有多個Mode,但是在每次RunLoop啟動時,只能指定其中一個 Mode

如果感覺這篇文章對您有所幫助,順手點個喜歡,謝謝啦