教你如何輕松搞定 Runloop
認識 Runloop
- Runloop 就是運行循環,如果沒有 Runloop,程序一運行就會退出,有 Runloop 就相當于在程序內部開了一個死循環
- 在 iOS 開發中,有兩套 API 可以訪問 Runloop: NSRunloop 和 CFRunloopRef ,它們是等價的,可以相互轉換
- NSRunloop 是基于 CFRunloopRef 的 OC 包裝
- 參考資料: 蘋果官方文檔 、 CFRunloopRef 源碼
Runloop 的本質
- Mach 是 XNU 的內核,進程、線程和虛擬內存等對象通過端口發消息進行通信,Runloop 通過 mach_msg() 函數發送消息
- 如果沒有 port 消息,內核會將線程置于等待狀態 mach_msg_trap()
- 如果有消息,判斷消息類型處理事件,并通過 modeItem 的 callback 進行回調
Runloop 的作用
- 保證程序的持續運行
- 處理 APP 中的各類事件
- 節省 CPU 資源,提高程序性能(有事情就做,沒事情就休息)
Runloop 與線程的關系
- NSRUnloop 與線程一一對應
- 主線程中的 runloop 在程序運行時已經創建并啟動了
- 子線程中的 runloop 需要我們手動創建并開啟
- runloop 在線程結束時,也會銷毀
獲取 Runloop 對象
-
獲取主線程 runloop 對象
NSRunLoop *mainRL = [NSRunLoop mainRunLoop]; CFRunLoopRef mainRLRef = CFRunLoopGetMain(); -
獲取當前線程 runloop 對象
NSRunLoop *currentRL = [NSRunLoop currentRunLoop]; CFRunLoopRef currentRlRef = CFRunLoopGetCurrent(); -
通過子線程創建 runloop
NSRunLoop *curRunloop = [NSRunLoop currentRunLoop];這個方法本身是懶加載的,如果是第一次調用該方法,那么就創建子線程對應的 runloop。
- 補充: runloop 對象是利用字典進行存儲的,key 值對應線程對象,value 值對應該線程的 runloop,在子線程中runloop 不會自動創建。
Runloop 的相關類
與 runloop 相關的共有五個類:CFRunLoopRef、CFRunLoopModeRef、CFRunLoopTimerRef、CFRunLoopSourceRef、CFRunLoopObserverRef

Runloop 五個相關類之間的關系
-
CFRunLoopRef ( Runloop 對象)
-
Runloop 對象就是Runloop 本身
-
-
CFRunLoopModeRef ( Runloop 的運行模式)
-
一個 Runloop 包含若干個 Mode,而每個 Mode 又包含若干個 Source/Timer/Observer,每次 RunLoop 啟動時,只能指定其中一個 Mode,這個 Mode 被稱作 CurrentMode。如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入,這樣就可以分隔開不同組的 Source/Timer/Observer,讓其互不影響。
-
Mode 的分類,系統默認注冊了 5 個 Mode:
-
KCFRunLoopDefaultMode :App的默認Mode,通常主線程是在這個 Mode 下運行
-
UITrackingRunLoopMode :界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
-
UIInitializationRunLoopMode : 在剛啟動 App 時進入的第一個 Mode,啟動完成后就不再使用
-
GSEventReceiveRunLoopMode : 接受系統事件的內部 Mode,通常用不到
-
kCFRunLoopCommonModes : 占位用的 Mode,不是一種真正的 Mode,就相當于 KCFRunLoopDefaultMode 和 UITrackingRunLoopMode的合體
-
-
-
CFRunLoopTimerRef( Timer 事件)
- 基于時間的觸發器,基本等同于 NSTimer
-
NSTimer 在各種模式下的運行效果
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];以上 scheduledTimerWithTimeInterval 方法內部默認把創建的定時器對象添加到當前的 Runloop 中,并且指定運行模式為 NSDefaultRunLoopMode
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES]; [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];以上 timerWithTimeInterval 方法創建定時器,如果想要定時器工作,還需要添加到 Runloop 中,并指定運行模式
-
注意: 當 Runloop 切換到非指定模式,定時器就會停止工作
-
CFRunLoopSourceRef ( Runloop 要處理的事件源)
-
事件源也就是輸入源,只需要對它的分類有所了解就可以了
-
以前的分法(根據官方文檔)
-
Port-Based Sources
-
Custom Input Sources
-
Cocoa Perform Selector Sources
-
-
現在的分法(基于函數的調用棧)
-
Source0:非基于 Port 的
-
Source1:基于 Port 的
-
-
-
CFRunLoopObserverRef( Runloop 的監聽者)
-
主要用于監聽 Runloop 的狀態
-
Runloop 的狀態主要有:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), //即將進入 Runloop kCFRunLoopBeforeTimers = (1UL << 1), //即將處理 NSTimer kCFRunLoopBeforeSources = (1UL << 2), //即將處理 Sources kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠 kCFRunLoopAfterWaiting = (1UL << 6), //剛從休眠中喚醒 kCFRunLoopExit = (1UL << 7), //即將退出 Runloop kCFRunLoopAllActivities = 0x0FFFFFFFU //所有狀態改變 }; -
實現 Runloop 的監聽
//創建監聽對象,當 Runloop 的狀態改變時就會調用該方法 CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { NSLog(@"對 Runloop 的狀態改變進行監聽", activity); }); //設置監聽 CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
-
Runloop 的啟動
-
選擇一個運行模式,且只能選擇一個
-
判斷當前選擇的運行模式是否為空
-
檢查當前運行模式里面是否有 Source 或 Timer,如果都沒有,則 Runloop 立即退出
-
至少有 Source 或 Timer 中的任意一個,則 Runloop 開啟
-
在檢查的時候不會檢查 Observer
Runloop 的運行處理邏輯

