什么是 RunLoop?
- 運(yùn)行循環(huán)
- 內(nèi)部就是一個(gè) do-while 循環(huán), 在這個(gè)循環(huán)里面不斷的處理各種任務(wù)
- 一個(gè)線程對應(yīng)有一個(gè) RunLoop, 主線程的 RunLoop 默認(rèn)已經(jīng)啟動, 子線程的 RunLoop 需要手動去啟動 (調(diào)用 run 方法)
- RunLoop 只能選擇一個(gè) Mode 啟動, 如果當(dāng)前 Mode 中沒有任何Source(Sources0饰抒、Sources1)撩银、Timer, 那么就直接退出 RunLoop.
- 基本的作用就是保持程序的持續(xù)運(yùn)行, 處理 app 中的各種事件. 通過 RunLoop, 有事運(yùn)行, 沒事就休息, 可以節(jié)省 cpu 資源, 提高程序性能.
RunLoop對象
iOS 中有2套API來訪問和使用RunLoop
- Foundation: NSRunLoop
- Core Foundation: CFRunLoopRef
- NSRunLoop 和 CFRunLoopRef 都代表著 RunLoop對象
- NSRunLoop 是基于 CFRunLoopRef 的一層 OC 包裝, 所以要了解 RunLoop內(nèi)部結(jié)構(gòu), 需要多研究 CFRunLoopRef 層面的 API
RunLoop和線程
- 每條線程都有唯一的一個(gè)與之對應(yīng)的 RunLoop 對象
- 主線程的 RunLoop 已經(jīng)自動創(chuàng)建好了, 子線程的 RunLoop 需要主動創(chuàng)建
- RunLoop 在第一次獲取時(shí)創(chuàng)建, 在線程結(jié)束時(shí)銷毀
獲取 RunLoop 對象
- Foundation
[NSRunLoop currentRunLoop]; // 獲取當(dāng)前 RunLoop 對象
[NSRunLoop mainRunLoop]; // 獲取主線程 RunLoop 對象 - Core Foundation
CFRunLoopGetCurrent(): // 獲取當(dāng)前 RunLoop 對象
CFRunLoopGetMain(); // 獲取主線程 RunLoop 對象
RunLoop相關(guān)類
Core Foundation 中關(guān)于 RunLoop 的5個(gè)類
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
CFRunLoopModeRef
- CFRunLoopModeRef 代表 RunLoop 的運(yùn)行模式.
- 一個(gè) RunLoop 包含若干個(gè) Mode, 每個(gè) Model 又包含若干個(gè)(set)Source/(array)Timer/(array)Observer
- 每次 RunLoop 啟動時(shí), 只能制定其中一個(gè) Mode, 這個(gè) Mode 被稱作 CurrentMode
- 如果需要切換 Mode, 只能退出 Loop, 重新制定一個(gè) Mode 再進(jìn)入
- mode 主要是用來制定事件在運(yùn)行循環(huán)中的優(yōu)先級, 分為:
- NSDefaultRunLoopMode (kCFRunLoopDefaultMode): 默認(rèn), 空閑狀態(tài)
- UITrackingRunLoopMode: ScrollView 滑動時(shí)會切換到這個(gè)Mode
- UIInitializationRunLoopMode: run loop 啟動時(shí), 會切換到該 Mode
- NSRunLoopCommonModes (kCFRunLoopCommonModes) : mode 集合
蘋果公開提供的mode有倆個(gè): NSDefaultRunLoopMode (kCFRunLoopDefaultMode), NSRunLoopCommonModes (kCFRunLoopCommonModes)
CFRunLoopTimerRef
- CFRunLoopTimerRef 是基于時(shí)間的觸發(fā)器
- CFRunLoopTimerRef 基本上說的就是 NSTimer, 它受 RunLoop 的 Mode 的影響
CFRunLoopSourceRef
- CFRunLoopSourceRef 是事件源 (輸入源)
- 按照官方的文檔, Source 的分類
- Port-Based Source
- Custom Input Sources
- Cocoa Perform Selector
- 按照函數(shù)調(diào)用棧, Source 的分類
- Source0: 非基于 Port 的
- Source1: 基于 Port 的, 通過內(nèi)核和其他線程通信, 接受、分發(fā)系統(tǒng)事件
CFRunLoopObserverRef
- CFRunLoopObserverRef 是觀察者, 能夠箭筒 RunLoop 的改變狀態(tài)
- 可以監(jiān)聽的時(shí)間點(diǎn)有以下幾個(gè):
- kcfRunLoopEntry (即將進(jìn)入 loop ) // 1
- kcfRunLoopBeforeTimers (即將處理 Timer) // 2
- kcfRunLoopBeforeSource (即將處理 source) // 4
- kcfRunLoopBeforeWaiting (即將進(jìn)入休眠) // 32
- kcfRunLoopAfterWaiting (剛從休眠中喚醒) // 64
- kcfRunLoopExit (即將退出 loop) // 128
- 添加觀察者
CFRunLoopObserverRef observer =
CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),
kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer,
CFRunLoopActivity activity) {
NSLog(@"----??????RunLoop????????????---%zd", activity);
});
// ????????????????RunLoop??????
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer,
kCFRunLoopDefaultMode);
// ????Observer
CFRelease(observer);
RunLoop處理邏輯
- 通知 Observer: 即將進(jìn)入 Loop (1)
- 通知 Observer: 將要處理 Timer (2)
- 通知 Observer: 將要處理 Source0 (3)
- 處理 Source0 (4)
- 如果有 Source0, 跳到第9步(5)
- 通知 Observer: 線程即將休眠(6)
- 休眠, 等待喚醒: (7)
- Source0(port).
- timer 啟動
- RunLoop 設(shè)置的 Timer 已經(jīng)超時(shí)
- RunLoop 被外部手動喚醒
- 通知 Observer: 線程將被喚醒 (8)
- 處理未處理的時(shí)間 (9)
- 如果用戶定義的定時(shí)器啟動, 處理定時(shí)器時(shí)間并重啟 RunLoop. 進(jìn)入步驟 (2)
- 如果輸入源啟動, 傳遞相應(yīng)的消息.
- 如果 RunLoop 被顯示喚醒二時(shí)間還沒有超時(shí), 重啟 RunLoop, 進(jìn)入步驟 (2)
- 通知 Observer: 即將退出 Loop
RunLoop的應(yīng)用
- NSTimer
- ImageView 顯示
- PerformSelector
- 常駐線程
- 自動釋放池
RunLoop 定時(shí)源和輸入源
- RunLoop 處理的輸入事件有倆種不同的來源: 輸入源 (input source) 和定時(shí)源 (timer source).
- 輸入源傳遞異步消息, 通常來自于其他線程或程序.
- 定時(shí)源則傳遞同步消息, 在特定時(shí)間或者一定時(shí)間間隔發(fā)生.
NSRunLoop 的實(shí)現(xiàn)機(jī)制, 以及在多線程中如何使用
- 實(shí)現(xiàn)機(jī)制: RunLoop 的基本作用, 處理邏輯.
- 程序創(chuàng)建子程序的時(shí)候, 才需要手動啟動 runLoop. 主線程的 runLoop 已經(jīng)默認(rèn)啟動.
- 在多線程中, 你需要判斷是否需要 RunLoop. 如果需要 RunLoop, 那么你要負(fù)責(zé)配置 RunLoop 并啟動. 你不需要在任何情況下都去啟動 RunLoop. 比如, 你使用線程去處理一個(gè)預(yù)先定義好的耗時(shí)極長的任務(wù)時(shí), 你就可以無需啟動 RunLoop. RunLoop 只在你要和線程有交互事才需要.
RunLoop和線程有什么關(guān)系?
- 主線程的 RunLoop 默認(rèn)是啟動的
iOS的應(yīng)用程序里面, 程序啟動后會有一個(gè)如下的main () 函數(shù)
重點(diǎn)的是 UIApplicationMain()函數(shù), 這個(gè)方法會為 mainThread 設(shè)置一個(gè) RunLoop 對象.int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
這就解釋了: 為什么我們的應(yīng)用可以在無人操作的時(shí)候休息, 需要讓它干活的時(shí)候又能立馬響應(yīng). - 對其他的線程來說, RunLoop 默認(rèn)是沒有啟動的, RunLoop 只有你在要和線程有交互的時(shí)候才有需要.
- 在任何一個(gè) coco 程序中, 都可以通過下面的代碼來獲取當(dāng)前的 RunLoop.
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
autorelease 對象在什么情況下會被釋放?
- 分倆種情況: 手動干預(yù)釋放和系統(tǒng)自動釋放.
- 手動干預(yù)釋放就是指定 autoreleasePool, Autorelease 對象會在當(dāng)前的 runLoop 迭代結(jié)束時(shí)釋放.
- kCFRunLoopEntry(1): 第一次進(jìn)去自動創(chuàng)建一個(gè) autorelease
- kCFRunLoopBeforeWaiting(32): 進(jìn)入休眠狀態(tài)前會自動銷毀一個(gè) autorelease, 然后重新創(chuàng)建一個(gè)新的 autorelease
- kCFRunLoopExit(128): 退出 RunLoop 時(shí)會自動銷毀最后一個(gè)創(chuàng)建的 autorelease
測試, RunLoop 的理解不正確的是?
A 每一個(gè)線程都有其對應(yīng)的RunLoop
B 默認(rèn)非主線程的RunLoop是沒有運(yùn)行的
C 在一個(gè)單獨(dú)的線程中沒有必要去啟用RunLoop
D 可以將NSTimer添加到runloop中
- 參考答案: C
- 理由: RunLoop, 它是多線程的法寶, 通常來說一個(gè)線程一次只執(zhí)行一次任務(wù), 執(zhí)行完任務(wù)會退出線程. 但是, 對于主線程是不能退出的, 因此我們需要讓主線程即時(shí)任務(wù)執(zhí)行完畢, 也可以繼續(xù)等待接受事件而不退出,那么 RunLoop 就成關(guān)鍵法寶了. 但是非主線程通常來說就是為了執(zhí)行某一任務(wù)的, 執(zhí)行完畢冀需要?dú)w還資源, 因此默認(rèn)是不運(yùn)行 RunLoop 的. NSRunLoop 提供了一個(gè)添加 NSTimer 的方法, 這個(gè)方法是正常狀態(tài)下就會回調(diào).
RunLoop 的 Mode 作用是什么?
mode 主要是用來指定時(shí)間在運(yùn)行循環(huán)中的優(yōu)先級, 分為:
- NSDefaultRunLoopMode (kCFRunLoopDefaultMode): 默認(rèn), 空閑狀態(tài)
- UITrackingRunLoopMode: ScrollView 滑動的時(shí)候會切換到這個(gè) mode
- UIInitializationRunLoopMode: RunLoop 啟動時(shí), 會切換到該 mode
- NSRunLoopCommonModes (kCFRunLoopCommonModes) : Mode 集合
蘋果公開提供的 Mode 有倆個(gè):
- NSDefaultRunLoopMode (kCFRunLoopDefaultMode)
- NSRunLoopCommonModes (kCFRunLoopCommonModes)
如果我們把一個(gè) NSTimer 對象以 NSDefaultRunLoopMode (kCFRunLoopDefaultMode) 添加到主運(yùn)行循環(huán)中的時(shí)候, ScrollView 的滑動會導(dǎo)致 Mode 的切換, 而導(dǎo)致 NSTimer 將不再被調(diào)度, 如果希望滑動的時(shí)候也能夠被調(diào)度, 我們就可以是用 NSRunLoopCommonMode (包含, NSDefaultRunLoopMode 和 NSTrackingRunLoopMode 倆個(gè)狀態(tài))
測試, 請寫出 NSTimer 使用時(shí)的注意事項(xiàng)
思路: 如果想要銷毀 timer , 應(yīng)該先把 timer 置為失效, 否則 timer 就一直占用內(nèi)存而不會釋放. 造成邏輯上的內(nèi)存泄漏. 而且這種泄漏不能用 Xcode 和 instruments 測出來. 未將 timer 置為失效, 每次創(chuàng)建一次, 則之前的不能得到釋放, 那么同時(shí)存在多個(gè) timer 的實(shí)例在內(nèi)存中.
參考答案:
- 注意 timer 添加到 runloop 時(shí)應(yīng)該設(shè)置什么 mode.
- 注意timer 在不需要時(shí), 一定要調(diào)用 invalidate 方法使定時(shí)器失效, 否則得不到釋放.
測試, UITableViewCell 上有個(gè) UILabel, 顯示 NSTimer 實(shí)現(xiàn)的秒表時(shí)間, 手指滾動 cell 過程中, label 是否刷新, 為什么?
思路同上, 自己作答.
測試, 為什么 UIScrollView 的滾動會導(dǎo)致 NSTimer 失效?
思路同上, 自己作答.
測試, 在滑動頁面上的列表, timer 會暫推酉拢回調(diào), 為什么? 如何解決?
思路同上, 自己作答.
在開發(fā)中如何使用 RunLoop? 什么應(yīng)用場景?
- 開啟一個(gè)常駐線程 (讓一個(gè)子線程不進(jìn)入消亡狀態(tài), 等待其他線程發(fā)來消息, 處理其他事情)
- 在子線程開啟一個(gè)定時(shí)器
- 在子線程中進(jìn)行一些長期監(jiān)控
- 可以控制定時(shí)器在特定模式下執(zhí)行
- 可以讓某些事件 (行為, 任務(wù)) 在特定模式下執(zhí)行
- 可以添加 Observer 監(jiān)聽 RunLoop 的狀態(tài), 比如監(jiān)聽點(diǎn)擊事件的處理 (在所有點(diǎn)擊事件之前做一些事情)
你在開發(fā)過程中常用到哪些定時(shí)器计盒,定時(shí)器時(shí)間會有誤差嗎卧抗,如果有禀倔,為什么會有誤差熬芜?
iOS中常NSTimer战坤、CADisplayLink曙强、GCD定時(shí)器,其中NSTimer途茫、CADisplayLink基于NSRunLoop實(shí)現(xiàn)碟嘴,故存在誤差,GCD定時(shí)器只依賴系統(tǒng)內(nèi)核囊卜,相對一前兩者是比較準(zhǔn)時(shí)的娜扇。
誤差原因是:與NSRunLoop機(jī)制有關(guān), 因?yàn)镽unLoop每跑完一次圈再去檢查當(dāng)前累計(jì)時(shí)間是否已經(jīng)達(dá)到定時(shí)設(shè)置的間隔時(shí)間栅组,如果未達(dá)到雀瓢,RunLoop將進(jìn)入下一輪任務(wù),待任務(wù)結(jié)束之后再去檢查當(dāng)前累計(jì)時(shí)間玉掸,而此時(shí)的累計(jì)時(shí)間可能已經(jīng)超過了定時(shí)器的間隔時(shí)間刃麸,故會存在誤差。
參考《iOS常見三種定時(shí)器-NSTimer司浪、CADisplayLink泊业、GCD定時(shí)器》
2. NSTimer、CADisplayLink會產(chǎn)生循環(huán)引用嗎啊易?如果會吁伺,你是如何解決的?
如果直接使用租谈,會產(chǎn)生循環(huán)引用問題篮奄。可以增加一個(gè)中間類,給這個(gè)類添加一個(gè)用weak修飾的id 類型target屬性宦搬,并重寫中間類的消息轉(zhuǎn)發(fā)方法牙瓢。實(shí)現(xiàn)如下代碼:
聲明文件.h:
#import <Foundation/Foundation.h>
@interface LXProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;
@end
復(fù)制代碼
實(shí)現(xiàn)文件.m
#import "LXProxy.h"
@interface LXProxy ()
/** weak target*/
@property (nonatomic, weak) id target;
@end
@implementation LXProxy
+ (instancetype)proxyWithTarget:(id)target{
LXProxy *proxy = [LXProxy alloc];
proxy.target = target;
return proxy;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation{
[invocation invokeWithTarget:self.target];
}
@end
復(fù)制代碼
調(diào)用代碼:
_timer = [NSTimer scheduledTimerWithTimeInterval:2 target:[LXProxy proxyWithTarget:self] selector:@selector(test) userInfo:nil repeats:YES];