RunLoop知識總結

1.RunLoop簡介

1.1 什麼是RunLoop

從字面上來講是運行循環,也能夠翻譯爲跑圈.安全

  • RunLoop本質上是一個對象,這個對象能夠保持程序的持續運行而且處理程序中的各類事件(如觸摸事件,定時器時間,selector事件).
  • RunLoop沒有事情處理時就會使線程進入睡眠狀態.這樣能夠節省CPU資源,提升程序性能.

1.2 RunLoop和線程

RunLoop和線程是息息相關的,咱們都知道線程的做用就是用來執行特定的一個或多個任務,正常狀況下,線程執行完當前任務後就會退出,以後若線程又有任務須要執行也沒法繼續執行了.這時咱們就須要一種方式讓線程能不斷執行任務,即便當前線程沒有任務執行,線程也不會退出,而是等待下一個任務的到來.因此咱們就有了RunLoop.markdown

  1. 每一條線程都有惟一一個與之對應的RunLoop對象.app

  2. 主線程的RunLoop對象系統已經自動幫咱們建立好了,而且只有主線程結束時即程序結束時纔會銷燬.框架

  3. 子線程的Runloop對象須要咱們主動建立並維護,子線程的Runloop對象在第一次獲取時就會建立,銷燬則是在子線程結束時. 而且建立出來的runLoop對象默認是不開啓的,必須手動開啓RunLoop.ide

  4. Runloop並不保證線程安全,咱們只能在當前線程內部操做當前線程的Runloop對象,而不能在當前線程中去操做其餘線程的RunLoop對象.函數

    相關代碼以下:oop

    NSRunLoop *currentRunLoop = [NSRunloop currentRunloop] //獲取當前線程的RunLoop對象,在子線程中調用時若是是第一次獲取內部會幫咱們建立RunLoop對象
    [currentRunLoop run];
    
    [NSRunLooop mainRunLoop] //獲取主線程的RunLoop對象
    複製代碼

1.3 默認狀況下主線程的RunLoop原理

咱們在啓動一個程序時,系統會自動調用建立項目時自動建立的main.m 的文件.main.m文件以下所示:性能

int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

複製代碼

其中UIApplicationMain函數中內部幫咱們開啓了主線程的RunLoop,這個RunLoop使得程序只要不退出或者崩潰,UIApplicationMain函數就一直不會返回,保持了程序的持續運行.上邊的代碼中主線程開啓RunLoop的過程能夠簡單理解爲如下代碼:spa

int main(int argc, char * argv[]) {
	BOOL isRunning = YES;
    do {
    //執行各類任務,處理各類事件
	} while(isRunning);
    
    return 0;
}
複製代碼

下圖是蘋果官方的RunLoop模型圖線程

從上圖能夠看出RunLoop就是線程中的一個循環,RunLoop會在循環中經過 Input sources(輸入源) 和 Timer sources(定時源)不斷檢測是否有事件須要執行.而後對接收到的事件通知線程去處理,而且在沒有事件的時候讓線程去休息.

2.RunLoop的相關類

iOS爲咱們提供了兩套API來訪問RunLoop, 一套是Foundation框架的NSRunLoop, 一套是Core Foundation框架的CFRunLoop. NSRunloop本質是基於CFRunLoop的oc對象封裝,因此咱們在這裏就講解Core Foundation框架下有關RunLoop的五個類.

  1. CFRunLoopRef: 表明RunLoop對象
  2. CFRunLoopModeRef: 表明RunLoop的運行模式
  3. CFRunLoopSourceRef: 就是上面RunLoop模型圖中的事件源/輸入源
  4. CFRunLoopTimerRef: 就是上面RunLoop模型圖中的定時源

5 CFRunLoopObserverRef: 觀察者,可以監聽RunLoop的狀態改變

下面詳細講解幾種類的具體含義相互關係. 先來看看一張能表示五個類關係的圖:

接着來說解這五個類的相互關係:

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

  • 每次RunLoop啓動時只能指定其中的一種運行模式, 這個運行模式被稱做當前的運行模式(CurrentMode).
  • 在每一個運行模式中知識須要一個輸入源或者一個定時源.
  • 若是須要切換運行模式, 必須退出當前RunLoop, 再從新指定一個運行模式進入,
  • 這樣作主要是爲了區別不一樣組以前的Source/Timer/Observer,讓其互不影響

