iOS多線程-RunLoop

前言

基本做用面試

  • 保持程序的持續運行
  • 處理App中的各類事件(好比觸摸事件、定時器事件、Selector事件)
  • 節省CPU資源,提升程序性能(該作事時作事,該休息時休息)

main函數中的RunLoop

最下面的UIApplicationMain函數內部就啓動了一個RunLoop,因此UIApplicationMain就一直沒有返回,保持了程序的持續運行markdown

  • 默認啓動的RunLoop是跟主線程相關的

RunLoop

iOS有兩套API來訪問和使用RunLoop網絡

  1. Foundation
  • NSRunLoop
  1. Core Foundation
  • CFRunLoopRef

NSRunLoop和CFRunLoopRef都表明着RunLoop對象 可是NSRunLoop是基於CFRunLoopRef的一層OC包裝 ,因此更底層的是CFRunLoopRef異步

RunLoop與線程

  • 每條線程都有惟一的一個與之對應的RunLoop對象
  • 主線程的RunLoop以及自動建立好了,子線程的RunLoop須要主動建立
  • RunLoop在第一次獲取時建立,在線程結束時銷燬

得到RunLoop對象

是如何保證每條線程有惟一一個對應的RunLoop對象的呢?函數

  • 系統會先判斷是否有RunLoop存在,若是不存在,就會建立一個RunLoop,並建立一個字典,裏面存放了線程-線程對應的RunLoop

注意,經過NSRunLoop和CFRunLoopRef獲得的RunLoop也仍是不一樣的對象,可是能夠經過.getCFRunLoop將NSRunLoop轉爲CFRunLoopRefoop

子線程的RunLoop直接經過[NSThread currentThread]方法建立獲得,實際上是懶加載的性能

RunLoop相關類

Runloop的五個類atom

  1. CFRunLoopRef:表明 RunLoop 的對象
  2. CFRunLoopModeRef:表明 RunLoop 的運行模式
  3. CFRunLoopSourceRef:就是 RunLoop 模型圖中提到的輸入源 / 事件源
  4. CFRunLoopTimerRef:就是 RunLoop 模型圖中提到的定時源
  5. CFRunLoopObserverRef:觀察者,可以監聽 RunLoop 的狀態改變

這五個類的關係:spa

  • 在runloop(CFRunLoopRef)中有多個運行模式(CFRunLoopModeRef),可是runloop只能選擇一種運行模式,這個mode就叫作CurrentMode(就比如空調有製冷制熱等多種模式,可是每次開啓空調只能選擇一種模式)
  • 若是須要切換mode,只能退出runloop,再從新指定一個mode進入
    • 這樣作主要是爲分隔開不一樣組的source/timer/observer(CFRunLoopSourceRef/CFRunLoopTimerRef/CFRunLoopObserverRef),讓其不相互影響

  • 每一個mode裏面至少要有一個timer或者是source,只有一個observer是不行的
  • 每一個mode能夠包含若干個ource/timer/observer

CFRunLoopModeRef

系統默認註冊了五個mode 線程

應用場景一

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self timer];
}

-(void)timer{
    //1. 建立計時器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //2. 將計時器添加到runloop中
    //第一個參數是計時器,第二個參數是runloop的mode,這裏選擇默認mode
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
    
}

-(void)run{
    NSLog(@"run---%@----%@",[NSThread currentThread],[NSRunLoop currentRunLoop].currentMode);
}
複製代碼

當你點擊模擬器時,打印結果以下,能夠看到每隔2s進行一次打印

2021-03-10 10:37:05.571371+0800 runloop1[1771:51979] run---<NSThread: 0x600001f44a00>{number = 1, name = main}----kCFRunLoopDefaultMode
2021-03-10 10:37:07.572174+0800 runloop1[1771:51979] run---<NSThread: 0x600001f44a00>{number = 1, name = main}----kCFRunLoopDefaultMode
2021-03-10 10:37:09.571467+0800 runloop1[1771:51979] run---<NSThread: 0x600001f44a00>{number = 1, name = main}----kCFRunLoopDefaultMode
複製代碼

