iOS探索:RunLoop本質、數據結構以及常駐線程實現

NO IMAGE

RunLoop的本質

RunLoop是通過內部維護的事件循環來對事件/消息進行管理的一個對象

  • 沒有消息需要處理時,休眠以避免資源佔用,狀態切換是從用戶態通過系統調用切換到內核態

  • 有消息處理時,立刻被喚醒,狀態切換是從內核態通過系統調用切換到用戶態

這裡有一個問題,我們應用程序中的main函數為什麼可以保持無退出呢

實際上呢,在我們的main函數中會調用UIApplicationMain函數,在這個函數中會啟動一個運行循環(也就是我們所說的RunLoop),在這個運行循環中可以處理很多事件,例如屏幕的點擊,滑動列表,或者網絡請求的返回等等,在處理完事件之後,會進入等待,在這個循環中,並不是一個單純的for循環或者while循環,而是從用戶態到內核態的切換,以及再從內核態到用戶態的切換,這裡面的等待也不等於死循環,這裡面最重要的是狀態的切換

RunLoop的數據結構

在OC中,系統為我們提供了兩個RunLoop,一個是CFRunLoop,另一個是NSRunLoop,而NSRunLoop是對CFRunLoop的一個封裝,提供了面向對象的API,並且它們也分別屬於不同的框架,NSRunLoop是屬於Foundation框架,而CFRunLoop是屬於Core Foundation框架

關於RunLoop的數據結構主要有三種:

  • CFRunLoop

  • CFRunLoopMode

  • Source/Timer/Observer

iOS探索:RunLoop本質、數據結構以及常駐線程實現

  • pthread:代表的是線程,RunLoop與線程的關係是一一對應的

  • currentMode:是一個CFRunLoopMode這樣一個數據結構

  • modes:是一個包含CFRunLoopMode類型的集合(NSMutableSet<CFRunLoopMode*>)

  • commonModes:是一個包含NSString類型的集合(NSMutableSet<NSString*>)

  • commonModeItems:也是一個集合,在這個集合中包含多個元素,其中包括多個Observer,多個Timer,多個Source

iOS探索:RunLoop本質、數據結構以及常駐線程實現

  • name:名稱,例如NSDefaultRunLoopMode,所以說是通過這樣一個名稱來切換對應的模式,例如在上面的commonModes裡面都是名稱字符串,也就是說通過這些名稱來支持多種模式

  • source0:集合類型的數據結構

  • source1:集合類型的數據結構

  • obsevers:數組類型的數據結構

  • timers:數組類型的數據結構

CFRunLoopSource

  • source0:需要手動喚醒線程

  • source1:具備喚醒線程的能力

CFRunLoopTimer

和NSTimer是toll-free bridge的(免費橋轉換)

CFRunLoopObserver

我們可以通過註冊一些Observer來實現對RunLoop相關時間點的觀測

可以觀測的時間點包括:

  • kCFRunLoopEntry:RunLoop的入口時機,RunLoop將要啟動的時候的回調通知

  • kCFRunLoopBeforeTimers:RunLoop將要處理Timer事件的時候

  • kCFRunLoopBeforeSources:RunLoop將要處理Source事件的時候

  • kCFRunLoopBeforeWaiting:RunLoop將要進入休眠的時候,將要進行用戶態到內核態的切換

  • kCFRunLoopAfterWaiting:RunLoop將要進入喚醒的時候,內核態到用戶態的切換後不久

  • kCFRunLoopExit:RunLoop退出的時候

RunLoop的mode

iOS探索:RunLoop本質、數據結構以及常駐線程實現

在RunLoop中,假如在mode1中運行,那麼在mode2中事件的回調就會接收不到,RunLoop只接受在當前mode中的回調,那麼這裡有一個經典問題,當我們在滑動列表時,為什麼會出現cell上的定時器停止的情況以及如何解決

因為在列表滑動的時候當前RunLoop的mode從Default切換到了Tracking,所以導致原來mode中的事件回調接收不到,想要解決便可將其加入commonModes中,下面我們來看一下commonMode

