你好,歡迎來到IOS教程網

 Ios教程網 >> IOS編程開發 >> IOS開發基礎 >> 解密——神秘的RunLoop

解密——神秘的RunLoop

編輯:IOS開發基礎

1599332-8949cd7b657686dc.jpg

投稿文章,作者:niu神DNS(簡書)

引言

一直以來RunLoop就是個神秘的領域,好多2.3年的開發者都不能准確的表述它的作用,說它神秘,其實RunLoop並沒有大家想象中的那麼神秘,那麼不好理解,本文就帶大家好好剖析一下“神秘的RunLoop”。

什麼是RunLoop

從字面上看

  • 運行循環

  • 跑圈

1599332-07f248f605df78f3.jpg

循環

基本作用

  • 保持程序的持續運行(比如主運行循環)

  • 處理App中的各種事件(比如觸摸事件、定時器事件、Selector事件)

  • 節省CPU資源,提高程序性能:該做事時做事,該休息時休息

存在價值

1599332-3123425efbef4349.jpg

1599332-9d876bccecf7f79c.jpg

main函數中的RunLoop(主運行循環)

1465699923398310.jpg

  • 第14行代碼的UIApplicationMain函數內部就啟動了一個RunLoop。

  • 所以UIApplicationMain函數一直沒有返回,保持了程序的持續運行。

  • 這個默認啟動的RunLoop是跟主線程相關聯的。

RunLoop對象

  • iOS中有2套API來訪問和使用RunLoop

  • Foundation

  • NSRunLoop

  • Core Foundation

  • CFRunLoopRef

  • NSRunLoop和CFRunLoopRef都代表著RunLoop對象

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

RunLoop資料

  • 蘋果官方文檔

  • CFRunLoopRef(開源)

RunLoop與線程

每條線程都有唯一的一個與之對應的RunLoop對象

主線程的RunLoop已經自動創建好了,子線程的RunLoop需要主動創建

RunLoop在第一次獲取時創建,在線程結束時銷毀

獲取RunLoop對象

Foundation

[NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象

Core Foundation

CFRunLoopGetCurrent(); // 獲得當前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象

RunLoop相關類

Core Foundation中關於RunLoop的5個類

  • CFRunLoopRef

  • CFRunLoopModeRef

  • CFRunLoopSourceRef

  • CFRunLoopTimerRef

  • CFRunLoopObserverRef

注:RunLoop如果沒有這些東西 會直接退出

CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的運行模式:

  • 一個 RunLoop 包含若干個 Mode,每個Mode又包含若干個Source/Timer/Observer

  • 每次RunLoop啟動時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode

  • 如果需要切換Mode,只能退出Loop,再重新指定一個Mode進入

  • 這樣做主要是為了分隔開不同組的Source/Timer/Observer,讓其互不影響

1599332-513941a7f2f0b6bf.jpg

相關類

系統默認注冊了5個Mode:(前兩個跟最後一個常用)

  • kCFRunLoopDefaultMode:App的默認Mode,通常主線程是在這個Mode下運行

  • UITrackingRunLoopMode:界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響

  • UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用

  • GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到

  • kCFRunLoopCommonModes: 這是一個占位用的Mode,不是一種真正的Mode

CFRunLoopSourceRef

CFRunLoopSourceRef是事件源(輸入源)

按照官方文檔的分類

  • Port-Based Sources (基於端口,跟其他線程交互,通過內核發布的消息)

  • Custom Input Sources (自定義)

  • Cocoa Perform Selector Sources (performSelector...方法)

按照函數調用棧的分類

  • Source0:非基於Port的

  • Source1:基於Port的

  • Source0: event事件,只含有回調,需要先調用CFRunLoopSourceSignal(source),將這個 Source 標記為待處理,然後手動調用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop。

  • Source1: 包含了一個 mach_port 和一個回調,被用於通過內核和其他線程相互發送消息,能主動喚醒 RunLoop 的線程。

函數調用棧

1465700131571169.jpg

CFRunLoopTimerRef

  • CFRunLoopTimerRef是基於時間的觸發器

  • 基本上說的就是NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影響

  • GCD的定時器不受RunLoop的Mode影響

CFRunLoopObserverRef

CFRunLoopObserverRef是觀察者,能夠監聽RunLoop的狀態改變

可以監聽的時間點有以下幾個

1599332-506dffddf2098c02.jpg

可監聽狀態

使用

- (void)observer
{
    // 創建observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"----監聽到RunLoop狀態發生改變---%zd", activity);
    });
    // 添加觀察者:監聽RunLoop的狀態
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    // 釋放Observer
    CFRelease(observer);
}

特別注意

/*
    CF的內存管理(Core Foundation)
    1.凡是帶有Create、Copy、Retain等字眼的函數,創建出來的對象,都需要在最後做一次release
    * 比如CFRunLoopObserverCreate
    2.release函數:CFRelease(對象);
 */

RunLoop處理邏輯

- 官方版

1599332-196a8c5eec368db8.jpg

官方版

1465700299984183.jpg

邏輯

- 網友整理版

1465700321708796.jpg

注:進入RunLoop前 會判斷模式是否為空,為空直接退出。

RunLoop應用

  • NSTimer

  • ImageView顯示

  • PerformSelector

  • 常駐線程

  • 自動釋放池

1.NSTimer(最常見RunLoop使用)

