淺析RunLoop原理及其應用
轉載本文需註明出處:微信公眾號EAWorld,違者必究。
引言:
目錄:
1、RunLoop的概念
2、RunLoop與執行緒的關係
3、RunLoop的常用模式
4、RunLoop的應用
將英文拆解不難理解其實RunLoop表示一直在執行著的迴圈或者從上面的定義原始碼中可以看出就是一個do..while..迴圈。當啟動一個iOS APP時主執行緒啟動與其對應的RunLoop也已經開啟。如果不殺掉APP則APP一直執行,就是因為RunLoop迴圈一直為開啟狀態保證主執行緒不會被摧毀。這也是RunLoop的作用之一保證執行緒不退出。RunLoop在迴圈過程中監聽事件,當前執行緒有任務時,喚醒噹噹執行緒去執行任務,任務執行完成以後,使當前執行緒進入休眠狀態。當然這裡的休眠不同於我們自己寫的死迴圈(while(1);),它在休眠時幾乎不會佔用系統資源,當然這是由作業系統核心去負責實現的。
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層面)。
RunLoop和執行緒是相輔相成的,一個Runloop對應著一條唯一的執行緒,可以這樣說RunLoop是為了執行緒而生,沒有執行緒,它也沒有存在的必要。RunLoop是執行緒的基礎架構部分, Cocoa 和 CoreFundation 都提供了RunLoop物件方便配置和管理執行緒的 RunLoop。每個執行緒,包括程式的主執行緒( main thread )都有與之相對應的 RunLoop物件。上圖從 input source 和 timer source 接受事件,然後線上程中處理事件都是由RunLoop推動完成。
注意:開一個子執行緒建立runloop,不是透過alloc init方法建立,而是直接透過呼叫currentRunLoop方法來建立,它本身是一個懶載入的。在子執行緒中,如果不主動獲取Runloop的話,那麼子執行緒內部是不會建立Runloop的。
RunLoop 的模式有五種。圖上列出了其中兩種分別是 NSDefaultRunLoopMode(預設模式) 和 UITRackingRunLoopMode(UI模式) 、NSRunLoopCommonModes(佔位模式)。其實佔位模式不是一個真正的模式,它相當於上面兩種模式之和。蘋果公開提供的 Mode 有兩個NSDefaultRunLoopMode(kCFRunLoopDefaultMode) NSRunLoopCommonModes(kCFRunLoopCommonModes)。
例如建立一個比較常見的註冊頁面,裡面用NSTimer來自處理常見的驗證碼倒數計時,每秒處理一下,如果NSTimer新增到的是預設模式的RunLoop這時候註冊頁面有一個展示註冊協議的UITextView當使用者滑動UITextView時驗證碼的倒數計時是停止的,這是因為主執行緒的RunLoop模式是UI模式這個時候RunLoop迴圈是優先處理UI模式的任務而忽略了預設模式的計時器。此時解決上面的問題就需要用到NSRunLoopCommonModes(佔位模式),這個模式相當於把NSTimer在兩種模式下都新增了,這就不難理解為什麼NSRunLoopCommonModes是一個複數形式了。這個模式下滑動UITextView或停止的時候RunLoop是在UITRacking和default模式下切換的(從列印日誌中可以看出)。如果覺得NSTimer設定RunLoop模式很複雜可以嘗試用GCD的Timer用法很簡便。
如圖程式碼展示,當載入高畫質大圖渲染螢幕,而此時不得不在主執行緒操作,會引起滑動的卡頓。
tableview 在載入 cell 時如果遇到多個耗時操作會有點卡頓。將耗時操作放到 DefaultMode 裡只能解決滑動時流暢,但是停止時需要載入耗時,仍然會有卡頓的感覺。正確方法是採用 RunLoop 監聽,將多個耗時操作分開執行,在每次 RunLoop 喚醒時去做一個耗時任務。
阻塞原因:kCFRunLoopDefaultMode時候 多張圖片(特別是高畫質大圖)一起載入(耗時)loop不結束無法BeforeWaiting(即將進入休眠) 切換至UITrackingRunLoopMode來處理等候的UI重新整理事件造成阻塞。
解決辦法:每次RunLoop迴圈只載入一張圖片 這樣loop就會很快進入到BeforeWaiting處理後面的UI重新整理(UITrackingRunLoopMode 優先處理)或者沒有UI重新整理事件繼續處理下一張圖片。
RunLoop 監聽新增Observer (監聽RunLoop的beforeWaiting)當處理完一張圖片即將進入到beforeWaiting時處理陣列裡的tasks,這些任務就在callback裡面做處理。
callBack拿到task處理了一部分就進入到了休眠 比如拿到18個任務只處理了7個就不處理了。
此處新增Timer是讓RunLoop一直處於活躍狀態 保證即使處理完所有task還是一直活躍狀態。
所以這裡可以再次最佳化,將模式改為kCFRunLoopCommonModes,這樣的話滑動或者不滑動都可以載入圖片渲染螢幕,而且是在不影響螢幕流暢性的基礎上。如以下GIF:
原始碼:
#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]; }
關於作者:熱河,普元移動端開發工程師,網際網路技術愛好者,專注於iOS開發。目前參與Mobile 8.0專案的開發,主要接觸RN技術的應用,黏合前端程式碼與iOS底層之間的互動。
關於EAWorld:微服務,DevOps,資料治理,移動架構原創技術分享。
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31562043/viewspace-2661694/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- [譯]淺析t-SNE原理及其應用
- RunLoop 淺析OOP
- 淺析volatile原理及其使用
- 淺談webscoket原理及其應用Web
- 零拷貝(Zero-copy) 淺析及其應用
- Tornado原理淺析及應用場景探討
- iOS應⽤簽名原理淺析iOS
- iOS應用程式的脫殼實現原理淺析iOS
- SpringBoot魔法堂:應用熱部署實踐與原理淺析Spring Boot熱部署
- Seata原理淺析
- 淺析DES原理
- 淺析Promise原理Promise
- AQS原理淺析AQS
- Webpack 原理淺析Web
- InheritedWidget原理淺析
- koa原理淺析
- BTrace 原理淺析
- ObjC RunLoop簡析OBJOOP
- Angular @Inject 註解的實際應用例子和工作原理淺析Angular
- markdown-it 原理淺析
- 堆排序原理及其應用場景排序
- 動態代理的原理及其應用
- iOS 淺談 RunloopiOSOOP
- mydumper使用及原理淺析
- Webpack相關原理淺析Web
- ArrayList底層原理淺析
- 淺析Hadoop基礎原理Hadoop
- redux-saga 原理淺析Redux
- react-loadable原理淺析React
- Vuex 原理淺析筆記Vue筆記
- Array、Slice、Map原理淺析
- MySQL事務原理淺析MySql
- HashSet淺析原理學習
- Android中mmap原理及應用簡析Android
- 淺析智慧交通有哪些應用場景?
- 淺析瀑布流佈局原理
- TSDB - VictoriaMetrics 技術原理淺析
- 淺析Vite本地構建原理Vite