iOS——RunLoop

一、RunLoop簡介

Run 表示運行,Loop 表示循環。結合在一起就是運行循環的意思。直觀理解就像是不停的跑圈。

  • RunLoop 實際上是一個對象,這個對象在循環中用來處理程序運行過程中出現的各種事件(比如說觸摸事件、UI刷新事件、定時器事件、Selector事件),從而保持程序的持續運行。
  • RunLoop 在沒有事件處理的時候,會使線程進入睡眠模式,從而節省 CPU 資源,提高程序性能。

二、RunLoop和線程

RunLoop 和線程是息息相關的,我們知道線程的作用是用來執行特定的一個或多個任務,在默認情況下,線程執行完之後就會退出,就不能再執行任務了。這時我們就需要採用一種方式來讓線程能夠不斷地處理任務,並不退出。所以,我們就有了 RunLoop。

  1. 一條線程對應一個RunLoop對象,每條線程都有唯一一個與之對應的 RunLoop 對象。
  2. RunLoop 並不保證線程安全。我們只能在當前線程內部操作當前線程的 RunLoop 對象,而不能在當前線程內部去操作其他線程的 RunLoop 對象方法。
  3. RunLoop 對象在第一次獲取 RunLoop 時創建,銷燬則是在線程結束的時候。
  4. 主線程的 RunLoop 對象系統自動幫助我們創建好了,而子線程的 RunLoop對象需要我們主動創建和維護。

下圖是蘋果官方給出的 RunLoop 模型圖。

從上圖中可以看出,RunLoop 就是線程中的一個循環,RunLoop 會在循環中會不斷檢測,通過 Input sources(輸入源)和 Timer sources(定時源)兩種來源等待接受事件;然後對接受到的事件通知線程進行處理,並在沒有事件的時候讓線程進行休息。

 a:默認情況下主線程的RunLoop原理

我們在啓動一個iOS程序的時候,系統會調用創建項目時自動生成的 main.m 的文件。main.m文件如下所示:

#import <UIKit/UIKit.h>
#import "AppDelegate.h"

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

其中 UIApplicationMain 函數內部幫我們開啓了主線程的 RunLoop,UIApplicationMain 內部擁有一個無限循環的代碼,只要程序不退出/崩潰,它就一直循環。上邊的代碼中主線程開啓 RunLoop 的過程可以簡單的理解爲如下代碼:

int main(int argc, char * argv[]) {
    
    BOOL running= YES;
    
    do {
        
        //執行各種r任務,處理各種事件
        
    } while (running);//判斷是否需要退出
    
}

從上邊可看出,程序一直在 do-while 循環中執行,所以 UIApplicationMain 函數一直沒有返回,我們在運行程序之後程序不會馬上退出,會保持持續運行狀態。

三、RunLoop相關類

下面我們來了解一下Core Foundation框架下關於 RunLoop 的 5 個類,只有弄懂這幾個類的含義,我們才能深入瞭解 RunLoop 的運行機制。

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

5 個類的相互關係:

一個RunLoop對象(CFRunLoopRef)中包含若干個運行模式(CFRunLoopModeRef)。而每一個運行模式下又包含若干個輸入源(CFRunLoopSourceRef)、定時源(CFRunLoopTimerRef)、觀察者(CFRunLoopObserverRef)。

  • 每次 RunLoop 啓動時,只能指定其中一個運行模式(CFRunLoopModeRef),這個運行模式(CFRunLoopModeRef)被稱作當前運行模式(CurrentMode)。
  • 如果需要切換運行模式(CFRunLoopModeRef),只能退出當前 Loop,再重新指定一個運行模式(CFRunLoopModeRef)進入。
  • 這樣做主要是爲了分隔開不同組的輸入源(CFRunLoopSourceRef)、定時源(CFRunLoopTimerRef)、觀察者(CFRunLoopObserverRef),讓其互不影響 。

1、CFRunLoopRef類

CFRunLoopRef 是 Core Foundation 框架下 RunLoop 對象類。我們可通過以下方式來獲取 RunLoop 對象:

