iOS學習——淺談RunLoop
阿新 • • 發佈:2018-12-14
RunLoop的字面意思是
1 RunLoop基礎
1.1 RunLoop的基本作用
1.2 關於RunLoop的幾點說明
- 如果沒有Runloop,那麼程式一啟動就會退出,什麼事情都做不了。
- 如果有了Runloop,那麼相當於在內部有一個死迴圈,能夠保證程式的持續執行
- a 在UIApplication函式內部就啟動了一個Runloop 該函式返回一個int型別的值
- b 這個預設啟動的Runloop是跟主執行緒相關聯的
1.3 RunLoop物件
- 在iOS開發中有兩套api來訪問Runloop
- foundation框架【NSRunloop】
- core foundation框架【CFRunloopRef】
- NSRunLoop和CFRunLoopRef都代表著RunLoop物件,它們是等價的,可以互相轉換
- NSRunLoop是基於CFRunLoopRef的一層OC包裝,所以要了解RunLoop內部結構,需要多研究CFRunLoopRef層面的API(Core Foundation層面)
1.4 RunLoop與執行緒
1.5 獲取Runloop物件
/*1.獲得當前Runloop物件*/ //01 NSRunloop NSRunLoop * runloop1 = [NSRunLoop currentRunLoop]; //02 CFRunLoopRef CFRunLoopRef runloop2 = CFRunLoopGetCurrent(); /*2.拿到當前應用程式的主Runloop(主執行緒對應的Runloop)*/ //01 NSRunloop NSRunLoop * runloop1 = [NSRunLoop mainRunLoop]; //02 CFRunLoopRef CFRunLoopRef runloop2 = CFRunLoopGetMain(); /*3.注意點:開一個子執行緒建立runloop,不是通過alloc init方法建立,而是直接通過呼叫currentRunLoop方法來建立,它本身是一個懶載入的。 4.在子執行緒中,如果不主動獲取Runloop的話,那麼子執行緒內部是不會建立Runloop的。可以下載CFRunloopRef的原始碼,搜尋_CFRunloopGet0,檢視程式碼。 5.Runloop物件是利用字典來進行儲存,而且key是對應的執行緒Value為該執行緒對應的Runloop。*/
2 RunLoop相關類
2.1 Runloop執行原理圖
線上程中開啟RunLoop後,系統會進入一個死迴圈,這個迴圈在有事件觸發時(觸控事件、定時器事件【NSTimer】、selector事件【選擇器·performSelector···】等)就工作,沒事情就休息,提高程式效能,節省CPU資源,示意圖如下。
2.2 RunLoop相關的5個類
- CFRunloopRef
- CFRunloopModeRef【Runloop的執行模式】
- CFRunloopSourceRef【Runloop要處理的事件源】
- CFRunloopTimerRef【Timer事件】
- CFRunloopObserverRef【Runloop的觀察者(監聽者)】
Runloop要想跑起來,它的內部必須要有一個mode,這個mode裡面必須有source\observer\timer,至少要有其中的一個。
2.2.1 CFRunloopModeRef
- CFRunloopModeRef代表著Runloop的執行模式
- 一個Runloop中可以有多個mode,一個mode裡面又可以有多個source\observer\timer等等
- 每次runloop啟動的時候,只能指定一個mode,這個mode被稱為該Runloop的當前mode
- 如果需要切換mode,只能先退出當前Runloop,再重新指定一個mode進入,這樣做主要是為了分割不同組的定時器等,讓他們相互之間不受影響
- 系統預設註冊了5個mode
- a.kCFRunLoopDefaultMode:App的預設Mode,通常主執行緒是在這個Mode下執行
- b.UITrackingRunLoopMode:介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響
- c.UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用
- d.GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到
- e.kCFRunLoopCommonModes: 這是一個佔位用的Mode,不是一種真正的Mode
2.2.2
觸發一個操作。基本上說的就是NSTimerNSTimer在實際開發中會出現不準的情況,出現這種情況的主要是NSTimer的初始化有兩種方法如下,然後第一種方法會自動新增到當前的RunLoop中,並且RunLoop的執行模式mode設定為kCFRunLoopDefaultMode,這種模式在介面被拖拽時執行mode變為UITrackingRunLoopMode,這時候defaultmode下的定時器就會停止工作,所以在介面拖拽時定時器不計時,導致計時不準。
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block; + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
解決上述所說的定時器不準的方案是設定RunLoop的工作mode為kCFRunLoopCommonModes,這種模式可以在多種mode下都進行工作。
/* 說明: (1)runloop一啟動就會選中一種模式,當選中了一種模式之後其它的模式就都不鳥。一個mode裡面可以新增多個NSTimer,也就是說以後當建立NSTimer的時候,可以指定它是在什麼模式下執行的。 (2)它是基於時間的觸發器,說直白點那就是時間到了我就觸發一個事件,觸發一個操作。基本上說的就是NSTimer (3)相關程式碼 */ - (void)timer2 { //NSTimer 呼叫了scheduledTimer方法,那麼會自動新增到當前的runloop裡面去,而且runloop的執行模式kCFRunLoopDefaultMode NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES]; //更改模式 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; } - (void)timer1 { // [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES]; NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES]; //定時器新增到UITrackingRunLoopMode模式,一旦runloop切換模式,那麼定時器就不工作 // [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode]; //定時器新增到NSDefaultRunLoopMode模式,一旦runloop切換模式,那麼定時器就不工作 // [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode]; //佔位模式:common modes標記 //被標記為common modes的模式 kCFRunLoopDefaultMode UITrackingRunLoopMode [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; // NSLog(@"%@",[NSRunLoop currentRunLoop]); } - (void)run { NSLog(@"---run---%@",[NSRunLoop currentRunLoop].currentMode); } - (IBAction)btnClick { NSLog(@"---btnClick---"); }
GCD中的定時器的使用
//0.建立一個佇列 dispatch_queue_t queue = dispatch_get_global_queue(0, 0); //1.建立一個GCD的定時器 /* 第一個引數:說明這是一個定時器 第四個引數:GCD的回撥任務新增到那個佇列中執行,如果是主佇列則在主執行緒執行 */ dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue); //2.設定定時器的開始時間,間隔時間以及精準度 //設定開始時間,三秒鐘之後呼叫 dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW,3.0 *NSEC_PER_SEC); //設定定時器工作的間隔時間 uint64_t intevel = 1.0 * NSEC_PER_SEC; /* 第一個引數:要給哪個定時器設定 第二個引數:定時器的開始時間DISPATCH_TIME_NOW表示從當前開始 第三個引數:定時器呼叫方法的間隔時間 第四個引數:定時器的精準度,如果傳0則表示採用最精準的方式計算,如果傳大於0的數值,則表示該定時切換i可以接收該值範圍內的誤差,通常傳0 該引數的意義:可以適當的提高程式的效能 注意點:GCD定時器中的時間以納秒為單位(面試) */ dispatch_source_set_timer(timer, start, intevel, 0 * NSEC_PER_SEC); //3.設定定時器開啟後回撥的方法 /* 第一個引數:要給哪個定時器設定 第二個引數:回撥block */ dispatch_source_set_event_handler(timer, ^{ NSLog(@"------%@",[NSThread currentThread]); }); //4.執行定時器 dispatch_resume(timer); //注意:dispatch_source_t本質上是OC類,在這裡是個區域性變數,需要強引用 self.timer = timer;
2.2.3 CFRunloopSourceRef
CFRunloopSourceRef是事件源也就是輸入源,有兩種分類模式;一種是按照蘋果官方文件進行劃分的,另一種是基於函式的呼叫棧來進行劃分的(source0和source1)。
(1)以前的分法
-
- Port-Based Sources
- Custom Input Sources Cocoa
- Perform Selector Sources
(2)現在的分法 Source0:非基於Port的; Source1:基於Port的
可以通過打斷點的方式檢視一個方法的函式呼叫棧
2.2.4
(1)CFRunLoopObserverRef是觀察者,能夠監聽RunLoop的狀態改變
(2)如何監聽
//建立一個runloop監聽者 CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { NSLog(@"監聽runloop狀態改變---%zd",activity); }); //為runloop新增一個監聽者 CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode); CFRelease(observer);
(3)監聽的狀態
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), //即將進入Runloop kCFRunLoopBeforeTimers = (1UL << 1), //即將處理NSTimer kCFRunLoopBeforeSources = (1UL << 2), //即將處理Sources kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠 kCFRunLoopAfterWaiting = (1UL << 6), //剛從休眠中喚醒 kCFRunLoopExit = (1UL << 7), //即將退出runloop kCFRunLoopAllActivities = 0x0FFFFFFFU //所有狀態改變 };