RunLoop運(yùn)行循環(huán)機(jī)制

基本概念

進(jìn)程

進(jìn)程是指在系統(tǒng)中正在運(yùn)行的一個(gè)應(yīng)用程序,而且每個(gè)進(jìn)程之間是獨(dú)立的,它們都運(yùn)行在其專用且受保護(hù)的內(nèi)存空間內(nèi),比如同時(shí)打開迅雷、Xcode,系統(tǒng)就會(huì)分別啟動(dòng)兩個(gè)進(jìn)程。

線程

一個(gè)人進(jìn)程如果想要執(zhí)行任務(wù),必須得有至少一條線程,進(jìn)程的所有任務(wù)都會(huì)在線程中執(zhí)行,比如使用網(wǎng)易云音樂(lè)播放音樂(lè),使用迅雷下載電影,都需要在線程中執(zhí)行。

主線程

iOS 程序運(yùn)行后,系統(tǒng)會(huì)默認(rèn)開啟一條線程,稱為“主線程”或者“UI 線程”,主線程是用來(lái)顯示/刷新 UI 界面,處理 UI 事件的。

簡(jiǎn)介

運(yùn)行循環(huán)、跑圈

總結(jié)下來(lái),RunLoop 的作用主要體現(xiàn)在三方面:

保持程序的持續(xù)運(yùn)行

處理App中的各種事件(比如觸摸事件、定時(shí)器事件、Selector事件)

節(jié)省CPU資源,提高程序性能:該做事的時(shí)候做事,該休息的時(shí)候休息

就是說(shuō),如果沒(méi)有 RunLoop 程序一運(yùn)行就結(jié)束了,你根本不可能看到持續(xù)運(yùn)行的 app。

iOS中有2套API訪問(wèn)和使用RunLoop

Foundation:NSRunLoop

Core Foundation: CFRunLoopRef

NSRunLoop是基于CFRunLoopRef的一層OC包裝,因此我們需要研究CFRunLoopRef層面的API(Core Foundation層面)

關(guān)于 RunLoop 的源碼請(qǐng)看這里

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) { // 如果沒(méi)有線程,則要?jiǎng)?chuàng)建線程

__CFUnlock(&loopsLock);

// 創(chuàng)建一個(gè)可變字典

CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);

// 將主線程放進(jìn)去,創(chuàng)建 RunLoop(也就是說(shuō),創(chuàng)建哪個(gè)線程的 RunLoop 需要將線程作為參數(shù)傳入)

CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());

// 將主線程的 RunLoop 和主線程以 key/value 的形式保存。

// 因此由此可以看出,一條線程和一個(gè) RunLoop 是一一對(duì)應(yīng)的

CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);

if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {

CFRelease(dict);

}

CFRelease(mainLoop);

__CFLock(&loopsLock);

}

// 當(dāng)你輸入 cunrrentRunLoop 時(shí),會(huì)通過(guò)當(dāng)前線程這個(gè) key,在字典中尋找對(duì)應(yīng)的 RunLoop

CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));

__CFUnlock(&loopsLock);

// 如果沒(méi)有在字典中找到

if (!loop) {

// 則重新創(chuàng)建一個(gè) RunLoop

CFRunLoopRef newLoop = __CFRunLoopCreate(t);

__CFLock(&loopsLock);

// 然后將 RunLoop 和線程以 key/value 的形式保存

// 再一次驗(yàn)證了 RunLoop 和 key 是一一對(duì)應(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;

}

程序啟動(dòng)時(shí),系統(tǒng)會(huì)自動(dòng)創(chuàng)建主線程的 RunLoop

每一條線程都有唯一的一個(gè)與之對(duì)應(yīng)的RunLoop對(duì)象

主線程的RunLoop已經(jīng)自動(dòng)創(chuàng)建好了,子線程的RunLoop需要手動(dòng)創(chuàng)建

RunLoop在第一次獲取時(shí)創(chuàng)建,在線程結(jié)束時(shí)銷毀

代碼:

// 獲取當(dāng)前的線程的RunLoop對(duì)象,注意RunLoop是懶加載,currentRunLoop時(shí)會(huì)自動(dòng)創(chuàng)建對(duì)象

[NSRunLoop currentRunLoop];

// 獲取主線程的RunLoop對(duì)象

[NSRunLoop mainRunLoop];

// 如果是 CF 層面

CFRunLoopGetCurrent();

CFRunLoopGetMain();