a:Core Foundation

    //獲取當前線程的RunLoop對象
    CFRunLoopRef currentRunLoop= CFRunLoopGetCurrent();
    
    //獲得主線程的RunLoop對象
    CGFunctionRef mainRunLoop=CFRunLoopGetMain();

b: 在Foundation 框架下獲取 RunLoop 對象類的方法如下

    //獲得當前線程的Runloop對象
    NSRunLoop *currentRunLoop=[NSRunLoop currentRunLoop];
    
    //獲得主線程的RunLoop對象
    NSRunLoop *mainRunLoop=[NSRunLoop mainRunLoop];

2、CFRunLoopModeRef

 系統默認定義了多種運行模式(CFRunLoopModeRef),如下:

  1. kCFRunLoopDefaultMode:(NSDefaultRunLoopMode)App的默認運行模式,通常主線程是在這個運行模式下運行
  2. UITrackingRunLoopMode:(UITrackingRunLoopMode)跟蹤用戶交互事件(用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他Mode影響)
  3. UIInitializationRunLoopMode:在剛啓動App時第進入的第一個 Mode,啓動完成後就不再使用
  4. GSEventReceiveRunLoopMode:接受系統內部事件,通常用不到
  5. kCFRunLoopCommonModes:(NSRunLoopCommonModes)僞模式,不是一種真正的運行模式(後邊會用到)

其中kCFRunLoopDefaultModeUITrackingRunLoopModekCFRunLoopCommonModes是我們開發中需要用到的模式,

3、CFRunLoopTimerRef

CFRunLoopTimerRef是定時源(RunLoop模型圖中提到過),理解爲基於時間的觸發器,基本上就是NSTimer(哈哈,這個理解就簡單了吧)。

下面我們來演示下CFRunLoopModeRef和CFRunLoopTimerRef結合的使用用法,從而加深理解。

在主界面添加一個textView

- (void)viewDidLoad {
    [super viewDidLoad];
   
    
    NSTimer *timer=[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
    //將定時器添加到當前RunLoop的NSDefaultRunLoopMode下
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
}

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

1)、運行上面的代碼發現如果我們不對模擬器進行任何操作的話,定時器會每隔1秒調用run方法。

2)、當我們拖動TextView時,run方法不再調用,也就是說NSTimer不工作了,而當我們鬆開的時候,NSTimer就又開始正常工作了。

這是因爲:

  • 當我們不做任何操作的時候,RunLoop處於NSDefaultRunLoopMode下。
  • 而當我們拖動Text View的時候,RunLoop就結束NSDefaultRunLoopMode,切換到了UITrackingRunLoopMode模式下,這個模式下沒有添加NSTimer,所以我們的NSTimer就不工作了。
  • 但當我們鬆開鼠標的時候,RunLoop就結束UITrackingRunLoopMode模式,又切換回NSDefaultRunLoopMode模式,所以NSTimer就又開始正常工作了。

你可以試着將上述代碼中的[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];語句換爲[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];,也就是將定時器添加到當前RunLoop的UITrackingRunLoopMode下,你就會發現定時器只會在拖動Text View的模式下工作,而不做操作的時候定時器就不工作。

那難道我們就不能在這兩種模式下讓NSTimer都能正常工作嗎?

當然可以,這就用到了我們之前說過的僞模式(kCFRunLoopCommonModes),這其實不是一種真實的模式,而是一種標記模式,意思就是可以在打上Common Modes標記的模式下運行。

那麼哪些模式被標記上了Common Modes呢?

NSDefaultRunLoopModeUITrackingRunLoopMode

所以我們只要我們將NSTimer添加到當前RunLoop的kCFRunLoopCommonModes(Foundation框架下爲NSRunLoopCommonModes)下,我們就可以讓NSTimer在不做操作和拖動Text View兩種情況下愉快的正常工作了。

具體做法就是講添加語句改爲

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