下面咱們來詳細講解一下這五個類:

2.1 CFRunLoopRef類

CFRunLoop類是Core Foundation框架下的RunLoop對象類.咱們能夠經過如下方式獲取RunLoop對象

  • Core Foundation
    • CFRunLoopGetCurrent(); //獲取當前線程的RunLoop對象,在子線程中調用時若是是第一次獲取內部會幫咱們建立RunLoop對象
    • CFRunLoopGetMain(); //獲取主線程的RunLoop對象

2.2 CFRunLoopModeRef

系統默認定義了多種運行模式, 以下:

  1. kCFRunLoopDefaultMode: APP的默認運行模式, 一般主線程就是在這個模式下運行的
  2. UITrackingRunLoopMode: 跟蹤用戶交互事件(用於ScrollView追蹤觸摸滑動,保證界面滑動時不受其餘Mode影響)
  3. UIInitializationRunLoopMode: 在剛啓動APP時進入的第一個Mode,啓動完成後就不會再使用
  4. CSEventReceiveRunLoopMode: 接受系統內部事件(用於繪圖),一般用不到
  5. kCFRunLoopCommonMode:這是一種佔位模式,並非一種真正的運行模式(後邊會用到)

其中kCFRunLoopDefaultMode, UITrackingRunLoopMode,kCFRunLoopCommonModes是咱們開發中須要用到的模式.具體使用方法咱們在2.3 CFRunLoopTimerRef中結合CFRunLoopTimerRef來演示說明

2.3 CFRunLoopTimerRef

CFRunLoopTimerRef是定時源, 理解爲基於時間的觸發器, 基本上就是NSTimer. 下面咱們來演示一下CFRunLoopModeRef和CFRunLoopTimerRef結合的使用方法.

在Main.Storyboard中拖入一個textView. 而後嘗試執行如下代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self timer1];
}
- (void)timer1 {
    //1.建立定時器
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //2.將定時器添加到當前的RunLoop,指定RunLoop的運行模式爲默認運行模式
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
    }
    - (void)run {
    NSLog(@"run --- %@ --- %@", [NSThread currentThread], [NSRunLoop currentRunLoop]);
}

複製代碼

當程序運行時, run方法每隔兩秒就會執行一次, 可是若拖動textView,run方法就不會執行.這是由於什麼呢?

咱們建立的timer是加入到RunLoop的NSDefaultRunLoopMode運行模式中, 可是當咱們拖動textView,當前RunLoop會退出當前運行模式,並進入到UITrackingRunLoopMode運行模式,咱們建立的timer並無添加到併到UITrackingRunLoopMode運行模式中,因此run方法就不會執行.

那麼有什麼解決方法呢?

  • 解決方法一:

    把timer也添加到UITrackingRunLoopMode運行模式中.這樣就能夠在兩種運行模式下都執行run方法.
    增長代碼以下:

    [[NSRunLoop currentRunLoop]addTimer:timer forMode:UITrackingRunLoopMode];
    複製代碼
  • 解決方法二:

    把timer加入到kCFRunLoopCommonMode運行模式中.前面2.2中已經提到這種模式其實知識一種佔位模式,並非真正的運行模式.如果將timer添加到這個模式中,那麼timer會被添加到打上common標籤的運行模式中.

    那麼那些運行模式會被打上common標籤呢?
    NSDefaultRunLoopMode 和 UITrackingRunLoopMode

    因此只要添加到kCFRunLoopCommonMode運行模式也就等價於把timer加入到NSDefaultRunLoopMode和UITrackingRunLoopMode這兩種運行模式中.

    將代碼替換成以下代碼:

    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
    複製代碼

    除了上面代碼中使用的timer的建立方法,還有一種經常使用的timer建立方法

    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil   repeats:YES];
    複製代碼

    這種方法建立出來的timer會被默認添加到NSDefaultRunLoopMode運行模式,若想添加到UITrackingRunLoopMode中,只要拿到timer對象而後選擇上面的其中一種解決方法便可.

注意點

剛纔提到了例子都是在主線程中建立timer並加入到RunLoop中特定的運行模式中,那麼要是在子線程中建立timer有什麼區別呢?
請嘗試執行下面的代碼:

- (void)viewDidLoad {
    [super viewDidLoad];
    [NSThread detachNewThreadSelector:@selector(timer2) toTarget:self withObject:nil];
}
- (void)timer2 {

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

}
- (void)run {
    NSLog(@"run --- %@ --- %@", [NSThread currentThread], [NSRunLoop currentRunLoop]);
}
複製代碼

你會發現run方法根本不會調用,這是爲何呢?

這其實就要和上面提到的runLoop的的建立和管理有關了.

子線程的Runloop對象須要咱們主動建立並維護,子線程的Runloop對象在第一次獲取時就會建立,銷燬則是在子線程結束時. 而且建立出來的runLoop對象默認是不開啓的,必須手動開啓RunLoop.

因此咱們應該修改代碼爲以下:

- (void)timer2 {
    //1.獲取RunLoop並建立
   NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
    //2.建立timer
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //3.啓動子線程的RunLoop
    [currentRunLoop run];
}
複製代碼

2.4 CFRunLoopSourceRef

CFRunLoopSourceRef是事件源(RunLoop模型圖中提到過的)

  • 之前的分法:
    • Port-Based Sources(基於端口的)
    • Custiom Input Sources(自定義)
    • Cocoa Peform Selector Sources(peform selector 方法)
  • 如今的分法:
    • Source0: 非基於Port(端口)的(用戶事件)
    • Source1: 基於Port的, 經過內核和其餘線程通訊,接收,分發系統事件(系統事件)

第一種是經過官方理論來分的, 第二種是在實際應用中經過調用函數來分的.

下面咱們舉個例子經過函數調用棧中的source
1.首先咱們在main.storyboard中拖入一個按鈕,並添加動做
2.而後在點擊動做中的代碼中加入一個輸出語句,並打上一個斷點

步驟以下:

當咱們運行程序後點擊按鈕後就會來到此斷點,而後咱們就能夠查看當前的函數調用棧.

以下圖所示:

因此點擊事件是這樣來的:

  1. 首先程序啓動而後運行到18行的main函數,以後在main函數中調用17行的UIApplicationMain函數,而後一直往上調用函數, 最終調用到點擊函數.
  2. 咱們能夠看到在12行中有CFRunLoopDoSources0,即咱們的點擊事件屬於sourece0函數的,點擊事件就是source0中處理的.
  3. 而至於source1就是用來接收和分發系統的事件,而後再分發到Source0中處理.

2.5 CFRunLoopObserver

CFRunLoopObserver是監聽者, 可以監聽RunLoop的狀態改變.

能夠監聽的時間點有如下:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0),           //即將進入RunLoop
        kCFRunLoopBeforeTimers = (1UL << 1),    //即將處理Timer
        kCFRunLoopBeforeSources = (1UL << 2),   //即將處理Source
        kCFRunLoopBeforeWaiting = (1UL << 5),   //即將進入休眠
        kCFRunLoopAfterWaiting = (1UL << 6),    //剛從休眠中被喚醒
        kCFRunLoopExit = (1UL << 7),  	        //即將退出RunLoop
        kCFRunLoopAllActivities = 0x0FFFFFFFU   //監聽全部事件
    };

複製代碼

具體使用方法以下:

- (void)viewDidLoad {
    [super viewDidLoad];
    [self observer];
}
- (void)observer {
    /**
     @param1:怎麼分配空間(通常傳入默認分配方式)
     @param2:要監聽的RunLoop的什麼狀態
     @param3:是否要持續監聽
     @param4:優先級 老是傳0
     @param5:當狀態改變時的回調
     */
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"即將進入RunLoop");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"即將處理Timer");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"即將處理Source");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"即將進入休眠");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"剛從休眠中被喚醒");
                break;
            case kCFRunLoopExit:
                NSLog(@"即將退出RunLoop");
                break;
        
            default:
                break;
        }
    });
    /**
     @param1:要監聽的RunLoop對象
     @param2:觀察者
     @param3:運行模式
     */
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
}
複製代碼

打印臺信息以下:

能夠看到RunLoop在程序運行後就會處理大量的Source和Timer事件,當沒有事情須要作的時候就會進入休眠狀態,即讓線程休眠,當有事件須要處理時就會喚醒RunLoop再次處理事件,

