淺析RunLoop原理及其應用

EAWorld發表於2020-04-06

淺析RunLoop原理及其應用

轉載本文需註明出處:微信公眾號EAWorld,違者必究。

引言:

一個APP的啟動與結束都是伴隨著RunLoop迴圈往復的,不斷的迴圈、不斷的往復。當執行緒被殺掉、APP退出後被系統以佔用記憶體為由殺掉,RunLoop就消失了。但平時開發中很少見到RunLoop,為何它如此神秘?本文跟大家分享一下RunLoop的相關知識。  

目錄:

1、RunLoop的概念  

2、RunLoop與執行緒的關係  

3、RunLoop的常用模式

4、RunLoop的應用  

1.RunLoop的概念  

淺析RunLoop原理及其應用

將英文拆解不難理解其實RunLoop表示一直在執行著的迴圈或者從上面的定義原始碼中可以看出就是一個do..while..迴圈。當啟動一個iOS APP時主執行緒啟動與其對應的RunLoop也已經開啟。如果不殺掉APP則APP一直執行,就是因為RunLoop迴圈一直為開啟狀態保證主執行緒不會被摧毀。這也是RunLoop的作用之一保證執行緒不退出。RunLoop在迴圈過程中監聽事件,當前執行緒有任務時,喚醒噹噹執行緒去執行任務,任務執行完成以後,使當前執行緒進入休眠狀態。當然這裡的休眠不同於我們自己寫的死迴圈(while(1);),它在休眠時幾乎不會佔用系統資源,當然這是由作業系統核心去負責實現的。


淺析RunLoop原理及其應用


UIApplicationMain()函式方法會預設為主執行緒設定一個NSRunLoop物件,這個迴圈會隨時監聽螢幕上由使用者觸控所帶來的底層訊息並將其傳遞給主執行緒去處理,當點選一個button事件的傳遞從圖上的呼叫棧可以看出。(監聽的範圍還包含時鐘/網路)RunLoop迴圈與While迴圈的區別在於,RunLoop會在沒有事件發生時進入休眠狀態從而不佔用CPU消耗,有事件發生才會去找對應的 Handler 處理事件,而While則會一直佔用。在 Cocoa 程式的執行緒中都可以透過程式碼NSRunLoop *runloop = [NSRunLoop currentRunLoop];來獲取到當前執行緒的Runloop物件。

RunLoop共有兩套API介面 :1. Foundation框架NSRunLoop 2. Core Foundation框架CFRunLoopRef。NSRunLoop和CFRunLoopRef都代表著RunLoop物件,它們是等價的,可以互相轉換。

NSRunLoop是基於CFRunLoopRef的一層OC包裝,所以要了解RunLoop內部結構,需要多研究CFRunLoopRef層面的API(Core Foundation層面)。

2.RunLoop與執行緒之間的關係
淺析RunLoop原理及其應用


RunLoop和執行緒是相輔相成的,一個Runloop對應著一條唯一的執行緒,可以這樣說RunLoop是為了執行緒而生,沒有執行緒,它也沒有存在的必要。RunLoop是執行緒的基礎架構部分, Cocoa 和 CoreFundation 都提供了RunLoop物件方便配置和管理執行緒的 RunLoop。每個執行緒,包括程式的主執行緒( main thread )都有與之相對應的 RunLoop物件。上圖從 input source 和 timer source 接受事件,然後線上程中處理事件都是由RunLoop推動完成。

注意:開一個子執行緒建立runloop,不是透過alloc init方法建立,而是直接透過呼叫currentRunLoop方法來建立,它本身是一個懶載入的。在子執行緒中,如果不主動獲取Runloop的話,那麼子執行緒內部是不會建立Runloop的。

3.RunLoop的常用模式


淺析RunLoop原理及其應用


RunLoop 的模式有五種。圖上列出了其中兩種分別是 NSDefaultRunLoopMode(預設模式) 和 UITRackingRunLoopMode(UI模式) 、NSRunLoopCommonModes(佔位模式)。其實佔位模式不是一個真正的模式,它相當於上面兩種模式之和。蘋果公開提供的 Mode 有兩個NSDefaultRunLoopMode(kCFRunLoopDefaultMode) NSRunLoopCommonModes(kCFRunLoopCommonModes)。