備註:NSTimer中的scheduledTimerWithTimeInterval方法和RunLoop的關係。添加下面的代碼:

 [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

這句代碼調用了scheduledTimer返回的定時器,NSTimer會自動被加入到了RunLoop的NSDefaultRunLoopMode模式下。這句代碼相當於下面兩句代碼:

NSTimer *timer=[NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    //將定時器添加到當前RunLoop的NSDefaultRunLoopMode下
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

4、CFRunLoopSourceRef

CFRunLoopSourceRef是事件源(RunLoop模型圖中提到過),CFRunLoopSourceRef有兩種分類方法。

  • 第一種按照官方文檔來分類(就像RunLoop模型圖中那樣):
    • Port-Based Sources(基於端口)
    • Custom Input Sources(自定義)
    • Cocoa Perform Selector Sources
  • 第二種按照函數調用棧來分類:
    • Source0 :非基於Port
    • Source1:基於Port,通過內核和其他線程通信,接收、分發系統事件

這兩種分類方式其實沒有區別,只不過第一種是通過官方理論來分類,第二種是在實際應用中通過調用函數來分類。

下邊我們舉個例子大致來了解一下函數調用棧和Source。

  1. 在我們的項目中的Main.storyboard中添加一個Button按鈕,並添加點擊動作。
  2. 然後在點擊動作的代碼中加入一句輸出語句,並打上斷點,如下圖所示:
  3. 然後運行程序,並點擊按鈕。
  4. 然後在項目中單擊圖紅色部分。
  5. 可以看到如下圖所示就是點擊事件產生的函數調用棧。

所以點擊事件是這樣來的:

  1. 首先程序啓動,調用18行的main函數,main函數調用17行UIApplicationMain函數,然後一直往上調用函數,最終調用到0行的BtnClick函數,即點擊函數。

  2. 同時我們可以看到12行中有Sources0,也就是說我們點擊事件是屬於Sources0函數的,點擊事件就是在Sources0中處理的。

  3. 而至於Sources1,則是用來接收、分發系統事件,然後再分發到Sources0中處理的。

5、CFRunLoopObserverRef

CFRunLoopObserverRef是觀察者,用來監聽RunLoop的狀態改變

CFRunLoopObserverRef可以監聽的狀態改變有以下幾種:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),               // 即將進入Loop:1
    kCFRunLoopBeforeTimers = (1UL << 1),        // 即將處理Timer:2    
    kCFRunLoopBeforeSources = (1UL << 2),       // 即將處理Source:4
    kCFRunLoopBeforeWaiting = (1UL << 5),       // 即將進入休眠:32
    kCFRunLoopAfterWaiting = (1UL << 6),        // 即將從休眠中喚醒:64
    kCFRunLoopExit = (1UL << 7),                // 即將從Loop中退出:128
    kCFRunLoopAllActivities = 0x0FFFFFFFU       // 監聽全部狀態改變  
};

下邊我們通過代碼來監聽下RunLoop中的狀態改變。

- (void)viewDidLoad {
    [super viewDidLoad];
   
   
    // 創建觀察者
    CFRunLoopObserverRef observer= CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"監聽RunLoop變化:%zd",activity);
        
    });
    
    //添加觀察者到當前RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    
    //釋放
    CFRelease(observer);
    
  
}

2019-04-22 16:27:17.752849+0800 RunLoop[18944:4578383] 監聽RunLoop變化:2
2019-04-22 16:27:17.752998+0800 RunLoop[18944:4578383] 監聽RunLoop變化:4
2019-04-22 16:27:17.753053+0800 RunLoop[18944:4578383] 監聽RunLoop變化:2
2019-04-22 16:27:17.753148+0800 RunLoop[18944:4578383] 監聽RunLoop變化:4
2019-04-22 16:27:17.753277+0800 RunLoop[18944:4578383] 監聽RunLoop變化:2
2019-04-22 16:27:17.753326+0800 RunLoop[18944:4578383] 監聽RunLoop變化:4
2019-04-22 16:27:17.753866+0800 RunLoop[18944:4578383] 監聽RunLoop變化:2

可以看到RunLoop的狀態在不斷的改變,最終變成了狀態 32,也就是即將進入睡眠狀態,說明RunLoop之後就會進入睡眠狀態。