3. RunLoop原理

五個類都理解完以後咱們就來具體說明RunLoop的運行原理.
其中咱們藉助下面這張網友的邏輯圖進行說明

結合上面這個邏輯圖咱們來講明一個蘋果官方文檔給出的RunLoop運行邏輯

具體順序以下:
首先RunLoop會去檢查Mode裏是否有source/timer, 沒有直接退出

  1. 通知觀察者RunLoop已經啓動(系統自己就會爲咱們添加一個觀察者)
  2. 通知觀察者即將要處理Timer
  3. 通知觀察者即將要處理Sourece0
  4. 啓動任何準備好的Source0
  5. 若是Soure1準備好並處於等待狀態進入,當即啓動,進入步驟9.(source1內部就是由source0和timer組成)
  6. 通知觀察者進入休眠狀態
  7. 將線程置於休眠狀態直到下面任一事件發生
    • 某一事件到達基於端口的源
    • 定時器啓動
    • RunLoop設置的時間已經超時
    • RunLoop被外部顯示喚醒。
  8. 通知觀察者,線程被喚醒
  9. 處理未處理的事件
    • 若是用戶定義的定時器啓動, 處理定時器事件並從新啓動RunLoop,進入步驟2.
    • 若是輸入源啓動, 傳遞相應消息。
    • 若是RunLoop被顯示喚醒而且時間還沒超時,重啓RunLoop,進入步驟2
  10. 通知觀察者RunLoop結束

4. RunLoop的實戰運用

前面都是一些理論知識的講解,接下來咱們咱們就講講在實戰中如何使用RunLoop.

4.1 NSTimer的使用

剛剛在前面的2.3中咱們已經講解了把Timer加入到RunLoop的不一樣運行模式的做用和區別.你們若是忘了能夠回去再看看如何使用.

4.2 ImageView推遲顯示

咱們可能有時會遇到一種狀況,就是咱們的界面有tableView,每一個tableView的cell中都有許多圖片.而後當咱們滾動tableView,須要顯示不少圖片,這時候可能就會出現卡頓現象.

那麼這時咱們就可使用RunLoop來解決這個問題.具體方法爲利用performSelector方法調用UIImageView的setImage:方法,而後指定在RunLoop下的NSDefaultRunLoopMode運行模式.代碼以下:

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"男孩"] afterDelay:5.0 inModes:@[NSDefaultRunLoopMode]];
複製代碼

咱們設置顯示圖片的時間爲五秒以後,可是程序運行後咱們拖動textView,發現五秒後圖片並無出現,而是當咱們拖動結束時候才顯示出來.

這是由於咱們設置顯示圖片的操做是在RunLoop的NSDefaultRunLoopMode模式中,當咱們拖動textView時,RunLoop會切換到UITrackingRunLoopMode模式,這時即便設定的操做執行時間也不會執行,而是要等到咱們結束完拖動後纔會切換回NSDefaultRunLoopMode模式執行設置圖片的操做.

注意點

在上面推遲顯示圖片的程序中,咱們能夠發現當咱們切換到UITrackingRunLoopMode中,設定的執行操做的時間並無中止計時,因此當咱們一中止拖動時就會立刻執行操做.

那麼咱們要是在RunLoop的NSDefaultRunLoopMode模式下添加了一個timer,拖動textView一段時間後,許多本該執行的操做在中止拖動以後會怎樣執行呢.讓咱們運行下面代碼來看看效果吧.

- (void)viewDidLoad {
    [super viewDidLoad];
    //[self observer];
    NSLog(@"%s", __func__);
    [self test2];
}
- (void)test2 {
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"test2");
    }];
    [[NSRunLoop currentRunLoop]addTimer:timer forMode:NSDefaultRunLoopMode];
    
}
複製代碼

效果以下圖:


我是在13:02:50進行拖動textView,而後13:03:14結束拖動,能夠發現若是當時期間24秒間隔本應該要執行12次打印,最後只執行了兩次,並且這兩次執行是基本緊接着執行的,期間沒有間隔.而後又開始了正常的兩秒種執行一次打印.

