RunLoop

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)建一個常駐線程?

參考ibireme 深入理解RunLoop

Runloop應(yīng)用

iOS實時卡頓監(jiān)控

推薦文章:

ibireme 深入理解RunLoop
iOS線下分享《RunLoop》by 孫源@sunnyxx
黑幕背后的Autorelease by sunnyxx
iOS Runloop實踐(常駐線程)
iOS刨根問底-深入理解RunLoop
Runloop應(yīng)用舉例
iOS RunLoop入門小結(jié)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容