四、RunLoop原理

 

這張圖對於我們理解RunLoop來說太有幫助了,下邊我們可以來說下官方文檔給我們的RunLoop邏輯。

在每次運行開啓RunLoop的時候,所在線程的RunLoop會自動處理之前未處理的事件,並且通知相關的觀察者。

具體的順序如下:

  1. 通知觀察者RunLoop已經啓動
  2. 通知觀察者即將要開始的定時器
  3. 通知觀察者任何即將啓動的非基於端口的源
  4. 啓動任何準備好的非基於端口的源
  5. 如果基於端口的源準備好並處於等待狀態,立即啓動;並進入步驟9
  6. 通知觀察者線程進入休眠狀態
  7. 將線程置於休眠知道任一下面的事件發生:
    • 某一事件到達基於端口的源
    • 定時器啓動
    • RunLoop設置的時間已經超時
    • RunLoop被顯示喚醒
  8. 通知觀察者線程將被喚醒
  9. 處理未處理的事件
    • 如果用戶定義的定時器啓動,處理定時器事件並重啓RunLoop。進入步驟2
    • 如果輸入源啓動,傳遞相應的消息
    • 如果RunLoop被顯示喚醒而且時間還沒超時,重啓RunLoop。進入步驟2
  10. 通知觀察者RunLoop結束。

 

五、RunLoop應用

a、NSTimer  b、ImageView顯示:控制方法在特定的模式下可用 c、常駐線程:在子線程中開啓一個RunLoop d、自動釋放池

1、ImageView推遲顯示

有時候,我們會遇到這種情況:
當界面中含有UITableView,而且每個UITableViewCell裏邊都有圖片。這時候當我們滾動UITableView的時候,如果有一堆的圖片需要顯示,那麼可能會出現卡頓的現象。

怎麼解決這個問題呢?

這時候,我們應該推遲圖片的顯示,也就是ImageView推遲顯示圖片。有兩種方法:

a 監聽UIScrollView的滾動

因爲UITableView繼承自UIScrollView,所以我們可以通過監聽UIScrollView的滾動,實現UIScrollView相關delegate即可。

b. 利用PerformSelector設置當前線程的RunLoop的運行模式

利用performSelector方法爲UIImageView調用setImage:方法,並利用inModes將其設置爲RunLoop下NSDefaultRunLoopMode運行模式。代碼如下:

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"tupianName"] afterDelay:4.0 inModes:NSDefaultRunLoopMode];

2、後臺常駐線程(很常用)

我們在開發應用程序的過程中,如果後臺操作特別頻繁,經常會在子線程做一些耗時操作(下載文件、後臺播放音樂等),我們最好能讓這條線程永遠常駐內存。

那麼怎麼做呢?

添加一條用於常駐內存的強引用的子線程,在該線程的RunLoop下添加一個Sources,開啓RunLoop。

- (void)viewDidLoad {
    [super viewDidLoad];
   
    
    // 創建線程,並調用run1方法執行任務
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
    // 開啓線程
    [self.thread start];
  
}

-(void)run1
{
    NSLog(@"需要處理的操作");
    
    //添加下邊兩句代碼,就可以開啓RunLoop,之後self.thread就變成了常駐線程,可隨時添加任務,並交於RunLoop處理
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    
    // 測試是否開啓了RunLoop,如果開啓RunLoop,則來不了這裏,因爲RunLoop開啓了循環。
    NSLog(@"未開啓RunLoop");
    
}
  1. 運行之後發現打印了需要處理的操作,而未開啓RunLoop 則未打印。

這時,我們就開啓了一條常駐線程,下邊我們來試着添加其他任務,除了之前創建的時候調用了run1方法,我們另外在點擊的時候調用run2方法。

那麼,我們在touchesBegan中調用PerformSelector,從而實現在點擊屏幕的時候調用run2方法。

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 利用performSelector,在self.thread的線程中調用run2方法執行任務
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void) run2
{
    NSLog(@"----run2------");
}

每當我們點擊屏幕,都能調用----run2------
這樣我們就實現了常駐線程的需求。
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章