RunLoop相關(guān)類

通過(guò):

NSLog(@"%@", [NSRunLoop mainRunLoop]);

可以對(duì) RunLoop 內(nèi)部一覽無(wú)余

Core Foundation中關(guān)于RunLoop的5個(gè)類:

CFRunLoopRef

CFRunLoopModeRef

CFRunLoopSourceRef

CFRunLoopObserverRef

RunLoop 想要跑起來(lái),必須有 Mode 對(duì)象支持,而 Mode 里面必須有

(NSSet *)Source、(NSArray *)Timer,源和定時(shí)器。

至于另外一個(gè)類(NSArray *)observer是用于監(jiān)聽(tīng) RunLoop 的狀態(tài),因此不會(huì)激活RunLoop。

CFRunLoopModeRef

CFRunLoopModeRef 代表 RunLoop 的運(yùn)行模式

每個(gè) RunLoop 都包含若干個(gè) Mode ,每個(gè) Mode 又包含若干個(gè) Source/Timer/Observer,每次

RunLoop 啟動(dòng)時(shí),只能指定其中一個(gè) Mode,這個(gè) Mode 被稱作CurrentMode,如果需要切換 Mode,只能退出

Loop,再重新指定一個(gè) Mode 進(jìn)入,這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響(可以通過(guò)切換

Mode,完成不同的 timer/source/observer)。

[NSRunLoop currentRunLoop].currentMode; // 獲取當(dāng)前運(yùn)行模式

