Runloop作用
- 1.保證當(dāng)前線程不退出。
- 2.監(jiān)聽事件:觸摸事件、時鐘事件和網(wǎng)絡(luò)事件。
- 3.節(jié)約資源:有事件時,處理事件。沒有事件,處于休眠狀態(tài)。
ps:事件產(chǎn)生到有結(jié)果的過程:
硬件設(shè)備接收信號 > 電信號轉(zhuǎn)化成模擬信號 > 操作系統(tǒng)接收信號 > 找到響應(yīng)的應(yīng)用程序 > 找到具體的某個類的某個方法執(zhí)行。
時鐘事件和Runloop關(guān)系
案列一:
- (void)viewDidLoad {
[super viewDidLoad];
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
}
- (void)timerMethod {
static int a = 0;
NSLog(@"%d---%@",a,[NSThread currentThread]);
}
該方法已經(jīng)將時鐘事件添加到當(dāng)前Runloop中,無需程序員操作什么。只是此時Runloop模式是默認模式。
案列二:
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode: NSRunLoopCommonModes];
}
- (void)timerMethod {
static int a = 0;
NSLog(@"%d---%@",a,[NSThread currentThread]);
}
該方法返回一個NSTimer對象,通過加入到Runloop的占位模式下,開啟定時服務(wù)。
Runloop的常見模式
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
默認模式,APP主線程是在該模式下運行。 - UITrackingRunLoopMode
UI模式,ScrollView滑動時的模式,其優(yōu)先級高于默認模式。 - UIInitializationRunLoopMode
啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用。 - NSRunLoopCommonModes(kCFRunLoopCommonModes)
占位模式,包含默認模式和UI模式。 - 更多模式
蘋果公開提供的 Mode 有三個:
- NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
- UITrackingRunLoopMode
- NSRunLoopCommonModes(kCFRunLoopCommonModes)
note:Runloop在同一段時間只能并且必須在一種特定的Mode下運行。更換mode時,需要停止當(dāng)前Loop,然后重啟新Loop。
理解ibireme 深入理解RunLoopRunLoop 的 Mode:
個人理解_commonModes和_commonModeItems關(guān)系:
以Timer在默認模式和UI模式(即占位模式)下為例。主線程的 RunLoop 里有兩個預(yù)置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。這兩個 Mode 都已經(jīng)被標記為”Common”屬性,就是說這兩個Mode已經(jīng)被添加到_commonModes中,并且Timer這個item已經(jīng)被加到_commonModeItems中。當(dāng)滑動Scrollview時,Runloop會退出,重新指定Mode為UI模式,再次開啟Runloop,RunLoop 會自動將 _commonModeItems 里的Timer 同步到具有 “Common” 標記的所有Mode里。
問題探索
問題一:
為哈蘋果建議時鐘事件添加到Runloop的默認模式下,而不放到UI模式中呢?
舉個例子:
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:UITrackingRunLoopMode];
}
- (void)timerMethod {
sleep(1.0);
static int a = 0;
NSLog(@"%d---%@",a,[NSThread currentThread]);
}
如果時鐘事件添加到UI模式下,在timer的回調(diào)方法中添加一個耗時操作,會阻塞主線程,出現(xiàn)UI卡頓。
問題二:
開啟一條子線程,執(zhí)行放到RunLoop中的timer定時器,定時器的seletor為什么不執(zhí)行?
代碼如下:
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
}];
[thread start];
}
- (void)timerMethod{
static NSInteger count = 0;
NSLog(@"count = %ld",count ++);
}
原因:子線程死掉,NSthread負責(zé)開辟一條線程,CPU負責(zé)調(diào)度線程,線程中任務(wù)一旦執(zhí)行完成就會釋放,若想保住子線程,開啟一個死循環(huán)。
代碼修改如下:
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
while (true) {
}
}];
[thread start];
}
- (void)timerMethod{
static NSInteger count = 0;
NSLog(@"count = %ld",count ++);
}
雖然開啟一個死循環(huán)保住了子線程,此時的timerMethod方法依然沒有執(zhí)行,為什么?
原因:雖然在子線程中把timer放到RunLoop中,也保住了子線程,但是死循環(huán)中并沒有操作什么,需要的是把子線程的RunLoop并沒有從Event隊列(消息隊列)中取出處理。
代碼再次修改如下:
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop]run];
}];
[thread start];
}
- (void)timerMethod{
static NSInteger count = 0;
NSLog(@"count = %ld",count ++);
}
如果需要手動停止這個線程?
代碼三次修改如下:
- (void)viewDidLoad {
[super viewDidLoad];
isFinished = YES;
NSThread *thread = [[NSThread alloc]initWithBlock:^{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
while (isFinished) {
[[NSRunLoop currentRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.0001]];
}
}];
[thread start];
}
- (void)timerMethod{
static NSInteger count = 0;
NSLog(@"count = %ld",count ++);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
isFinished = NO;
}
上述代碼中為什么Runloop模式都是NSDefaultRunLoopMode?
原因:在子線程中使用默認模式,就可以,沒有必要使用占位模式,子線程中使用默認模式并不會阻塞主線程。
問題三:
每一條線程都有一個runloop這種說法對嗎?
不對,原因:創(chuàng)建了一個線程,RunLoop并未創(chuàng)建,但是當(dāng)你第一次獲取時,就會創(chuàng)建一個RunLoop, 再次獲取時,拿到的只是第一次獲取的RunLoop。(有點類似于懶加載)當(dāng)線程退出([NSThread exit], RunLoop釋放。
問題四:
主線程退出,對子線程有影響嗎?
沒有,主線程也是一條線程,主線程死掉,子線程依然可以正常運行。
問題五:
主線程為什么只有一個?
原因:線程之間訪問資源存在資源搶奪的問題,假如存在兩條線程就需要對同一個資源進行加鎖,這樣APP的流暢性就會降低,所以只會開辟一條主線程。
問題擴展一:
NSTimer定時器時間不精確原因?
一個循環(huán)中如果RunLoop沒有被識別(這個時間大概在50-100ms)或者說當(dāng)前RunLoop在執(zhí)行一個長的call out(例如執(zhí)行某個循環(huán)操作)則NSTimer可能就會存在誤差,RunLoop在下一次循環(huán)中繼續(xù)檢查并根據(jù)情況確定是否執(zhí)行。
問題擴展二:
performSelector:withObject:afterDelay:本質(zhì)?
performSelector:withObject:afterDelay:執(zhí)行的本質(zhì)還是通過創(chuàng)建一個NSTimer然后加入到當(dāng)前線程RunLoop。(類似的還有performSelector:onThread:withObject:afterDelay:,只是它會在另一個線程的RunLoop中創(chuàng)建一個Timer),所以此方法事實上在任務(wù)執(zhí)行完之前會對觸發(fā)對象形成引用,任務(wù)執(zhí)行完進行釋放(注意:performSelector: withObject:等方法則等同于直接調(diào)用,原理與此不同)。
相應(yīng)的方法還有:
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
針對下面的幾個方法,則是創(chuàng)建了一個source0事件。
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
擴展問題來自: iOS刨根問底-深入理解RunLoop
觸摸事件與Runloop
觸摸事件又叫事件源(輸入源),對應(yīng)于coreFoundation框架中的CFRunLoopSourceRef。
事件源分類
- source0:非基于Port的,用于用戶主動觸發(fā)的事件。諸如UIEvent(觸摸,滑動等),performSelector這種需要手動觸發(fā)的操作。
- source1:基于Port的系統(tǒng)內(nèi)核事件,可以通過內(nèi)核和其他線程相互發(fā)送消息 。
問題六:
線程之間怎么進行通訊?
- (void)viewDidLoad {
[super viewDidLoad];
isFinished = YES;
}
- (void)timerMethod{
static NSInteger count = 0;
NSLog(@"count = %ld",count ++);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSThread *thread = [[NSThread alloc]initWithBlock:^{
while (isFinished) {
[[NSRunLoop currentRunLoop]run];
}
}];
[thread start];
[self performSelector:@selector(otherMethod) onThread:thread withObject:nil waitUntilDone:NO];
}
- (void)otherMethod {
for (NSInteger i = 0; i < 10; i++) {
NSLog(@"i = %ld currentThread = %@",i,[NSThread currentThread]);
}
isFinished = NO;
}
線程之間通過Source事件進行通訊。
CFRunloopObserverRef與Runloop
CFRunLoopObserverRef是觀察者,能夠監(jiān)聽RunLoop的狀態(tài)改變。
可以監(jiān)聽的狀態(tài)有:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即將進入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), //即將處理Timer
kCFRunLoopBeforeSources = (1UL << 2), //即將處理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), //即將推出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
問題七
autoreleasepool什么時候釋放?
盜用一張圖,看一下Runloop內(nèi)部邏輯:

上圖地址
- 即將進入Loop,其會調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池。
- BeforeWaiting(準備進入休眠) 時調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池。
ps:問題七答案可以在ibireme 深入理解RunLoop找到。
問題八
AFNetworking為什么要有一個常駐線程?
使用NSURLConnection有幾種選擇:
A.在主線程調(diào)異步接口
若直接在主線程調(diào)用異步接口,會有個Runloop相關(guān)的問題:當(dāng)在主線程調(diào)用 [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES] 時,請求發(fā)出,偵聽任務(wù)會加入到主線程的 Runloop 下,RunloopMode 會默認為 NSDefaultRunLoopMode。這表明只有當(dāng)前線程的Runloop 處于 NSDefaultRunLoopMode 時,這個任務(wù)才會被執(zhí)行。但當(dāng)用戶滾動 tableview 或 scrollview 時,主線程的 Runloop 是處于 NSEventTrackingRunLoopMode 模式下的,不會執(zhí)行 NSDefaultRunLoopMode 的任務(wù),所以會出現(xiàn)一個問題,請求發(fā)出后,如果用戶一直在操作UI上下滑動屏幕,那在滑動結(jié)束前是不會執(zhí)行回調(diào)函數(shù)的,只有在滑動結(jié)束,RunloopMode 切回 NSDefaultRunLoopMode,才會執(zhí)行回調(diào)函數(shù)。蘋果一直把動畫效果性能放在第一位,估計這也是蘋果提升UI動畫性能的手段之一。
所以若要在主線程使用 NSURLConnection 異步接口,需要手動把 RunloopMode 設(shè)為 NSRunLoopCommonModes。這個 mode 意思是無論當(dāng)前 Runloop 處于什么狀態(tài),都執(zhí)行這個任務(wù)。
NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];
B.在子線程調(diào)同步接口
若在子線程調(diào)用同步接口,一條線程只能處理一個請求,因為請求一發(fā)出去線程就阻塞住等待回調(diào),需要給每個請求新建一個線程,這是很浪費的,這種方式唯一的好處應(yīng)該是易于控制請求并發(fā)的數(shù)量。
C.在子線程調(diào)異步接口
子線程調(diào)用異步接口,子線程需要有 Runloop 去接收異步回調(diào)事件,這里也可以每個請求都新建一條帶有 Runloop 的線程去偵聽回調(diào),但這一點好處都沒有,既然是異步回調(diào),除了處理回調(diào)內(nèi)容,其他時間線程都是空閑可利用的,所有請求共用一個響應(yīng)的線程就夠了。
AFNetworking 用的就是第三種方式,創(chuàng)建了一條常駐線程專門處理所有請求的回調(diào)事件,這個模型跟 nodejs 有點類似。網(wǎng)絡(luò)請求回調(diào)處理完,組裝好數(shù)據(jù)后再給上層調(diào)用者回調(diào),這時候回調(diào)是拋回主線程的,因為主線程是最安全的,使用者可能會在回調(diào)中更新UI,在子線程更新UI會導(dǎo)致各種問題,一般使用者也可以不需要關(guān)心線程問題。
答案來自這里:AFNetworking2.0的源碼解析
問題九
怎么創(chuàng)建一個常駐線程?
Runloop應(yīng)用
推薦文章:
ibireme 深入理解RunLoop
iOS線下分享《RunLoop》by 孫源@sunnyxx
黑幕背后的Autorelease by sunnyxx
iOS Runloop實踐(常駐線程)
iOS刨根問底-深入理解RunLoop
Runloop應(yīng)用舉例
iOS RunLoop入門小結(jié)