基本概念
進程
進程是指在系統(tǒng)中正在運行的一個應(yīng)用程序,而且每個進程之間是獨立的,它們都運行在其專用且受保護的內(nèi)存空間內(nèi),比如同時打開迅雷、Xcode,系統(tǒng)就會分別啟動兩個進程。
線程
一個人進程如果想要執(zhí)行任務(wù),必須得有至少一條線程,進程的所有任務(wù)都會在線程中執(zhí)行,比如使用網(wǎng)易云音樂播放音樂,使用迅雷下載電影,都需要在線程中執(zhí)行。
主線程
iOS 程序運行后,系統(tǒng)會默認開啟一條線程,稱為“主線程”或者“UI 線程”,主線程是用來顯示/刷新 UI 界面,處理 UI 事件的。
簡介
運行循環(huán)、跑圈
總結(jié)下來,RunLoop 的作用主要體現(xiàn)在三方面:
保持程序的持續(xù)運行
處理App中的各種事件(比如觸摸事件、定時器事件、Selector事件)
節(jié)省CPU資源,提高程序性能:該做事的時候做事,該休息的時候休息
就是說,如果沒有 RunLoop 程序一運行就結(jié)束了,你根本不可能看到持續(xù)運行的 app。
iOS中有2套API訪問和使用RunLoop
Foundation:NSRunLoop
Core Foundation: CFRunLoopRef
NSRunLoop是基于CFRunLoopRef的一層OC包裝,因此我們需要研究CFRunLoopRef層面的API(Core Foundation層面)
關(guān)于 RunLoop 的源碼請看這里
RunLoop與線程
源碼中,關(guān)于創(chuàng)建線程的核心代碼如下:
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) { // 如果沒有線程,則要創(chuàng)建線程
__CFUnlock(&loopsLock);
// 創(chuàng)建一個可變字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 將主線程放進去,創(chuàng)建 RunLoop(也就是說,創(chuàng)建哪個線程的 RunLoop 需要將線程作為參數(shù)傳入)
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 將主線程的 RunLoop 和主線程以 key/value 的形式保存。
// 因此由此可以看出,一條線程和一個 RunLoop 是一一對應(yīng)的
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 當你輸入 cunrrentRunLoop 時,會通過當前線程這個 key,在字典中尋找對應(yīng)的 RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果沒有在字典中找到
if (!loop) {
// 則重新創(chuàng)建一個 RunLoop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
// 然后將 RunLoop 和線程以 key/value 的形式保存
// 再一次驗證了 RunLoop 和 key 是一一對應(yīng)的
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
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;
}
程序啟動時,系統(tǒng)會自動創(chuàng)建主線程的 RunLoop
每一條線程都有唯一的一個與之對應(yīng)的RunLoop對象
主線程的RunLoop已經(jīng)自動創(chuàng)建好了,子線程的RunLoop需要手動創(chuàng)建
RunLoop在第一次獲取時創(chuàng)建,在線程結(jié)束時銷毀
代碼:
// 獲取當前的線程的RunLoop對象,注意RunLoop是懶加載,currentRunLoop時會自動創(chuàng)建對象
[NSRunLoop currentRunLoop];
// 獲取主線程的RunLoop對象
[NSRunLoop mainRunLoop];
// 如果是 CF 層面
CFRunLoopGetCurrent();
CFRunLoopGetMain();
RunLoop相關(guān)類
通過:
NSLog(@"%@", [NSRunLoop mainRunLoop]);
可以對 RunLoop 內(nèi)部一覽無余
Core Foundation中關(guān)于RunLoop的5個類:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopObserverRef

RunLoop 想要跑起來,必須有 Mode 對象支持,而 Mode 里面必須有
(NSSet *)Source、(NSArray *)Timer,源和定時器。
至于另外一個類(NSArray *)observer是用于監(jiān)聽 RunLoop 的狀態(tài),因此不會激活RunLoop。
CFRunLoopModeRef
CFRunLoopModeRef 代表 RunLoop 的運行模式
每個 RunLoop 都包含若干個 Mode ,每個 Mode 又包含若干個 Source/Timer/Observer,每次
RunLoop 啟動時,只能指定其中一個 Mode,這個 Mode 被稱作CurrentMode,如果需要切換 Mode,只能退出
Loop,再重新指定一個 Mode 進入,這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響(可以通過切換
Mode,完成不同的 timer/source/observer)。
[NSRunLoop currentRunLoop].currentMode; // 獲取當前運行模式
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
系統(tǒng)默認注冊了5個Mode:
NSDefaultRunLoopMode:App 的默認 Mode,通常主線程是在這個 Mode 下運行(默認情況下運行)
UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響(操作 UI 界面的情況下運行)
UIInitializationRunLoopMode:在剛啟動 App 時進入的第一個 Mode,啟動完成后就不再使用
GSEventReceiveRunLoopMode:接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到(繪圖服務(wù))
NSRunLoopCommonModes:這是一個占位用的 Mode,不是一種真正的 Mode (RunLoop無法啟動該模式,設(shè)置這種模式下,默認和操作 UI 界面時線程都可以運行,但無法改變 RunLoop 同時只能在一種模式下運行的本質(zhì))
下面主要區(qū)別 NSDefaultRunLoopMode 和 UITrackingRunLoopMode 以及 NSRunLoopCommonModes。請看以下代碼:
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 在默認模式下添加的 timer 當我們拖拽 textView 的時候,不會運行 run 方法
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 在 UI 跟蹤模式下添加 timer 當我們拖拽 textView 的時候,run 方法才會運行
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// timer 可以運行在兩種模式下,相當于上面兩句代碼寫在一起
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)run
{
NSLog(@"--------run");
}
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES modes:@[UITrackingRunLoopMode]];
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:YES modes:@[NSRunLoopCommonModes]];
CFRunLoopTimerRef
CFRunLoopTimerRef 是基于事件的觸發(fā)器
CFRunLoopTimerRef 基本上就是 NSTimer,它受 RunLoop的Mode 影響
創(chuàng)建 Timer 有兩種方式,下面的這種方式必須手動添加到 RunLoop 中去才會被調(diào)用
// 這種方式創(chuàng)建的timer 必須手動添加到RunLoop中去才會被調(diào)用
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(time) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer
forMode:NSDefaultRunLoopMode];
// 同時讓RunLoop跑起來
[[NSRunLoop currentRunLoop] run];
而通過scheduledTimer創(chuàng)建 Timer 一開始就會自動被添加到當前線程并且以
NSDefaultRunLoopMode模式運行起來,代碼如下:
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
/*
注意:調(diào)用了 scheduledTimer 返回的定時器,已經(jīng)自動被添加到當前
runLoop 中,而且是 NSDefaultRunLoopMode ,想讓上述方法起作用,
必須先讓添加了上述 timer的RunLoop 對象 run 起來,通過
scheduledTimerWithTimeInterval 創(chuàng)建的 timer 可以通過以下方法修改 mode
*/
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode];
注意: GCD的定時器不受RunLoop的Mode影響
CADisplayLink *display = [CADisplayLink displayLinkWithTarget:self selector:@selector(run)];
/*
注意:CADisplayLink ,也是在 Runloop 下運行的,
有一個方法可以將CADisplayLink 對象添加到一個 Runloop 對象中去
*/
[display addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
CFRunLoopSourceRef
CFRunLoopSourceRef 其實是事件源(輸入源)
按照官方文檔,Source的分類
Port-Based Sources:基于端口的:跟其他線程進行交互的,Mac內(nèi)核發(fā)過來一些消息
Custom Input Sources:自定義輸入源
Cocoa Perform Selector Sources(self performSelector:...)
按照函數(shù)調(diào)用棧,Source的分類
Source0:非基于Port的(觸摸事件、按鈕點擊事件)
Source1:基于Port的,通過內(nèi)核和其他線程通信,接收分發(fā)系統(tǒng)事件
(觸摸硬件,通過 Source1 接收和分發(fā)系統(tǒng)事件到 Source0 處理)
為了搞清楚,Source 是如何通過函數(shù)調(diào)用棧來傳遞事件的,我們做如下實驗:

我們可以看到,從程序啟動 start 開始,函數(shù)調(diào)用棧在監(jiān)聽到事件點擊后,會一路往下,一直到-buttonClick:方法,中間會經(jīng)過CFRunLoopSource0,這說明我們的按鈕點擊事件是屬于 Source0 的。
而 Source1 是基于 Port 的,就是說,Source1 是和硬件交互的,觸摸首先在屏幕上被包裝成一個 event 事件,再通過 Source1 進行分發(fā)到 Source0,最后通過 Source0 進行處理。

CFRunLoopObserverRef
CFRunLoopObserverRef 是觀察者,能夠監(jiān)聽 RunLoop 的狀態(tài)改變,主要監(jiān)聽以下幾個時間節(jié)點:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity)
{
kCFRunLoopEntry = (1UL << 0), // 1 即將進入 Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 2 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 4 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 32 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 64 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 128 即將退出 Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 監(jiān)聽所有事件
};
// 1.創(chuàng)建觀察者 監(jiān)聽 RunLoop
// 參1: 有個默認值 CFAllocatorRef :CFAllocatorGetDefault()
// 參2: CFOptionFlags activities 監(jiān)聽RunLoop的活動 枚舉 見上面
// 參3: 重復(fù)監(jiān)聽 Boolean repeats YES
// 參4: CFIndex order 傳0
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
// 該方法可以在添加timer之前做一些事情,? 在添加source之前做一些事情
NSLog(@"%zd", activity);
});
// 2.添加觀察者,監(jiān)聽當前的RunLoop對象
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// CF層面的東西 凡是帶有create、copy、retain等字眼的函數(shù)在CF中要進行內(nèi)存管理
CFRelease(observer);
通過打印可以觀察的 RunLoop 的狀態(tài)