[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

系統(tǒng)默認(rèn)注冊(cè)了5個(gè)Mode:

NSDefaultRunLoopMode:App 的默認(rèn) Mode,通常主線程是在這個(gè) Mode 下運(yùn)行(默認(rèn)情況下運(yùn)行)

UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他 Mode 影響(操作 UI 界面的情況下運(yùn)行)

UIInitializationRunLoopMode:在剛啟動(dòng) App 時(shí)進(jìn)入的第一個(gè) Mode,啟動(dòng)完成后就不再使用

GSEventReceiveRunLoopMode:接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到(繪圖服務(wù))

NSRunLoopCommonModes:這是一個(gè)占位用的 Mode,不是一種真正的 Mode (RunLoop無(wú)法啟動(dòng)該模式,設(shè)置這種模式下,默認(rèn)和操作 UI 界面時(shí)線程都可以運(yùn)行,但無(wú)法改變 RunLoop 同時(shí)只能在一種模式下運(yùn)行的本質(zhì))

下面主要區(qū)別 NSDefaultRunLoopMode 和 UITrackingRunLoopMode 以及 NSRunLoopCommonModes。請(qǐng)看以下代碼:

- (void)viewDidLoad {

[super viewDidLoad];

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

// 在默認(rèn)模式下添加的 timer 當(dāng)我們拖拽 textView 的時(shí)候,不會(huì)運(yùn)行 run 方法

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

// 在 UI 跟蹤模式下添加 timer 當(dāng)我們拖拽 textView 的時(shí)候,run 方法才會(huì)運(yùn)行

[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

// timer 可以運(yùn)行在兩種模式下,相當(dāng)于上面兩句代碼寫在一起

[[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 有兩種方式,下面的這種方式必須手動(dòng)添加到 RunLoop 中去才會(huì)被調(diào)用

// 這種方式創(chuàng)建的timer 必須手動(dòng)添加到RunLoop中去才會(huì)被調(diào)用

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(time) userInfo:nil repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:timer

forMode:NSDefaultRunLoopMode];

// 同時(shí)讓RunLoop跑起來(lái)

[[NSRunLoop currentRunLoop] run];

而通過(guò)scheduledTimer創(chuàng)建 Timer 一開始就會(huì)自動(dòng)被添加到當(dāng)前線程并且以

NSDefaultRunLoopMode模式運(yùn)行起來(lái),代碼如下:

[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

/*

注意:調(diào)用了 scheduledTimer 返回的定時(shí)器,已經(jīng)自動(dòng)被添加到當(dāng)前

runLoop 中,而且是 NSDefaultRunLoopMode ,想讓上述方法起作用,

必須先讓添加了上述 timer的RunLoop 對(duì)象 run 起來(lái),通過(guò)

scheduledTimerWithTimeInterval 創(chuàng)建的 timer 可以通過(guò)以下方法修改 mode

*/

[[NSRunLoop currentRunLoop] addTimer:timer2 forMode:UITrackingRunLoopMode];

注意: GCD的定時(shí)器不受RunLoop的Mode影響

CADisplayLink *display = [CADisplayLink displayLinkWithTarget:self selector:@selector(run)];

/*

注意:CADisplayLink ,也是在 Runloop 下運(yùn)行的,

有一個(gè)方法可以將CADisplayLink 對(duì)象添加到一個(gè) Runloop 對(duì)象中去

*/

[display addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];

CFRunLoopSourceRef

CFRunLoopSourceRef 其實(shí)是事件源(輸入源)

按照官方文檔,Source的分類

Port-Based Sources:基于端口的:跟其他線程進(jìn)行交互的,Mac內(nèi)核發(fā)過(guò)來(lái)一些消息

Custom Input Sources:自定義輸入源

Cocoa Perform Selector Sources(self performSelector:...)

按照函數(shù)調(diào)用棧,Source的分類

Source0:非基于Port的(觸摸事件、按鈕點(diǎn)擊事件)

Source1:基于Port的,通過(guò)內(nèi)核和其他線程通信,接收分發(fā)系統(tǒng)事件

(觸摸硬件,通過(guò) Source1 接收和分發(fā)系統(tǒng)事件到 Source0 處理)

為了搞清楚,Source 是如何通過(guò)函數(shù)調(diào)用棧來(lái)傳遞事件的,我們做如下實(shí)驗(yàn):

我們可以看到,從程序啟動(dòng) start 開始,函數(shù)調(diào)用棧在監(jiān)聽(tīng)到事件點(diǎn)擊后,會(huì)一路往下,一直到-buttonClick:方法,中間會(huì)經(jīng)過(guò)CFRunLoopSource0,這說(shuō)明我們的按鈕點(diǎn)擊事件是屬于 Source0 的。

而 Source1 是基于 Port 的,就是說(shuō),Source1 是和硬件交互的,觸摸首先在屏幕上被包裝成一個(gè) event 事件,再通過(guò) Source1 進(jìn)行分發(fā)到 Source0,最后通過(guò) Source0 進(jìn)行處理。

CFRunLoopObserverRef

CFRunLoopObserverRef 是觀察者,能夠監(jiān)聽(tīng) RunLoop 的狀態(tài)改變,主要監(jiān)聽(tīng)以下幾個(gè)時(shí)間節(jié)點(diǎn):

/* Run Loop Observer Activities */

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity)

{

kCFRunLoopEntry = (1UL << 0), // 1 即將進(jìn)入 Loop

kCFRunLoopBeforeTimers = (1UL << 1), // 2 即將處理 Timer

kCFRunLoopBeforeSources = (1UL << 2), // 4 即將處理 Source

kCFRunLoopBeforeWaiting = (1UL << 5), // 32 即將進(jìn)入休眠

kCFRunLoopAfterWaiting = (1UL << 6), // 64 剛從休眠中喚醒

kCFRunLoopExit = (1UL << 7), // 128 即將退出 Loop

kCFRunLoopAllActivities = 0x0FFFFFFFU // 監(jiān)聽(tīng)所有事件

};

// 1.創(chuàng)建觀察者 監(jiān)聽(tīng) RunLoop

// 參1: 有個(gè)默認(rèn)值 CFAllocatorRef :CFAllocatorGetDefault()

// 參2: CFOptionFlags activities 監(jiān)聽(tīng)RunLoop的活動(dòng) 枚舉 見(jiàn)上面

// 參3: 重復(fù)監(jiān)聽(tīng) 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)聽(tīng)當(dāng)前的RunLoop對(duì)象

CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

// CF層面的東西 凡是帶有create、copy、retain等字眼的函數(shù)在CF中要進(jìn)行內(nèi)存管理

CFRelease(observer);

通過(guò)打印可以觀察的 RunLoop 的狀態(tài)

補(bǔ)充:在進(jìn)入第一個(gè)階段前,會(huì)先判斷當(dāng)前 RunLoop 空不空, 如果是空的 直接來(lái)到10階段!

RunLoop的應(yīng)用

NSTimer

需求 讓定時(shí)器 在其他線程開啟

NSBlockOperation *block = [NSBlockOperation blockOperationWithBlock:^{

// 這種方式創(chuàng)建的timer 必須手動(dòng)添加到Runloop中去才會(huì)被調(diào)用

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(time) userInfo:nil repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

// 同時(shí)讓RunLoop跑起來(lái)

[[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

需求

有時(shí)候,用戶拖拽scrollView的時(shí)候,mode:UITrackingRunLoopMode,顯示圖片,如果圖片很大,會(huì)渲染比較耗時(shí),造成不好的體驗(yàn),因此,設(shè)置當(dāng)用戶停止拖拽的時(shí)候再顯示圖片,進(jìn)行延遲操作

方法1:設(shè)置scrollView的delegate? 當(dāng)停止拖拽的時(shí)候做一些事情

方法2:使用performSelector 設(shè)置模式為default模式 ,則顯示圖片這段代碼只能在RunLoop切換模式之后執(zhí)行

// 加載比較大的圖片時(shí),

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event

{

// inModes 傳入一個(gè) mode 數(shù)組,這句話的意思是

// 只有在 NSDefaultRunLoopMode 模式下才會(huì)執(zhí)行 seletor 的方法顯示圖片

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"avater"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

}

效果為:當(dāng)用戶點(diǎn)擊之后,下載圖片,但是圖片太大,不能及時(shí)下載。這時(shí)用戶可能會(huì)做些其他 UI 操作,比如拖拽,但是如果用戶正在拖拽瀏覽其他的東西時(shí),圖片下載完畢了,此時(shí)如果要渲染顯示,會(huì)造成不好的用戶體驗(yàn),所以當(dāng)用戶拖拽完畢后,顯示圖片。

這是因?yàn)椋脩敉献В幱?UITrackingRunLoopMode 模式下,所以圖片不會(huì)顯示。

常駐線程

需求:

搞一個(gè)線程一直存在,一直在后臺(tái)做一些操作 比如監(jiān)聽(tīng)某個(gè)狀態(tài), 比如監(jiān)聽(tīng)是否聯(lián)網(wǎng)。

- (void)viewDidLoad {

[super viewDidLoad];

// 需求:搞一個(gè)線程一直不死,一直在后臺(tái)做一些操作 比如監(jiān)聽(tīng)某個(gè)狀態(tài), 比如監(jiān)聽(tīng)是否聯(lián)網(wǎng)。

// 需要在線程中開啟一個(gè)RunLoop 一個(gè)線程對(duì)應(yīng)一個(gè)RunLoop 所以獲得當(dāng)前RunLoop就會(huì)自己創(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)部沒(méi)有添加任何Source Timer

* 會(huì)直接退出循環(huán),因此需要自己添加一些source才能保持RunLoop運(yùn)轉(zhuǎn)

*/

[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];

// [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

[[NSRunLoop currentRunLoop] run];

NSLog(@"-----------22222222");

}

從 RunLoop 的源碼看來(lái),如果一個(gè) RunLoop 中沒(méi)有添加任何的 Source Timer,會(huì)直接退出循環(huán)。

自動(dòng)釋放池

RunLoop循環(huán)時(shí),在進(jìn)入睡眠之前會(huì)清掉自動(dòng)釋放池,并且創(chuàng)建一個(gè)新的釋放池,用于內(nèi)部變量的銷毀。

在子線程開RunLoop的時(shí)候一定要自己寫一個(gè)@autoreleasepool,一個(gè)RunLoop對(duì)應(yīng)一條線程,自動(dòng)釋放吃是針對(duì)當(dāng)前線程里面的對(duì)象。

- (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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • ======================= 前言 RunLoop 是 iOS 和 OSX 開發(fā)中非常基礎(chǔ)的一個(gè)...
    i憬銘閱讀 912評(píng)論 0 4
  • 一、什么是runloop 字面意思是“消息循環(huán)、運(yùn)行循環(huán)”。它不是線程,但它和線程息息相關(guān)。一般來(lái)講,一個(gè)線程一次...
    WeiHing閱讀 8,177評(píng)論 11 111
  • runtime 和 runloop 作為一個(gè)程序員進(jìn)階是必須的,也是非常重要的, 在面試過(guò)程中是經(jīng)常會(huì)被問(wèn)到的, ...
    made_China閱讀 1,226評(píng)論 0 7
  • runtime 和 runloop 作為一個(gè)程序員進(jìn)階是必須的,也是非常重要的, 在面試過(guò)程中是經(jīng)常會(huì)被問(wèn)到的, ...
    SOI閱讀 21,848評(píng)論 3 63
  • 誰(shuí)的青春歲月里不會(huì)犯錯(cuò),迷茫
    一條會(huì)飛的魚six閱讀 160評(píng)論 0 0