Runloop 的運行處理邏輯
-
在運行 Runloop 時,RUnloop 會自動處理之前未處理的消息,并通知相關的監聽者,具體的處理邏輯如下:
-
通知觀察者 Runloop 已經啟動
-
通知觀察者即將要開始的定時器
-
通知觀察者即將啟動的非基于端口的源
-
啟動已經準備好的非基于端口的源
-
如果有基于端口的源并處于等待狀態,立即啟動,跳到第九步
-
通知觀察者 Runloop 進入休眠
-
Runloop 進入休眠,等待發生以下事件時喚醒
-
有事件到達基于端口的源
-
定時器啟動
-
Runloop 超時
-
Runloop 被外界手動喚醒
-
-
通知觀察者,線程剛被喚醒
-
處理喚醒時收到的消息,之后跳到第二步
-
如果有用戶定義的定時器啟動,處理定時器事件并重啟 Runloop
-
如果輸入源啟動,傳遞相應的信息
-
如果 Runloop 被外界手動喚醒且未超時,重啟 Runloop
-
-
通知觀察者,Runloop 即將結束
-
Runloop 的應用
-
常駐線程
-
在子線程中創建一個 Runloop
NSRunLoop *currentRunloop= [NSRunLoop currentRunLoop]; - 需要至少指定 Runloop 的 Source 或者 Timer 中的任意一個(一般情況下指定 Source,比較簡單)
-
需要指定 Runloop 的運行模式(保證 Runloop 不退出)
[currentRunloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; -
需要手動開啟 Runloop
[currentRunloop run];
-
-
imageView 的顯示
-
控制方法在特定模式下可用
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip2016"] afterDelay:3.0];以上方法默認添加到當前的 Runloop 中,并且指定運行模式為默認 KCFRunLoopDefaultMode,如果 Runloop 切換運行模式,則圖片不會加載到 imageView 上。
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20161009_32"] afterDelay:3.0 inModes:@[NSRunLoopCommonModes]];以上方法添加到當前 Runloop 中,且指定運行模式為 NSRunLoopCommonModes
-
-
自動釋放池
- 進入 Runloop 的時候第一次創建
- 退出 Runloop 的時候最后一次釋放(超時或線程銷毀)
-
其他時候的創建與釋放
- 當 Runloop 即將休眠的時候會把之前的自動釋放池釋放,再重新創建一個新的自動釋放池
void msg(int n) { NSLog(@"runloop被喚醒"); NSLog(@"runloop處理事件---%zd",n); } int main(int argc, const char * argv[]) { @autoreleasepool { NSLog(@"runloop啟動了"); do { NSLog(@"runloop詢問,還有事情需要我處理嗎?"); NSLog(@"沒有事情的話,我就睡覺了"); NSLog(@"runloop進入到休眠"); int number = 0; scanf("%zd",&number); msg(number); } while (1); } return 0; }
來自:http://www.jianshu.com/p/4bad817df9ae