淺析RunLoop原理及其應用

4.RunLoop的應用


例如建立一個比較常見的註冊頁面,裡面用NSTimer來自處理常見的驗證碼倒數計時,每秒處理一下,如果NSTimer新增到的是預設模式的RunLoop這時候註冊頁面有一個展示註冊協議的UITextView當使用者滑動UITextView時驗證碼的倒數計時是停止的,這是因為主執行緒的RunLoop模式是UI模式這個時候RunLoop迴圈是優先處理UI模式的任務而忽略了預設模式的計時器。此時解決上面的問題就需要用到NSRunLoopCommonModes(佔位模式),這個模式相當於把NSTimer在兩種模式下都新增了,這就不難理解為什麼NSRunLoopCommonModes是一個複數形式了。這個模式下滑動UITextView或停止的時候RunLoop是在UITRacking和default模式下切換的(從列印日誌中可以看出)。如果覺得NSTimer設定RunLoop模式很複雜可以嘗試用GCD的Timer用法很簡便。

淺析RunLoop原理及其應用
RunLoop在TableView中的應用(解決滑動卡頓問題)
淺析RunLoop原理及其應用

如圖程式碼展示,當載入高畫質大圖渲染螢幕,而此時不得不在主執行緒操作,會引起滑動的卡頓。

tableview 在載入 cell 時如果遇到多個耗時操作會有點卡頓。將耗時操作放到 DefaultMode 裡只能解決滑動時流暢,但是停止時需要載入耗時,仍然會有卡頓的感覺。正確方法是採用 RunLoop 監聽,將多個耗時操作分開執行,在每次 RunLoop 喚醒時去做一個耗時任務。

淺析RunLoop原理及其應用


阻塞原因:kCFRunLoopDefaultMode時候 多張圖片(特別是高畫質大圖)一起載入(耗時)loop不結束無法BeforeWaiting(即將進入休眠) 切換至UITrackingRunLoopMode來處理等候的UI重新整理事件造成阻塞。

解決辦法:每次RunLoop迴圈只載入一張圖片 這樣loop就會很快進入到BeforeWaiting處理後面的UI重新整理(UITrackingRunLoopMode 優先處理)或者沒有UI重新整理事件繼續處理下一張圖片。


淺析RunLoop原理及其應用


淺析RunLoop原理及其應用

RunLoop 監聽新增Observer (監聽RunLoop的beforeWaiting)當處理完一張圖片即將進入到beforeWaiting時處理陣列裡的tasks,這些任務就在callback裡面做處理。

callBack拿到task處理了一部分就進入到了休眠 比如拿到18個任務只處理了7個就不處理了。

此處新增Timer是讓RunLoop一直處於活躍狀態 保證即使處理完所有task還是一直活躍狀態。


淺析RunLoop原理及其應用


注意:當CFRunLoopAddObserver(runloop, observer , kCFRunLoopDefaultMode); 新增到觀察者時模式為kCFRunLoopDefaultMode 這樣的的話只能監聽到一般模式的BeforeWaiting,即不滑動的時候。所以圖上的載入只在拖動結束時,而拖動UI時無任何載入。如下圖:

淺析RunLoop原理及其應用

所以這裡可以再次最佳化,將模式改為kCFRunLoopCommonModes,這樣的話滑動或者不滑動都可以載入圖片渲染螢幕,而且是在不影響螢幕流暢性的基礎上。如以下GIF:

淺析RunLoop原理及其應用


原始碼:

#import "ViewController.h"

@interface ViewController ()<UITableViewDelegate, UITableViewDataSource>
@property (weak, nonatomic) IBOutlet UITableView *tableView;

@property (nonatomic, strong) NSTimer *timer;
@property (nonatomic, strong) NSMutableArray *tasks;
@property (nonatomic, assign) NSInteger maxTaskNumber;

@end

void callBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
    //C語言與OC的交換用到橋接 __bridge
    //處理控制器載入圖片的事情
    ViewController *VC = (__bridge ViewController *)(info);
    if (VC.tasks.count == 0) {
        return;
    }
    void(^task)() = [VC.tasks firstObject];
    task();
    [VC.tasks removeObject:task];
    NSLog(@"COUNT:%ld",VC.tasks.count);
    
}

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    
    [self addRunloopOvserver];
    
    self.maxTaskNumber = 18;
    self.tasks = [NSMutableArray array];
    
    
    [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(timerMethod) userInfo:nil repeats:YES];
    
}
-(void)timerMethod{
    
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    ViewController2 *vc2 = [ViewController2 new];
    [self presentViewController:vc2 animated:YES completion:^{
        
    }];
}

- (void)addRunloopOvserver{
    //獲取當前的RunLoop
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    //上下文 (此處為C語言 對OC的操作需要上下文)將(__bridge void *)self 傳入到Callback
    CFRunLoopObserverContext context = {0, (__bridge void *)self, &CFRetain, &CFRelease};
    //建立觀察者 監聽BeforeWaiting 監聽到就呼叫回撥callBack
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &callBack, &context);
    //新增觀察者到當前runloop kCFRunLoopDefaultMode可以改為kCFRunLoopCommonModes
    CFRunLoopAddObserver(runloop, observer , kCFRunLoopCommonModes);
    //C語言中 有create就需要release
    CFRelease(observer);
}


- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
    return 30000;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"identity" forIndexPath:indexPath];
    NSLog(@"---run---%@",[NSRunLoop currentRunLoop].currentMode);
    //以下兩個迴圈的UI操作在必須放在主執行緒,但是弊端就是太多圖片的處理會阻塞tableview的滑動流暢性
    for (int i = 1; i < 4; i++) {
        UIImageView *imageView = [cell.contentView viewWithTag:i];
        [imageView removeFromSuperview];
    }
    for (int i = 1; i < 4; i++) {
    /*
     阻塞模式
    */
    //        CGFloat leading = 10, space = 20, width = 103, height = 87, top = 15;
    //        UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake((i - 1) * (width + space) + leading, top, width, height)];
    //        [cell.contentView addSubview:imageView];
    //        imageView.tag = i;
    //        imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]];

        
    
    //阻塞原因:kCFRunLoopDefaultMode時候 多張圖片一起載入(耗時)loop不結束無法BeforeWaiting(即將進入休眠) 切換至UITrackingRunLoopMode來處理等候的UI重新整理事件造成阻塞
    //解決辦法:每次RunLoop迴圈只載入一張圖片 這樣loop就會很快進入到BeforeWaiting處理後面的UI重新整理(UITrackingRunLoopMode 優先處理)或者沒有UI重新整理事件繼續處理下一張圖片
    
        /*
         流暢模式
        */
        //下面只是把任務放到陣列 不消耗效能
        void(^task)() = ^{
            CGFloat leading = 10, space = 20, width = 103, height = 87, top = 15;
            UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake((i - 1) * (width + space) + leading, top, width, height)];
            [cell.contentView addSubview:imageView];
            imageView.tag = i;
            imageView.image = [UIImage imageWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"image" ofType:@"png"]];
        };
        [self.tasks addObject:task];
        //保證只拿最新的18個任務處理
        if (self.tasks.count > self.maxTaskNumber) {
            [self.tasks removeObjectAtIndex:0];
        }
    }
    return cell;
}



- (void)didReceiveMemoryWarning {
    [super didReceiveMemoryWarning];
}

淺析RunLoop原理及其應用

關於作者熱河,普元移動端開發工程師,網際網路技術愛好者,專注於iOS開發。目前參與Mobile 8.0專案的開發,主要接觸RN技術的應用,黏合前端程式碼與iOS底層之間的互動。

關於EAWorld:微服務,DevOps,資料治理,移動架構原創技術分享。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31562043/viewspace-2661694/,如需轉載,請註明出處,否則將追究法律責任。

相關文章