RunLoop運行循環(huán)機制

基本概念

進程

進程是指在系統(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容