補充:在進入第一個階段前,會先判斷當前 RunLoop 空不空, 如果是空的 直接來到10階段!
RunLoop的應(yīng)用
NSTimer
需求 讓定時器 在其他線程開啟
NSBlockOperation *block = [NSBlockOperation blockOperationWithBlock:^{
// 這種方式創(chuàng)建的timer 必須手動添加到Runloop中去才會被調(diào)用
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(time) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 同時讓RunLoop跑起來
[[NSRunLoop currentRunLoop] run];
}];
[[[NSOperationQueue alloc] init] addOperation:block];
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode];
ImageView:顯示performSelector
需求
有時候,用戶拖拽scrollView的時候,mode:UITrackingRunLoopMode,顯示圖片,如果圖片很大,會渲染比較耗時,造成不好的體驗,因此,設(shè)置當用戶停止拖拽的時候再顯示圖片,進行延遲操作
方法1:設(shè)置scrollView的delegate? 當停止拖拽的時候做一些事情
方法2:使用performSelector 設(shè)置模式為default模式 ,則顯示圖片這段代碼只能在RunLoop切換模式之后執(zhí)行
// 加載比較大的圖片時,
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// inModes 傳入一個 mode 數(shù)組,這句話的意思是
// 只有在 NSDefaultRunLoopMode 模式下才會執(zhí)行 seletor 的方法顯示圖片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"avater"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
}
效果為:當用戶點擊之后,下載圖片,但是圖片太大,不能及時下載。這時用戶可能會做些其他 UI 操作,比如拖拽,但是如果用戶正在拖拽瀏覽其他的東西時,圖片下載完畢了,此時如果要渲染顯示,會造成不好的用戶體驗,所以當用戶拖拽完畢后,顯示圖片。
這是因為,用戶拖拽,處于 UITrackingRunLoopMode 模式下,所以圖片不會顯示。
常駐線程
需求:
搞一個線程一直存在,一直在后臺做一些操作 比如監(jiān)聽某個狀態(tài), 比如監(jiān)聽是否聯(lián)網(wǎng)。
- (void)viewDidLoad {
[super viewDidLoad];
// 需求:搞一個線程一直不死,一直在后臺做一些操作 比如監(jiān)聽某個狀態(tài), 比如監(jiān)聽是否聯(lián)網(wǎng)。
// 需要在線程中開啟一個RunLoop 一個線程對應(yīng)一個RunLoop 所以獲得當前RunLoop就會自己創(chuàng)建RunLoop
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run2) object:nil];
self.thread = thread;
[thread start];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(run) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)run2
{
NSLog(@"----------");
/*
* 創(chuàng)建RunLoop,如果RunLoop內(nèi)部沒有添加任何Source Timer
* 會直接退出循環(huán),因此需要自己添加一些source才能保持RunLoop運轉(zhuǎn)
*/
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
[[NSRunLoop currentRunLoop] run];
NSLog(@"-----------22222222");
}
從 RunLoop 的源碼看來,如果一個 RunLoop 中沒有添加任何的 Source Timer,會直接退出循環(huán)。
自動釋放池
RunLoop循環(huán)時,在進入睡眠之前會清掉自動釋放池,并且創(chuàng)建一個新的釋放池,用于內(nèi)部變量的銷毀。
在子線程開RunLoop的時候一定要自己寫一個@autoreleasepool,一個RunLoop對應(yīng)一條線程,自動釋放吃是針對當前線程里面的對象。
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(excute) object:nil];
self.thread = thread;
[thread start];
}
- (void)excute
{
@autoreleasepool {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(text) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
}
這樣保證了內(nèi)存安全。
文本代碼:RunLoop