因此咱們能夠得出RunLoop的邏輯,當timer添加到RunLoop的NSDefaultRunLoopMode模式時,在切換到UITrackingRunLoopMode模式後,RunLoop會最多暫存兩次操做,而後等到RunLoop切換回NSDefaultRunLoopMode模式下,再緊挨着執行兩次操做.

結論:
因此當NSTimer添加到NSDefaultRunLoopMode模式並非絕對精準的,當咱們滾動一些視圖時,執行操做就會變得不按時.解決方法就是把timer也添加到UITrackingRunLoopMode模式中,或者使用其餘定時器如GCD定時器.

4.3 後臺常駐線程

線程有關知識

  • [NSThread detachNewThreadSelector:@selector(run1) toTarget:self withObject:nil]會建立並自動開啓一條線程執行任務,不須要手動啓動

  • 咱們以前建立線程都是爲了執行特定任務,執行問特定任務後,線程會自動進入死亡狀態.線程進入死亡狀態後,是沒法再次啓動線程,讓線程繼續執行任務的.

    若線程進入死亡狀態再次調用start方法會報錯

利用RunLoop實現後臺常駐線程

咱們在作項目時可能會在後臺執行頻繁操做,在子線程中執行耗時操做(以下載文件,後臺播放音樂,後臺記錄用戶信息),那麼我最好能讓線程不進入死亡狀態所以能夠持續的執行任務,而不是頻繁的建立和銷燬線程.

那麼咱們應該怎麼作呢?

添加一條指向常駐內存的線程強引用,而後在這條線程中建立一個RunLoop,並添加一個Sources,而後開啓RunLoop.緣由是RunLoop只要沒有超時,任務就會一直執行不完,那麼線程就不會進入死亡狀態.

具體實現過程:

  1. 首先建立一條子線程並添加要執行的方法
  2. 在執行的方法中開啓一個RunLoop,並添加一個Source或Timer,若不添加RunLoop循環會直接退出.通常作法是添加一個port即端口,由於port並不須要指定須要作什麼任務,而timer須要指定,咱們這裏添加Source或Timer只是爲了保證循環不退出,因此不須要指定任務,因此通常選擇port.

實現代碼以下:

- (void)viewDidLoad {
    [super viewDidLoad];
    NSLog(@"%s", __func__);
    [self residentThread];
}

- (void)residentThread {
    NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run1) object:nil];
    self.thread = thread;
    [self.thread start];
}
- (void)run1 {
    //這裏寫須要執行的代碼
    NSLog(@"run1 -- %@", [NSThread currentThread]);
    //一個RunLoop至少須要一個Source或者Timer,在這裏添加一個Source1
    [[NSRunLoop currentRunLoop]addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop]run];
    NSLog(@"未開啓RunLoop -- %@", [NSThread currentThread]);
}

複製代碼

3.運行後會發現 未開啓RunLoop 並不打印,由於RunLoop循環一直沒有返回.

爲了線程是否還能夠繼續執行其餘任務即沒有進入死亡狀態,咱們在touchesBegan中調用PerformSelector方法,看看是否會打印.

代碼以下:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:nil];
}

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

複製代碼

運行代碼後點擊屏幕,發現能夠打印,即線程可以繼續執行任務.這樣常駐線程就完成了.

5.RunLoop有關知識注意點

1 只有子線程的RunLoop設置退出時間纔有用,主線程的RunLoop是沒法退出的.即下面這句代碼是不會起到使RunLoop退出的做用.

[[NSRunLoop mainRunLoop]runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
複製代碼

2 RunLoop何時建立和銷燬自動釋放池

首先咱們要知道RunLoop爲何要建立自動釋放池?
由於在一個RunLoop運行循環過程當中會產生大量變量和對象,並且大多數變量是不會再使用的.那麼若不清理掉這些不用的變量,內存就可能會被堆滿.因此RunLoop會按期建立一個自動釋放池,而且在特意時間釋放掉釋放池,並從新再建立一個.

第一次建立: 啓動RunLoop的時候 最後一次銷燬: 退出RunLoop以前 其餘時候的建立和銷燬: 在RunLoop進入休眠狀態前會釋放掉舊的釋放池,釋放池中的變量也一塊兒被銷燬了.而後建立出一個新的釋放池,用來存放新產生的不用的變量.

相關文章
相關標籤/搜索