1. 程式人生 > >RunLoop 之初探

RunLoop 之初探

你好2019!一起努力呀!

 

1、什麼是runloop

runloop是通過內部維護的事件迴圈事件/訊息進行管理的一個物件

事件迴圈(Event loop):通俗的解釋:沒有訊息處理的時候,休眠以避免資源佔用;有訊息需要處理時,立即被喚醒!書面的解釋:沒有需要處理的訊息時,使用者態切換為核心態;有訊息需要處理時,核心態切換為使用者態!

使用者態:一般時開發者開發使用的、常見的api

核心態:系統呼叫底層的相關,例如:開關機、來電等!

使用者態與核心態的理解!

需要注意的是:接收訊息->處理訊息->等待 ,此處等待≠死迴圈!在之後runloop的是用場景以及分析中會解釋這個!

可參考下圖理解runloop的處理流程

2、runloop的組成

NSRunLoop是基於Foundation框架對CFRunLoop(基於CoreFoundation)的封裝,因為NSRunloop是沒有開源的,但是CFRunLoop是開源的,其結構基本一致,所以分析CFRunLoop的組成即可。

主要涉及到CFRunLoop、CFRunLoopMode、Source/Timer/oberser

CFRunLoop的構成:

pthread(這個可以知道runloop和執行緒一一對應)、currentMode、modes<mode型的集合,說明一個runloop可以用多個mode,如下圖>、commondModes<字串型的集合>、commonModeItems<多個Observer、多個timer、多個source的集合>

runloop與mode 一對多

關於mode:

一般我們常用的Mode有三種 1 .kCFRunLoopDefaultMode(CFRunLoop)/NSDefaultRunLoopMode(NSRunLoop) 預設模式,在RunLoop沒有指定Mode的時候,預設就跑在DefaultMode下。一般情況下App都是執行在這個mode下的   2 .(CFStringRef)UITrackingRunLoopMode(CFRunLoop)/UITrackingRunLoopMode(NSRunLoop) 一般作用於ScrollView滾動的時候的模式,保證滑動的時候不受其他事件影響。
  3 .kCFRunLoopCommonModes(CFRunLoop)/NSRunLoopCommonModes(NSRunLoop) 這個並不是某種具體的Mode,而是一種模式組合,在主執行緒中預設包含了NSDefaultRunLoopMode和 UITrackingRunLoopMode。子執行緒中只包含NSDefaultRunLoopMode。 注意: ①在選擇RunLoop的runMode時不可以填這種模式否則會導致RunLoop執行不成功。 ②在新增事件源的時候填寫這個模式就相當於向組合中所有包含的Mode中註冊了這個事件源。 ③你也可以通過呼叫CFRunLoopAddCommonMode()方法將自定義Mode放到 kCFRunLoopCommonModes組合。  

mode是由source/timer/observer組成

Source就是輸入源事件,分為:source0,諸如UIEvent(觸控,滑動等),performSelector這種需要手動觸發的操作;source1,處理系統核心的mach_msg事件(系統內部的埠事件)。諸如喚醒RunLoop或者讓RunLoop進入休眠節省資源等。我們需要對常駐執行緒進行操作的事件大多都是source0。一般來說日常開發中我們需要關注的是source0,source1只需要瞭解。

Timer即為定時源事件,常見到的就是NSTimer,NSTimer定時器的觸發正是基於RunLoop執行的,所以使用NSTimer之前必須註冊到RunLoop,但是RunLoop為了節省資源並不會在非常準確的時間點呼叫定時器,如果一個任務執行時間較長,那麼當錯過一個時間點後只能等到下一個時間點執行,並不會延後執行(NSTimer提供了一個tolerance屬性用於設定寬容度,如果確實想要使用NSTimer並且希望儘可能的準確,則可以設定此屬性)。

Oberver相當於訊息迴圈中的一個監聽器,隨時通知外部當前RunLoop的執行狀態。NSRunLoop沒有相關方法,只能通過CFRunLoop相關方法建立

 // 建立observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
 
        NSLog(@"----監聽到RunLoop狀態發生改變---%zd", activity);
 
    });
 
    // 新增觀察者:監聽RunLoop的狀態
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

runloop是如何建立的呢?

蘋果是不允許直接建立runloop,它提供了四個函式獲取runloop