可是當你向storyboard中添加一個textView會發生什麼狀況呢?

咱們來看看場景二

應用場景二

你會發現當你點擊背景時,正常每隔2s進行打印,可是當你滑動textView時,打印中止,且你再中止滑動,打印又從新開始

這是爲何?

  • 由於在你拖動textView後,runloop會自動進入到頁面追蹤模式,當進入頁面追蹤模式後,就不會再理會計時器了
  • 當你中止拖動後,runloop又自動進入到默認模式,timer繼續運行,因此會繼續打印

解決方法,修改runloop的mode類型,改成UITrackingRunLoopMode(界面追蹤模式)

-(void)timer{
    //1. 建立計時器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //2. 將計時器添加到runloop中
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:UITrackingRunLoopMode];
}
複製代碼

從新運行,會發現當你改成滑動textView時,打印結果以下

2021-03-10 10:55:22.039501+0800 runloop1[2084:70476] run---<NSThread: 0x600002eb4880>{number = 1, name = main}----UITrackingRunLoopMode
2021-03-10 10:55:22.367009+0800 runloop1[2084:70476] run---<NSThread: 0x600002eb4880>{number = 1, name = main}----UITrackingRunLoopMode
複製代碼

能夠看到此時的模式是界面追蹤模式

那麼如何既點擊view時啓動計時器打印,拖動textview時也打印呢?

接下來進入下一場景

應用場景三

但願達到 既點擊view時啓動計時器打印,拖動textview時也打印 有兩種方式:

  1. 添加兩種方式
-(void)timer{
    //1. 建立計時器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //2. 將計時器添加到runloop中
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:UITrackingRunLoopMode];
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
}
複製代碼

打印結果以下

2021-03-10 11:01:20.588388+0800 runloop1[2269:79479] run---<NSThread: 0x60000038c240>{number = 1, name = main}----kCFRunLoopDefaultMode
2021-03-10 11:01:22.588833+0800 runloop1[2269:79479] run---<NSThread: 0x60000038c240>{number = 1, name = main}----kCFRunLoopDefaultMode
2021-03-10 11:01:24.588094+0800 runloop1[2269:79479] run---<NSThread: 0x60000038c240>{number = 1, name = main}----UITrackingRunLoopMode
2021-03-10 11:01:26.588974+0800 runloop1[2269:79479] run---<NSThread: 0x60000038c240>{number = 1, name = main}----UITrackingRunLoopMode
複製代碼
  1. 使用佔位用mode:NSRunLoopCommonModes

NSRunLoopCommonModes = kCFRunLoopDefaultMode + UITrackingRunLoopMode

  • 佔用其實就是一種標籤,凡是添加到NSRunLoopCommonModes中的事件都會被同時添加到大賞common標籤的運行模式上
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
複製代碼

定時器的建立有兩種方式,上面的場景中使用的是第一種,也就是timerWithTimeInterval的方式,接下來的場景咱們使用第二種方式scheduledTimerWithTimeInterval

應用場景四:

回憶一下咱們剛剛上面建立定時器的方法還須要本身將定時器添加到runloop當中,可是scheduledTimerWithTimeInterval建立的定時器是不須要這樣的,系統會幫你作,而且設置運行模式位默認mode

-(void)timer2{
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}
複製代碼

還有一個問題,若是咱們的timer是在子線程中建立的,會出現什麼問題?

  • 很顯然,不會有任何反應

這是爲何呢?緣由很簡單

  • 由於你的timer是在子線程中運行的,可是你的子線程並無與之對應的runloop(由於你沒有建立)
  • 因此只要咱們再手動建立子線程對應的runloop便可
  • 主線程的runloop默認建立,子線程的runloop須要手動建立