CommonMode的特殊性

  • CommonMode並不是一個實際存在的模式

  • 是同步Source/Timer/Observer到多個Mode中的一中技術方案

事件循環的實現機制

iOS探索:RunLoop本質、數據結構以及常駐線程實現

  • 在RunLoop啟動之後會發送一個通知,來告知觀察者

  • 將要處理Timer/Source0事件這樣一個通知的發送

  • 處理Source0事件

  • 如果有Source1要處理,這時會通過一個go to語句的實現來進行代碼邏輯的跳轉,處理喚醒是收到的消息

  • 如果沒有Source1要處理,線程就將要休眠,同時發送一個通知,告訴觀察者

  • 然後線程進入一個用戶態到內核態的切換,休眠,然後等待喚醒,喚醒的條件大約包括三種:
    1、Source1
    2、Timer事件
    3、外部手動喚醒

  • 線程剛被喚醒之後也要發送一個通知告訴觀察者,然後處理喚醒時收到的消息

  • 回到將要處理Timer/Source0事件這樣一個通知的發送

  • 然後再次進行上面步驟,這就是一個RunLoop的事件循環機制

這裡有一個這樣的問題:當我們點擊一個app,從我們點擊到程序啟動、程序運行再到程序殺死這個過程,系統都發生了什麼呢

實際上當我們調用了main函數之後,會調用UIApplicationMain函數,在這個函數內部會啟動主線程的RunLoop,然後經過一系列的處理,最終主線程的RunLoop會處於一個休眠狀態,然後我們此時如果點擊一下屏幕,會轉化成一個Source1來講我們的主線程喚醒,然後當我們殺死程序時,會調用RunLoop的退出,同時發送通知告訴觀察者

RunLoop與多線程

  • 線程與RunLoop是一一對應的

  • 自己創建的線程默認沒有RunLoop

實現一個常駐線程

  • 為當前線程開啟一個RunLoop

  • 向該RunLoop中添加一個Port/Source等維持RunLoop的事件循環

  • 啟動該RunLoop

請看下面的一個代碼邏輯

#import "WXObject.h"
static NSThread *thread = nil;
/** 是否繼續事件循環*/
static BOOL runAlways = YES;
@implementation WXObject
+ (NSThread *)threadForDispatch {
if (thread == nil) {
@synchronized (self) {
if (thread == nil) {
thread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequest) object:nil];
[thread setName:@"alwaysThread"];
//啟動線程
[thread start];
}
}
}
return thread;
}
+ (void)runRequest {
//創建一個Source
CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
//創建RunLoop,同時向RunLoop的defaultMode下面添加Source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
//如果可以運行
while (runAlways) {
@autoreleasepool {
//令當前RunLoop運行在defaultMode下
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
}
}
//某一時機,靜態變量runAlways變為NO時,保證跳出RunLoop,線程推出
CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
CFRelease(source);
}
@end
  • 首先我們在這裡定義兩個全局靜態變量,一個是我們自定義的線程thread,還有一個是用來控制是否事件循環

  • 然後我們創建線程,用@synchronized來保證線程安全,創建的時候添加入口方法,然後啟動線程,當線程調用start方法時,會調用下面入口方法

  • 在這個方法中首先創建source,傳入一個上下文,然後創建RunLoop,同時向RunLoop的defaultMode下面添加Source,CFRunLoopGetCurrent()這個方法如果獲取不到就會創建一個RunLoop,然後添加到defaultMode中

  • 通過我們前面定義的靜態變量來進行判斷,如果可以運行,就令當前RunLoop運行在defaultMode下,這裡用了一個自動釋放池,減小內存峰值消耗,這裡需要注意的是,如果我們上面添加到的是defaultMode,這裡也需要運行在defaultMode中,否則會出現死循環

  • 某一時機,靜態變量runAlways變為NO時,保證跳出RunLoop,線程推出,釋放source

以上就是實現一個常駐線程的代碼邏輯

GitHub

Demo

相關文章

iOS傳感器集錦

iOSCoreData(二)版本升級和數據庫遷移

iOSCoreData(一)增刪改查

iOS探索:網絡相關