- (void)timer
{
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    // 定時器只運行在NSDefaultRunLoopMode下,一旦RunLoop進入其他模式,這個定時器就不會工作
    //    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    // 定時器只運行在UITrackingRunLoopMode下,一旦RunLoop進入其他模式,這個定時器就不會工作
    //    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    // 定時器會跑在標記為common modes的模式下
    // 標記為common modes的模式:UITrackingRunLoopMode和NSDefaultRunLoopMode兼容
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)timer2
{
    // 調用了scheduledTimer返回的定時器,已經自動被添加到當前runLoop中,而且是NSDefaultRunLoopMode
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    // 修改模式
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

場景還原

拖拽時模式由NSDefaultRunLoopMode 進入 UITrackingRunLoopMode

此時如下圖: NSTimer 不再響應圖片停止輪播

1599332-edb373e839c5d023.gif

NSDefaultRunLoopMode模式

NSRunLoopCommonModes 模式下兩種模式都可運行

此時如下圖: NSTimer 在兩個模式下都可正常運行

1599332-e22a55c291a87eb7.gif

2.ImageView

需求:當用戶在拖拽時(UI交互時)不顯示圖片,拖拽完成時顯示圖片

  • 方法1 監聽UIScrollerView滾動 (通過UIScrollViewDelegate監聽,此處不再舉例)

  • 方法2 RunLoop 設置運行模式

// 只在NSDefaultRunLoopMode模式下顯示圖片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

3.PerformSelector

1465700442751910.jpg

PerformSelector

inModes:設置運行模式

4.常駐線程 (重要)

應用場景:經常在後台進行耗時操作,如:監控聯網狀態,掃描沙盒等 不希望線程處理完事件就銷毀,保持常駐狀態

第一種(推薦)

開啟

- (void)run
{
  //addPort:添加端口(就是source)  forMode:設置模式
   [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
  //啟動RunLoop
    [[NSRunLoop currentRunLoop] run];
 /*
  //另外兩種啟動方式
    [NSDate distantFuture]:遙遠的未來  這種寫法跟上面的run是一個意思
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    不設置模式
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
  */
}

退出-退出當前線程

[NSThread exit];

第二種(奇葩法)

優點:退出RunLoop比較方便-定義個標記 while(flag){...}

- (void)run
{
    while (1) {
        [[NSRunLoop currentRunLoop] run];
    }
}

5.自動釋放池

1599332-76577a6a1a31a55c.jpg

在休眠前(kCFRunLoopBeforeWaiting)進行釋放,處理事件前創建釋放池,中間創建的對象會放入釋放池

特別注意:在啟動RunLoop之前建議用 @autoreleasepool {...}包裹

意義:創建一個大釋放池,釋放{}期間創建的臨時對象,一般好的框架的作者都會這麼做

- (void)execute
{
    @autoreleasepool {
        NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    }
}

題外話:

以後為了增加用戶體驗 在用戶UI交互的時候 不做事件處理 我們可以把需要做的操作放到NSDefaultRunLoopMode

補充:GCD定時器

一般的NSTimer定時器因為受到RunLoop,會存在時間不准時的情況.

上文有提到GCD不受RunLoop影響,下面簡單的說一下它的使用

/** 定時器(這裡不用帶*,因為dispatch_source_t就是個類,內部已經包含了*) */
@property (nonatomic, strong) dispatch_source_t timer;
int count = 0;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 獲得隊列
//    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue = dispatch_get_main_queue();
    // 創建一個定時器(dispatch_source_t本質還是個OC對象)
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    // 設置定時器的各種屬性(幾時開始任務,每隔多長時間執行一次)
    // GCD的時間參數,一般是納秒 NSEC_PER_SEC(1秒 == 10的9次方納秒)
    // 何時開始執行第一個任務
    // dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC) 比當前時間晚3秒
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
    uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(self.timer, start, interval, 0);
    // 設置回調
    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"------------%@", [NSThread currentThread]);
        count++;
//        if (count == 4) {
//            // 取消定時器
//            dispatch_cancel(self.timer);
//            self.timer = nil;
//        }
    });
    // 啟動定時器
    dispatch_resume(self.timer);
}

RunLoop面試題

經常會有喜歡裝B的面試官,面試的時候就喜歡問RunLoop,其實他真的會嗎? 說不定他自己都不太理解

下面我對有關RunLoop的面試做一個簡單的總結,也算是對全文一個總結

什麼是RunLoop?

從字面上看:運行循環、跑圈

其實它內部就是do-while循環,在這個循環內部不斷的處理各種任務(比如Source、Timer、Observer)

一個線程對應一個RunLoop,主線程的RunLoop默認已經啟動,子線程的RunLoop需要手動啟動(調用run方法)

RunLoop只能選擇一個Mode啟動,如果當前Mode中沒有任何Soure、Timer、Observer,那麼就直接退出RunLoop

在開發中如何使用RunLoop?什麼應用場景?

開啟一個常駐線程(讓一個子線程不進入消亡狀態,等待其他線程發來消息,處理其他事件)

在子線程中開啟一個定時器

在子線程中進行一些長期監控

可以控制定時器在特定模式下執行

可以讓某些事件(行為、任務)在特定模式下執行

可以添加Observer監聽RunLoop的狀態,比如監聽點擊事件的處理(在所有點擊事件之前做一些事情)

最後

之前發布的文章寫得不是很完整,我又花了兩天時間重新做了梳理,還有什麼不足之處,歡迎大家指出,我會第一時間更新.

特別感謝

感謝@Delpan提出的寶貴意見,文章已經增加了source0/source1的解釋

  1. 上一頁:
  2. 下一頁:
蘋果刷機越獄教程| IOS教程問題解答| IOS技巧綜合| IOS7技巧| IOS8教程
Copyright © Ios教程網 All Rights Reserved