什么是RunLoop
- 從字面上看,就是運行循環,跑圈
- 其實它內部就是do-while循環,在這個循環內部不斷地處理各種任務(比如Source、Timer、Observer)
- 循環體的開始需要檢測是否有需要處理的事件,如果有則去處理,如果沒有則進入睡眠以節省CPU時間
基本作用
- 保持程序的持續運行
- 處理App中的各種事件(比如觸摸事件、定時器事件、Selector事件)
- 節省CPU資源,提高程序性能:該做事時做事,該休息時休息
- ······
RunLoop對象
- iOS中有2套API來訪問和使用RunLoop
- Core Foundation
- NSRunLoop
- NSRunLoop和CFRunLoopRef都代表著RunLoop對象
- Core Foundation
- CFRunLoopRef是在 CoreFoundation 框架內的,它提供了純 C 函數的 API,所有這些 API 都是線程安全的。
- NSRunLoop是基于CFRunLoopRef的一層OC包裝,提供了面向對象的 API,但是這些 API 不是線程安全的。所以要了解RunLoop內部結構,需要多研究CFRunLoopRef層面的API(Core Foundation層面)
獲得RunLoop對象
- Foundation
[NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
- Core Foundation
CFRunLoopGetCurrent(); // 獲得當前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象
RunLoop與線程
每條線程都有唯一的一個與之對應的RunLoop對象,其關系是保存在一個全局的 Dictionary 里
主線程的RunLoop已經自動創建和啟動,子線程的RunLoop需要主動創建、調用run方法啟動
RunLoop在第一次獲取時創建,在線程結束時銷毀
RunLoop資料
CFRunLoopRef是開源的
http://opensource.apple.com/source/CF/CF-1151.16/
Core Foundation中關于RunLoop的5個類
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
CFRunLoopModeRef
CFRunLoopModeRef 類并沒有對外暴露,只是通過 CFRunLoopRef的接口進行了封裝,代表RunLoop的運行模式
-
關系
- 一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer
- 每次調用 RunLoop 的主函數時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode
- 如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進入
- 這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響
-
系統默認注冊了5個Mode:
NSDefaultRunLoopMode:App的默認Mode,通常主線程是在這個Mode下運行
UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用
GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到
NSRunLoopCommonModes: 這是一個占位用的Mode,不是一種真正的Mode
"CommonModes"概念
一個 Mode 可以將自己標記為"Common"屬性(通過將其 ModeName 添加到 RunLoop 的 "commonModes" 中)。每當 RunLoop 的內容發生變化時,RunLoop 都會自動將 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 標記的所有Mode里。
-
應用場景
主線程的 RunLoop 里有兩個預置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個 Mode 都已經被標記為"Common"屬性。DefaultMode 是 App 平時所處的狀態,TrackingRunLoopMode 是追蹤 ScrollView 滑動時的狀態。當你創建一個 Timer 并加到 DefaultMode 時,Timer 會得到重復回調,但此時滑動一個TableView時,RunLoop 會將 mode 切換為 TrackingRunLoopMode,這時 Timer 就不會被回調,并且也不會影響到滑動操作。
有時你需要一個 Timer,在兩個 Mode 中都能得到回調,一種辦法就是將這個 Timer 分別加入這兩個 Mode。還有一種方式,就是將 Timer 加入到頂層的 RunLoop 的 "commonModeItems" 中。"commonModeItems" 被 RunLoop 自動更新到所有具有"Common"屬性的 Mode 里去
定時器NSTimer,代碼實現
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 定時器只運行在NSDefaultRunLoopMode下,一旦RunLoop進入其他模式,這個定時器就不會工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 定時器只運行在UITrackingRunLoopMode下,一旦RunLoop進入其他模式,這個定時器就不會工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 定時器會跑在標記為common modes的模式下
// 標記為common modes的模式:UITrackingRunLoopMode和NSDefaultRunLoopMode
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
// 調用了scheduledTimer返回的定時器,已經自動被添加到當前runLoop中,而且是NSDefaultRunLoopMode
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 修改模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
CFRunLoopSourceRef
- CFRunLoopSourceRef 是事件產生的地方。Source有兩個版本:Source0 和 Source1。
- Source0 非基于Port的
,只包含了一個回調(函數指針),它并不能主動觸發事件。使用時,你需要先調用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然后手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop,讓其處理這個事件。 - 基于Port的,通過內核和其他線程通信,接收、分發系統事件
CFRunLoopTimerRef
- CFRunLoopTimerRef是基于時間的觸發器
- CFRunLoopTimerRef基本上說的就是NSTimer,它受RunLoop的Mode影響
- GCD的定時器不受RunLoop的Mode影響
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 // 可以監聽以上所有狀態
};
- 添加觀察者Observer代碼實現
// 創建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----監聽到RunLoop狀態發生改變---%zd", activity);
});
// 添加觀察者:監聽RunLoop的狀態
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 釋放Observer
CFRelease(observer);
CF的內存管理(Core Foundation)
- 凡是帶有Create、Copy、Retain等字眼的函數,創建出來的對象,都需要在最后做一次release
- 比如CFRunLoopObserverCreate
- release函數:CFRelease(對象);
PerformSelecter
當調用 NSObject 的 performSelecter:afterDelay: 后,實際上其內部會創建一個 Timer 并添加到當前線程的 RunLoop 中。所以如果當前線程沒有 RunLoop,則這個方法會失效。
當調用 performSelector:onThread: 時,實際上其會創建一個 Timer 加到對應的線程去,同樣的,如果對應線程沒有 RunLoop 該方法也會失效。
-
應用:可以讓某些事件(行為、任務)在特定模式下執行
- 有時候圖片比較大,渲染到屏幕耗費時間,會造成界面卡頓,可以讓圖片在UIScrollView滾動完之后執行
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:2 inModes:@[NSDefaultRunLoopMode]];
使用RunLoop開啟一個常駐線程
讓一個子線程不進入消亡狀態,等待其他線程發來消息,處理其他事件
子線程RunLoop的三種手動啟動方式
// 默認RunLoop指定的模式是NSDefaultRunLoopMode,周期無限長
[[NSRunLoop currentRunLoop] run];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
- 常駐線程,讓子線程不消亡,兩種實現方式
- 給RunLoop的Mode添加一個Source或者Timer
// RunLoop只能選擇一個Mode啟動,如果當前Mode中沒有任何Source、Timer,那么就直接退出RunLoop [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode]; [[NSRunLoop currentRunLoop] run];
- 一直循環啟動RunLoop,退出RunLoop,直到添加了Source或Timer,才會跑圈
while (1) { [[NSRunLoop currentRunLoop] run]; }
RunLoop處理邏輯
-
官方版
RunLoop1.png 網友版