-(void)timer2{
    //1. 建立子線程的runloop
    NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
    //2. 建立計時器
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //3. 啓動runloop
    [currentRunLoop run];
}
複製代碼

RunLoop應用

  • NSTimer
  • 常駐線程
  • ImageView顯示
  • 自動釋放池
  • performSelector

重點來看這個常駐線程

  • 通常的子線程,在線程裏的任務都執行完畢後就會進入死亡狀態
  • 這時候即使start也沒法從新開啓
  • 爲了讓咱們的線程知足咱們須要使用時就使用,不須要使用時就處於等待狀態,隨時能夠從新使用呢?

這時候就須要咱們的常駐線程了

注意,咱們想要達到的目的是讓一個線程不在他的任務執行完畢後就死亡,而是進入等待模式,在須要時再從新使用該線程

解決方法:

  • 開啓線程的runloop
  • 咱們知道runloop是能夠知足該作事時作事,該休息時休息的要求

接下來進入具體應用狀況

定義三個按鈕以下

  • 首先,咱們定義一個線程屬性
@property(nonatomic,strong) NSThread *thread;
複製代碼
  • 點擊建立線程按鈕時建立線程
    • 注意咱們在建立線程的同時調用了createRunLoop方法來建立runloop
- (IBAction)createClickBtn:(id)sender {
    // 建立線程
    self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(createRunLoop) object:nil];
    [self.thread start]; 
}
複製代碼

建立runloop有兩種設置方式

  1. 一種是設置timer
- (void)createRunLoop{
    //1. 得到子線程對應的runloop
    NSRunLoop *currentLoop = [NSRunLoop currentRunLoop];
    //2. 在runloop中設置一個timer
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //3. 將timer添加到runloop中
    [currentLoop addTimer:timer forMode:NSDefaultRunLoopMode];
    //4. 啓動runloop
    [currentLoop run];
}
複製代碼
  1. 一種是設置source(這個方式更可取,由於咱們沒有必要設置一個計時器)
- (void)createRunLoop{
    //1. 得到子線程對應的runloop
    NSRunLoop *currentLoop = [NSRunLoop currentRunLoop];
    //2. 在runloop中設置一個source
    [currentLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    //3. 啓動runloop
    [currentLoop run];
}
複製代碼
  • 定義任務
- (IBAction)task1ClickBtn:(id)sender {
    [self performSelector:@selector(task1) onThread:self.thread withObject:nil waitUntilDone:YES];
}


- (IBAction)task2ClickBtn:(id)sender {
    [self performSelector:@selector(task2) onThread:self.thread withObject:nil waitUntilDone:YES];
}

-(void)task1{
    NSLog(@"task1---%@",[NSThread currentThread]);
}


-(void)task2{
    NSLog(@"task2---%@",[NSThread currentThread]);
}

//這個run函數是用於計時器狀況時的
-(void)run{
    NSLog(@"%s",__func__);
}
複製代碼

運行結果以下

能夠看到當你交換點擊兩個按鈕時,任務也是交替執行的,而且是在同一線程下

RunLoop面試題

  1. 什麼是runloop?

  1. runloop的處理邏輯

  1. 自動釋放池何時釋放
  • 第一次建立:runloop啓動
  • 最後一次銷燬:runloop退出
  • 其餘時候的建立和銷燬:當runloop即將休眠的時候銷燬以前的釋放池,被喚醒時就從新建立一個新的
  1. observer能夠用來作什麼?
  • 監聽runloop狀態
  1. 在開發中如何使用runloop?什麼應用場景?
  2. 開啓一個常駐線程(讓一個子線程不進入消亡狀態,等待其餘線程發來消息,處理其餘事件)

例如一個網絡請求,由於網絡請求是異步的,且比較耗時,因此咱們能夠建立一個子線程來負責網絡請求的功能

  • 在子線程中開啓一個定時器
  • 在子線程中進行一些長期監控

相關文章
相關標籤/搜索