[NSRunLoop currentRunLoop]; //獲取當前執行緒的RunLoop [NSRunLoop mainRunLoop]; CFRunLoopGetMain(); CFRunLoopGetCurrent(); 這些函式內部的邏輯大概是下面這樣(這是簡化之後的邏輯,涉及到的連結在文末提供):
/// 全域性的Dictionary,key 是 執行緒, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 訪問 loopsDic 時的鎖
static CFSpinLock_t loopsLock;
 
/// 獲取一個 pthread 對應的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次進入時,初始化全域性Dic,並先為主執行緒建立一個 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接從 Dictionary 裡獲取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
     
    if (!loop) {
        /// 取不到時,建立一個
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 註冊一個回撥,當執行緒銷燬時,順便也銷燬其對應的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
  
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

執行緒和 RunLoop 之間是一一對應的,其關係是儲存在一個全域性的 Dictionary 裡。執行緒剛建立時並沒有 RunLoop,如果你不主動獲取,那它一直都不會有。RunLoop 的建立是發生在第一次獲取時,RunLoop 的銷燬是發生線上程結束時。你只能在一個執行緒的內部獲取其 RunLoop(主執行緒除外)。

除了主執行緒,其他執行緒的runloop預設是沒有開啟的,且從上面的原始碼來看,任意一個子執行緒的RunLoop都會保證主執行緒的RunLoop的存在。

RunLoop正常執行的條件是:1.有Mode。2.Mode有事件源。3.執行在有事件源的Mode下。

 原始碼分析之小結論:

①.RunLoop是寄生於執行緒的訊息迴圈機制,它能保證執行緒存活,而不是線性執行完任務就消亡。

②.RunLoop與執行緒是一一對應的,每個執行緒只有唯一與之對應的一個RunLoop。我們不能建立RunLoop,只能在當前執行緒當中獲取執行緒對應的RunLoop(主執行緒RunLoop除外)。

③.子執行緒預設沒有RunLoop,需要我們去主動開啟,但是主執行緒是自動開啟了RunLoop的。

④.RunLoop想要正常啟用需要執行在添加了事件源的Mode下。

⑤.RunLoop有三種啟動方式run、runUntilDate:(NSDate *)limitDate、runMode:(NSString *)mode beforeDate:(NSDate *)limitDate。第一種無條件永遠執行RunLoop並且無法停止,執行緒永遠存在。第二種會在時間到後退出RunLoop,同樣無法主動停止RunLoop。前兩種都是在NSDefaultRunLoopMode模式下執行。第三種可以選定執行模式,並且在時間到後或者觸發了非Timer的事件後退出。

3、runloop的是用場景

3.1:保持執行緒存活

自定義一個繼承自NSThread的HFSThread:目的是重寫其dealloc方法,看何時被釋放掉的

3.1.1:Thread關聯的方法中不開啟runloop的情況下:

1 - (void)threadTest
2 {
3     HFSThread *testHead = [[HFSThread alloc]initWithTarget:self selector:@selector(threadAction) object:nil];
4     testHead.name = @"testThread";
5 //    self.myThread = testHead;
6     [testHead start];
9 }
2019-01-13 15:09:38.664810+0800 HaiFeiTestProject[26482:874633] begin threadAction -[NSThread currentThread] = <HFSThread: 0x600002da9a40>{number = 3, name = testThread}
2019-01-13 15:09:38.667483+0800 HaiFeiTestProject[26482:874633] 121212
2019-01-13 15:09:38.667780+0800 HaiFeiTestProject[26482:874633] end  threadAction -[NSThread currentThread] = <HFSThread: 0x600002da9a40>{number = 3, name = testThread}
2019-01-13 15:09:38.668259+0800 HaiFeiTestProject[26482:874633] HFSThread dealloc name = testThread

可以發現,此thread關聯的方法執行完畢之後被釋放掉!

可能我們會覺得此處建立的thread是臨時變數,那將其設定為控制器的屬性,再次執行一次相關操作

Thread設定為屬性 執行結果

此時的執行結果中沒有執行其dealloc操作,我們會認為此thread沒有被釋放

那麼如果再次執行其start方法會如何?

thread連續執行兩次start的執行結果

程式崩潰,原因如下:[HFSThread start]: attempt to start the thread again  也就是說雖然thread沒有被釋放,但是它處於死亡狀態(執行緒執行結束之後就會進入這個狀態),蘋果不允許,已死亡的執行緒再次開啟!

以上的操作會發現執行緒執行完其關聯的任務之後就會死亡,如何保持其存活呢?

也許我們可以使用while迴圈保持執行緒存活,但是實踐證明如果我們線上程關聯的方法中執行如下操作

while迴圈保持執行緒存活

確實會讓執行緒保持存活,但是此方法將會一直如此執行下去,顯然不符合我們的期望!

3.1.2:初步嘗試使用runloop

我們再次建立一個臨時變數thread 在其關聯的方法中如下操作

臨時變數Thread中新增runloop 執行結果

執行結果可以看出,雖然是臨時的thread,但是沒有在其關聯的方法結束之後沒有執行dealloc操作,並且根據執行結果可以發現[runloop run]之後的程式碼沒有在執行!

原因:RunLoop本質就是個Event Loop的do while迴圈,所以執行到這一行以後子執行緒就一直在進行接受訊息->等待->處理的迴圈。所以不會執行[runLoop run];之後的程式碼(這點需要注意,在使用RunLoop的時候如果要進行一些資料處理之類的要放在這個函式之前否則寫的程式碼不會被執行),也就不會因為任務結束導致執行緒死亡進而銷燬。

關於runloop的使用方法在講述其組成的時候會進一步講述!

3.2:執行緒在我們需要的時候響應訊息

我們實現可以保持執行緒存活之後,希望實現在我們需要的時候響應訊息

我們知道系統提供了幾個某個執行緒中執行任務的放

performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:
在主執行緒中響應指定Selector。這兩個方法給你提供了選項來阻斷當前執行緒(不是執行Selector的執行緒而是呼叫上述方法的執行緒)直到selector被執行完畢。
 
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:
在某個子執行緒(NSThread對像)中響應指定Selector。這兩個方法同樣給你提供了選項來阻斷當前執行緒直到Selector被執行完畢。
 
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:
在當前執行緒中執行Selector,並附加了延遲選項。多個排隊的Selector會按照順序一個一個的執行。

這幾個方法都是向執行緒中的RunLoop傳送了訊息,然後RunLoop接收到了訊息就喚醒執行緒,去做對應的事情。所以想要正常使用這幾個方法,響應selector的執行緒必須開啟了RunLoop。

例如:

View Code 執行結果

最後子執行緒任務結束然後被釋放是因為之前提到的,runMode:(NSString *)mode beforeDate:(NSDate *)limitDate這種啟動RunLoop的方式有一個特性,那就是這個介面在非Timer事件觸發(此處是達成了這個條件)、顯式的用CFRunLoopStop停止RunLoop或者到達limitDate後會退出。而例子當中也沒有用while把RunLoop包圍起來,所以RunLoop退出後子執行緒完成了任務最後退出了!如果使用的是 [runloop run];那相關的觸發操作可以一直執行!

3.3:執行緒定時執行某個任務

我們最初使用NSTimer的時候 建立之後卻並沒有按照我們預期的那樣執行,之後添加了[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];才可以按照預期執行,當時我們大約並沒有仔細分析其中緣由,

因為timer還提供了一些方法(以scheduledTimerWithTimeInterval開頭的方法),可以不必設定runloop,因為這些方法建立一個定時器並自動新增到當前執行緒RunLoop的NSDefaultRunLoopMode中所以不需要開發者為處理相關!

 NSTimer常見的問題:

1、是好是不好

用Timer的時間長了總有一天突然發現,為啥我的Timer執行的好好的突然就時好時壞了?在進行Scrollview的滾動操作時Timer不進行響應,滑動結束後timer又恢復正常了。大約很多人都曾經遇到過吧!
我們知道每次runloop只能執行在一個mode上,當我們建立一個NSTimer的時候,預設都是講定時器新增到了加到了主執行緒RunLoop的NSDefaultRunLoopMode中。一般情況下主執行緒RunLoop就執行在NSDefaultRunLoopMode下,所以定時器正常執行。

但是當Scrollview開始滑動時,主執行緒RunLoop自動切換了當前執行的Mode(currentMode),變成了UITrackingRunLoopMode。所以現在RunLoop要處理的就是UITrackingRunLoopMode中item。

而我們的timer是新增在NSDefaultRunLoopMode中的,並沒有新增到UITrackingRunLoopMode中。即我們的timer不是UITrackingRunLoopMode中的item。因為不同mode的item相關沒有影響,所以RunLoop也就不會處理非當前Mode的item,所以定時器就不會響應。

當Scrollview滑動結束,主執行緒RunLoop自動切換了當前執行的Mode(currentMode),變成了NSDefaultRunLoopMode。我們的Timer是NSDefaultRunLoopMode的item,所以RunLoop會處理它,所以又正常響應了。

想Timer在兩種Mode中都得到響應怎麼辦?前面提到過,一個item可以被同時加入多個mode。讓Timer同時成為兩種Mode的item就可以了(分別新增或者直接加到commonMode中),這樣不管RunLoop處於什麼Mode,timer都是當前Mode的item,都會得到處理。

我們還可以使用commonMode講timer同步新增到多個mode中

commonMode它不是實際的一種mode,是同步source、timer、observer到多個mode的一種技術方案!

2、導致的ViewController無法釋放問題

建立NSTimer會因為設定target為self導致Timer對ViewController有一個強引用,最後結果就是ViewController無法釋放。

關於這個問題,目前個人有兩個處理方法:建立一箇中間物件,處理timer和控制器 ;使用GCDTimer(關於這個的使用,在之後會進行詳細說明!)

針對第一種方法的實現程式碼說明:

 1 #import <Foundation/Foundation.h>
 2 #import <UIKit/UIKit.h>
 3 
 4 
 5 NS_ASSUME_NONNULL_BEGIN
 6 
 7 @interface ManagerTimer : NSObject
 8 
 9 @property (nonatomic, weak) NSTimer *myTimer;
10 
11 @property (nonatomic, weak) UIViewController *myVC;
12 - (void)startTimer;
13 @end
14 
15 NS_ASSUME_NONNULL_END
ManagerTimer.h
 1 #import "ManagerTimer.h"
 2 
 3 @implementation ManagerTimer
 4 
 5 - (id)init{
 6     if (self = [super init]) {
 7         
 8     }
 9     
10     return self;
11 }
12 - (void)startTimer
13 {
14     self.myTimer =  [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
15 }
16 
17 - (void)dealloc
18 {
19     NSLog(@"ManagerTimer dealloc ");
20     NSLog(@"self.myTimer = %@",self.myTimer);
21 
22 }
23 - (void)timerAction:(NSTimer *)timer
24 {
25     static NSInteger num = 0;
26     NSLog(@"ManagerTimer num = %ld",num++);
27     if (self.myVC == nil) {
28        [timer invalidate];
29         
30         timer = nil;
31         
32     }
33 
34 }
35 
36 
37 @end
ManagerTimer.m

 在控制器中使用

 ManagerTimer *managerTimer = [[ManagerTimer alloc]init];
    managerTimer.myVC = self;
    [managerTimer startTimer];
 1 2019-01-13 17:10:00.438225+0800 HaiFeiTestProject[27851:974282] ManagerTimer num = 0
 2 2019-01-13 17:10:01.439114+0800 HaiFeiTestProject[27851:974282] ManagerTimer num = 1
 3 2019-01-13 17:10:02.439077+0800 HaiFeiTestProject[27851:974282] ManagerTimer num = 2
 4 2019-01-13 17:10:03.438536+0800 HaiFeiTestProject[27851:974282] ManagerTimer num = 3
 5 2019-01-13 17:10:04.438006+0800 HaiFeiTestProject[27851:974282] ManagerTimer num = 4
 6 2019-01-13 17:10:04.601969+0800 HaiFeiTestProject[27851:974282]  FirstViewController dealloc
 7 2019-01-13 17:10:04.602102+0800 HaiFeiTestProject[27851:974282] (null)
 8 2019-01-13 17:10:05.439199+0800 HaiFeiTestProject[27851:974282] ManagerTimer num = 5
 9 2019-01-13 17:10:05.439585+0800 HaiFeiTestProject[27851:974282] ManagerTimer dealloc
10 2019-01-13 17:10:05.440027+0800 HaiFeiTestProject[27851:974282] self.myTimer = <__NSCFTimer: 0x600003b78300>
執行結果

按照我們的預期實現了!

如果不是用這個中間物件,在我們離開當前控制器的時候,定時器無法停止,控制器也無法釋放!

 

文中若有不對之處,還請勞駕之處,謝謝!

部分參考連結:RunLoop入門小結 這個連結中相關的其他關於runloop的一些部落格分析也很值得去看!