RunLoop運行循環機制


http://www.jianshu.com/p/0be6be50e461

基本概念

進程

進程是指在系統中正在運行的一個應用程序,而且每個進程之間是獨立的,它們都運行在其專用且受保護的內存空間內,比如同時打開迅雷、Xcode,系統就會分別啟動兩個進程。

線程

一個人進程如果想要執行任務,必須得有至少一條線程,進程的所有任務都會在線程中執行,比如使用網易雲音樂播放音樂,使用迅雷下載電影,都需要在線程中執行。

主線程

iOS 程序運行后,系統會默認開啟一條線程,稱為“主線程”或者“UI 線程”,主線程是用來顯示/刷新 UI 界面,處理 UI 事件的。


簡介

運行循環、跑圈

總結下來,RunLoop 的作用主要體現在三方面:

  1. 保持程序的持續運行
  2. 處理App中的各種事件(比如觸摸事件、定時器事件、Selector事件)
  3. 節省CPU資源,提高程序性能:該做事的時候做事,該休息的時候休息

就是說,如果沒有 RunLoop 程序一運行就結束了,你根本不可能看到持續運行的 app。

iOS中有2套API訪問和使用RunLoop

  • Foundation:NSRunLoop
  • Core Foundation: CFRunLoopRef

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

關於 RunLoop 的源碼請看這里


RunLoop與線程

源碼中,關於創建線程的核心代碼如下:

// 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) { // 如果沒有線程,則要創建線程 __CFUnlock(&loopsLock); // 創建一個可變字典 CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks); // 將主線程放進去,創建 RunLoop(也就是說,創建哪個線程的 RunLoop 需要將線程作為參數傳入) CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np()); // 將主線程的 RunLoop 和主線程以 key/value 的形式保存。 // 因此由此可以看出,一條線程和一個 RunLoop 是一一對應的 CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop); if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) { CFRelease(dict); } CFRelease(mainLoop); __CFLock(&loopsLock); } // 當你輸入 cunrrentRunLoop 時,會通過當前線程這個 key,在字典中尋找對應的 RunLoop CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t)); __CFUnlock(&loopsLock); // 如果沒有在字典中找到 if (!loop) { // 則重新創建一個 RunLoop CFRunLoopRef newLoop = __CFRunLoopCreate(t); __CFLock(&loopsLock); // 然后將 RunLoop 和線程以 key/value 的形式保存 // 再一次驗證了 RunLoop 和 key 是一一對應的 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; }

程序啟動時,系統會自動創建主線程的 RunLoop

  • 每一條線程都有唯一的一個與之對應的RunLoop對象
  • 主線程的RunLoop已經自動創建好了,子線程的RunLoop需要手動創建
  • RunLoop在第一次獲取時創建,在線程結束時銷毀

代碼:

// 獲取當前的線程的RunLoop對象,注意RunLoop是懶加載,currentRunLoop時會自動創建對象 [NSRunLoop currentRunLoop]; // 獲取主線程的RunLoop對象 [NSRunLoop mainRunLoop]; // 如果是 CF 層面 CFRunLoopGetCurrent(); CFRunLoopGetMain();

RunLoop相關類

通過:

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

可以對 RunLoop 內部一覽無余

Core Foundation中關於RunLoop的5個類:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopObserverRef

 

RunLoop 想要跑起來,必須有 Mode 對象支持,而 Mode 里面必須有
(NSSet *)Source(NSArray *)Timer ,源和定時器。

至於另外一個類(NSArray *)observer是用於監聽 RunLoop 的狀態,因此不會激活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]];

系統默認注冊了5個Mode:

  • NSDefaultRunLoopMode:App 的默認 Mode,通常主線程是在這個 Mode 下運行(默認情況下運行)
  • UITrackingRunLoopMode:界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響(操作 UI 界面的情況下運行)
  • UIInitializationRunLoopMode:在剛啟動 App 時進入的第一個 Mode,啟動完成后就不再使用
  • GSEventReceiveRunLoopMode:接受系統事件的內部 Mode,通常用不到(繪圖服務)
  • NSRunLoopCommonModes:這是一個占位用的 Mode,不是一種真正的 Mode (RunLoop無法啟動該模式,設置這種模式下,默認和操作 UI 界面時線程都可以運行,但無法改變 RunLoop 同時只能在一種模式下運行的本質)

下面主要區別 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 是基於事件的觸發器
  • CFRunLoopTimerRef 基本上就是 NSTimer,它受 RunLoop的Mode 影響

創建 Timer 有兩種方式,下面的這種方式必須手動添加到 RunLoop 中去才會被調用

// 這種方式創建的timer 必須手動添加到RunLoop中去才會被調用 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 創建 Timer 一開始就會自動被添加到當前線程並且以
NSDefaultRunLoopMode 模式運行起來,代碼如下:

[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES]; /* 注意:調用了 scheduledTimer 返回的定時器,已經自動被添加到當前 runLoop 中,而且是 NSDefaultRunLoopMode ,想讓上述方法起作用, 必須先讓添加了上述 timer的RunLoop 對象 run 起來,通過 scheduledTimerWithTimeInterval 創建的 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內核發過來一些消息
  • Custom Input Sources:自定義輸入源
  • Cocoa Perform Selector Sources(self performSelector:...)

按照函數調用棧,Source的分類

  • Source0:非基於Port的(觸摸事件、按鈕點擊事件)
  • Source1:基於Port的,通過內核和其他線程通信,接收分發系統事件
          (觸摸硬件,通過 Source1 接收和分發系統事件到 Source0 處理)

為了搞清楚,Source 是如何通過函數調用棧來傳遞事件的,我們做如下實驗:


 

我們可以看到,從程序啟動 start 開始,函數調用棧在監聽到事件點擊后,會一路往下,一直到 -buttonClick: 方法,中間會經過 CFRunLoopSource0 ,這說明我們的按鈕點擊事件是屬於 Source0 的。

而 Source1 是基於 Port 的,就是說,Source1 是和硬件交互的,觸摸首先在屏幕上被包裝成一個 event 事件,再通過 Source1 進行分發到 Source0,最后通過 Source0 進行處理。


 

CFRunLoopObserverRef

CFRunLoopObserverRef 是觀察者,能夠監聽 RunLoop 的狀態改變,主要監聽以下幾個時間節點:

/* 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 // 監聽所有事件 };
// 1.創建觀察者 監聽 RunLoop // 參1: 有個默認值 CFAllocatorRef :CFAllocatorGetDefault() // 參2: CFOptionFlags activities 監聽RunLoop的活動 枚舉 見上面 // 參3: 重復監聽 Boolean repeats YES // 參4: CFIndex order 傳0 CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { // 該方法可以在添加timer之前做一些事情, 在添加source之前做一些事情 NSLog(@"%zd", activity); }); // 2.添加觀察者,監聽當前的RunLoop對象 CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); // CF層面的東西 凡是帶有create、copy、retain等字眼的函數在CF中要進行內存管理 CFRelease(observer);

通過打印可以觀察的 RunLoop 的狀態


 

補充:在進入第一個階段前,會先判斷當前 RunLoop 空不空, 如果是空的 直接來到10階段!


RunLoop的應用

NSTimer

需求 讓定時器 在其他線程開啟

NSBlockOperation *block = [NSBlockOperation blockOperationWithBlock:^{ // 這種方式創建的timer 必須手動添加到Runloop中去才會被調用 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,顯示圖片,如果圖片很大,會渲染比較耗時,造成不好的體驗,因此,設置當用戶停止拖拽的時候再顯示圖片,進行延遲操作

  • 方法1:設置scrollView的delegate 當停止拖拽的時候做一些事情
  • 方法2:使用performSelector 設置模式為default模式 ,則顯示圖片這段代碼只能在RunLoop切換模式之后執行
// 加載比較大的圖片時, - (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event { // inModes 傳入一個 mode 數組,這句話的意思是 // 只有在 NSDefaultRunLoopMode 模式下才會執行 seletor 的方法顯示圖片 [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"avater"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]]; }

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

這是因為,用戶拖拽,處於 UITrackingRunLoopMode 模式下,所以圖片不會顯示。

常駐線程

需求:
搞一個線程一直存在,一直在后台做一些操作 比如監聽某個狀態, 比如監聽是否聯網。

- (void)viewDidLoad { [super viewDidLoad]; // 需求:搞一個線程一直不死,一直在后台做一些操作 比如監聽某個狀態, 比如監聽是否聯網。 // 需要在線程中開啟一個RunLoop 一個線程對應一個RunLoop 所以獲得當前RunLoop就會自己創建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(@"----------"); /* * 創建RunLoop,如果RunLoop內部沒有添加任何Source Timer * 會直接退出循環,因此需要自己添加一些source才能保持RunLoop運轉 */ [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; // [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]; [[NSRunLoop currentRunLoop] run]; NSLog(@"-----------22222222"); }

從 RunLoop 的源碼看來,如果一個 RunLoop 中沒有添加任何的 Source Timer,會直接退出循環。

自動釋放池

RunLoop循環時,在進入睡眠之前會清掉自動釋放池,並且創建一個新的釋放池,用於內部變量的銷毀。

在子線程開RunLoop的時候一定要自己寫一個@autoreleasepool,一個RunLoop對應一條線程,自動釋放吃是針對當前線程里面的對象。

- (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]; } }

這樣保證了內存安全。

文本代碼:RunLoop



文/Ammar(簡書作者)
原文鏈接:http://www.jianshu.com/p/0be6be50e461
著作權歸作者所有,轉載請聯系作者獲得授權,並標注“簡書作者”。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM