NSRunLoop

1.什麼是RunLoop

  • 運行循環
  • 一個線程對應一個RunLoop,主線程的RunLoop默認已經啓動,子線程的RunLoop得手動啓動(調用run方法)
  • RunLoop只能選擇一個Mode啓動,若是當前Mode中沒有任何Source(Sources0、Sources1)、Timer,那麼就直接退出RunLoop
  • 線程退出,則RunLoop也退出;強制退出RunLoop,RunLoop也會退出
  • RunLoop做用html

    • 保持程序的持續運行
    • 處理App中的各類事件(好比觸摸事件、定時器事件、Selector事件)
    • 節省CPU資源,提升程序性能:該作事時作事,該休息時休息 ......
  • 模擬RunLoop內部實現面試

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

得到RunLoop對象

  • RunLoop對象
    • NSRunLoop
    • CFRunLoopRef

iOS中有兩套API能夠建立獲取RunLoop對象。分別是Foundation框架的NSRunLoop和C語言的CFRunLoopRef微信

NSRunLoop和CFRunLoopRef都表明着RunLoop對象
NSRunLoop是基於CFRunLoopRef的一層OC包裝,因此要了解RunLoop內部結構,須要多研究CFRunLoopRef層面的API(Core Foundation層面)app

模擬RunLoop內部實現

void message(int num)
{
    printf("執行第%i個任務", num);
}
int main(int argc, const char * argv[]) {
    do {
        printf("有事嗎? 沒事我睡了");
        int number;
        scanf("%i", &number);
        message(number);
    } while (1);
    return 0;
}

 

RunLoop與線程的關係

一個線程對應一個RunLoop,主線程的RunLoop默認程序啓動就已經建立好了。框架

子線程默認沒有RunLoop,不過子線程能夠有RunLoop,子線程的RunLoop得手動建立而且手動啓動(調用run方法)函數

RunLoop在第一次獲取時建立,在線程結束時銷燬oop

能夠理解爲,子線程的RunLoop是懶加載的(主線程除外)。只有用到的時候纔會建立( 調用currentRunLoop方法)。性能

若是是在子線程中調用currentRunLoop,那麼系統會先查看當前子線程是否有與之對應的NSRunLoop,若是沒有就建立一個RunLoop對象學習

注意:若是想給子線程添加一個與之對應的RunLoop,不能經過alloc、init方法,只能經過currentRunLoop,若是用alloc、init建立出來的RunLoop不能添加到子線程。ui

如何獲取RunLoop對象

1.經過Foundation框架獲取

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

2.經過Core Foundation框架獲取

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

RunLoop底層實現

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
        // 建立字典
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        // 建立主線程
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        // 保存主線程
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    // 從字典中獲取子線程
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
        // 若是不存在建立一個新的子線程
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        // 保存子線程
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }

以上是從CF-1151.16的CFRunLoop.c文件中拷貝的RunLoop的源碼:

當咱們經過[NSRunLoop currentRunLoop]調用NSRunLoop的currentRunLoop方法的時候,底層就會調用NSRunLoopRef的get方法。

  1.程序啓動,底層會先建立一個字典。

  2.而後立刻會建立一個主線程的RunLoop,並把主線程做爲key,把主線程的RunLoop做爲value添加到字典中。

注意:這也就是爲何一個線程對應一個RunLoop的緣由由於RunLoop是經過key-value的形式和線程以一一對應的方式保存在字典中的。

  3.若是從子線程經過[NSRunLoop currentRunLoop]調用NSRunLoop的currentRunLoop方法的時候,系統會以子線程做爲key,去字典中取對應的RunLoop對象。

  4.若是取出來的RunLoop對象爲空,則系統會建立一個RunLoop對象並以子線程做爲key把該RunLoop對象存儲到字典中去。

RunLoop相關類

Core Foundation中關於RunLoop的5個類:
  CFRunLoopRef :RunLoop對象
  CFRunLoopModeRef :RunLoop的模式,能夠把RunLoop理解爲空調,對應着許多模式,可是一個RunLoop同時只能執行一種模式
  CFRunLoopSourceRef : 事件來源,用來處理RunLoop的事件
  CFRunLoopTimerRef :定時器,處理和定時器相關的事情
  CFRunLoopObserverRef :經過observer監聽事件

RunLoop的結構

一個RunLoop有多個模式:每一個模式都有各自的source、timer和observer。

注意:一個RunLoop有多個模式,可是在同一時刻只能執行一種模式。

CFRunLoopModeRef:

CFRunLoopModeRef表明RunLoop的運行模式
一個 RunLoop 包含若干個 Mode,每一個Mode又包含若干個Source/Timer/Observer
每次RunLoop啓動時,只能指定其中一個 Mode,這個Mode被稱做 CurrentMode
若是須要切換Mode,只能退出Loop,再從新指定一個Mode進入
這樣作主要是爲了分隔開不一樣組的Source/Timer/Observer,讓其互不影響

系統默認註冊了5個Mode:

  NSDefaultRunLoopMode:App的默認Mode,一般主線程是在這個Mode下運行。程序啓動,若是用戶什麼都沒作,默認就在這個模式
  UITrackingRunLoopMode:界面跟蹤 Mode,用於 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其餘 Mode 影響。程序啓動,若是用戶滑動了scrollView,就會從默認模式切換到這個模式
  UIInitializationRunLoopMode: 在剛啓動 App 時第進入的第一個 Mode,啓動完成後就再也不使用
  GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,一般用不到                                       
  NSRunLoopCommonModes: 這是一個佔位用的Mode,不是一種真正的Mode

PS:前四種模式是真正的模式,最後一種模式不是真正的模式。主要學習前兩種模式和最後一種模式。

runLoop默認是個死循環,源碼以下:

// 用DefaultMode啓動
void CFRunLoopRun(void) {    /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

CFRunLoopSourceRef:

CFRunLoopSourceRef是事件源(輸入源)

按照官方文檔,Source的分類
Port-Based Sources
Custom Input Sources
Cocoa Perform Selector Sources

按照函數調用棧,Source的分類
Source0:非基於Port的, 用於用戶主動觸發事件
Source1:基於Port的,經過內核和其餘線程相互發送消息

CFRunLoopTimerRef:

CFRunLoopTimerRef是基於時間的觸發器
CFRunLoopTimerRef基本上說的就是NSTimer,它受RunLoop的Mode影響
GCD的定時器不受RunLoop的Mode影響

建立出來NSTimer對象,咱們須要把NSTimer對象添加到runLoop中

// 建立一個NSTimer以後, 必須將NSTimer添加到RunLoop中, 才能執行
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(demo) userInfo:nil repeats:YES];
// 添加到runLoop中(下面這就話就是把timer添加到當前線程的默認模式下)
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

runLoop同一時間只能執行一個模式,因此若是把timer添加到默認模式,那麼timer只在默認模式下生效。例如,切換到追蹤模式,默認模式下的timer是無效的。

那麼怎麼讓timer在默認模式和追蹤模式下都有效呢?

/* common modes = { 0 : <CFString 0x105b56e50 [0x104e83180]>{contents = "UITrackingRunLoopMode"} 2 : <CFString 0x104e5f080 [0x104e83180]>{contents = "kCFRunLoopDefaultMode"} } */

//
這是一個佔位用的Mode,不是一種真正的Mode // 其實Common是一個標識, 它是將NSDefaultRunLoopMode和UITrackingRunLoopMode標記爲了Common // 因此, 只要將timer添加到Common佔位模式下,timer就能夠在NSDefaultRunLoopMode和UITrackingRunLoopMode模式下都能運行
// 至關於timer添加到了這兩個模式中,在這兩個模式中都有效
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

 

GCD的timer不受NSRunLoop定時器的影響

    // 1.建立tiemr
    // queue: 表明定時器未來回調的方法在哪一個線程中執行
//    dispatch_queue_t queue = dispatch_get_main_queue();
    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    self.timer = timer;
    // 2.設置timer
    /*
     第一個參數: 須要設置哪一個timer
     第二個參數: 指定定時器開始的時間
     第三個參數: 指定間隔時間
     第四個參數: 定時器的精準度, 若是傳0表明要求很是精準(系統會讓定時器執行的時間變得更加準確) 若是傳入一個大於0的值, 就表明咱們容許的偏差
     // 例如傳入60, 就表明容許偏差有60秒
     */
    
    // 定時器開始時間
//    dispatch_time_t startTime = DISPATCH_TIME_NOW;
    // 調用這個函數,就能夠指定兩秒以後開始/而不是當即開始
    dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC));
    
    // 定時器間隔的時間
    uint64_t timerInterval = 2.0 * NSEC_PER_SEC;
    dispatch_source_set_timer(timer, startTime, timerInterval, 0 * NSEC_PER_SEC);
    
    // 3.設置timer的回調
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"我被調用了  %@", [NSThread currentThread]);
    });
    
    // 4.開始執行定時器
    dispatch_resume(timer);
    
}

CFRunLoopObserverRef:

CFRunLoopObserverRef是觀察者,可以監聽RunLoop的狀態改變
能夠監聽的時間點有如下幾個:

自定義Observer來監聽指定線程的狀態的改變:

    // 0.建立一個監聽對象
    /*
     第一個參數: 告訴系統如何給Observer對象分配存儲空間
     第二個參數: 須要監聽的類型
     第三個參數: 是否須要重複監聽
     第四個參數: 優先級
     第五個參數: 監聽到對應的狀態以後的回調
     */
    CFRunLoopObserverRef observer =  CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
//        NSLog(@"%lu", 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;
        }
    });
    
    // 1.給主線程的RunLoop添加監聽
    /*
     第一個參數:須要監聽的RunLoop對象
     第二個參數:給指定的RunLoop對象添加的監聽對象
     第三個參數:在那種模式下監聽
     */
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    
    // 若是經過scheduled方法建立NSTimer, 系統會默認添加到當前線程的默認模式下
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(demo) userInfo:nil repeats:YES];

 總結:

一條線程對應一條RunLoop,程序一啓動,主線程的RunLoop就已經建立而且和主線程綁定好。經過查看RunLoop源代碼,系統內部是經過字典的形式把線程和RunLoop進行了綁定。

子線程的RunLoop默認是沒有的,若是想使用子線程的RunLoop,只須要在子線程調用NSRunLoopcurrentRunLoop方法便可。

咱們能夠把RunLoop理解爲懶加載的,只有在用到的時候纔會建立。ru若是子線程中調用了currentRunLoop方法,那麼系統會先根據子線程去字典中取對應的RunLoop,若是沒有,則系統會建立一個RunLoop而且和該子線程進行綁定而且保存到字典中。

每一個RunLoop中又有不少的mode,每一個mode中又能夠有不少的source、timer和observer。須要注意的是,RunLoop在同一時刻只能執行一種模式,也就是同一時刻,只有一個模式中的source、timer和observer有效,其餘模式的source、timer和observer無效。蘋果這樣作的目的是防止不一樣模式中的source、timer和observer相互影響,很差控制。

能夠經過timer的形式來監聽RunLoop的執行流程:

進入RunLoop,首先會處理一些系統的事件(也就是首先執行timer、source0、source1)當處理完後,RunLoop就會睡覺。當用戶觸發一些事件後,RunLoop就會從睡眠中醒來,處理timer、source0和source1.處理完事件後又繼續睡覺。

RunLoop是有生命週期的,RunLoop掛掉有兩種狀況:

  1.生命週期到了,默認RunLoop的生命週期是很大的,不過咱們能夠本身設置runLoop的生命週期

  2.線程掛了,RunLoop也會掛掉

 

RunLoop的應用

runLoop主要有5個應用場景:NSTimer、ImageView顯示圖片、performSelecter、常駐線程、自動釋放池

1.NSTimer

2.ImageView顯示

  默認程序啓動會進入runLoop的default模式。performSelecter: withObject:afterDelay:inMode:方法默認就是在default模式下有效。而在track追蹤模式下無效,因此能夠經過設置模式來控制imageView圖片的顯示。

 

// 只有在追蹤模式下才會給imageView設置圖片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"def"] afterDelay:2.0 inModes:@[UITrackingRunLoopMode]];

  開發中通常在默認狀況下設置圖片而在追蹤模式下是不設置圖片的,這樣一來能夠提升咱們應用程序的流暢性,爲何呢?若是在track模式下,不只處理屏幕的拖拽事件,還要給imageView設置圖片,很容易出現程序卡頓的現象。

3.PerformSelector

4.常駐線程

常駐線程應用場景:

  舉個例子,某個應用須要頻繁的下載或者上傳大容量的音頻或者視頻,默認主線程就是一個常駐線程,可是這種耗時操做確定要轉移到子線程中取完成。好比說微信\陌陌,用戶有時候須要一直髮送語音,若是每發送一條語音就開啓一個自子線程,那麼頻繁的開啓、銷燬線極大的消耗手機性能,因此常駐線程就應運而生。

如何建立常駐線程?

嘗試一:再次調用[self.thread  start];答案固然是否認的。緣由以下:

  注意點:默認狀況下,只要一個線程的任務執行完畢,那麼這個線程就不能使用了。因此不能經過start方法來從新啓動一個已經執行完任務的線程。不然會報如下錯誤: -[WSThread start]: attempt to start the thread again'

嘗試二:給這個子線程一個執行不完的任務while(1);答案依然是否認的,緣由以下:

  把while(1)添加到子線程執行,而子線程的任務中有一個while死循環,那麼其餘任務永遠也執行不到。

  因此,經過死循環雖然保證了子線程永遠不死,可是不能讓子線程處理任務,由於子線程一直在處理while死循環的任務。

嘗試三:聯想主線程爲何不死,由於主線程默認一啓動就會綁定一個runLoop,因此嘗試給子線程綁定一個runLoop

[NSRunLoop currentRunLoop];
[runLoop run];

可是僅僅建立一個runLoop而後run依然無效。緣由以下:

注意:
(1). currentRunLoop僅僅表明建立了一個NSRunLoop對象, 並無運行RunLoop
(2). 一個NSRunLoop中, 若是沒有source或者timer, 那麼NSRunLoop就會退出死循環(面試極可能問到)。由於若是runLoop沒有source和timer,那麼這個runLoop就沒有source和timer事件處理,這個runLoop也就變得沒有意義,因此runLoop會自動退出。(runLoop是否退出和observer沒有關係,只和source和timer有關係)

因此,給runLoop添加一個source或者timer

最終的解決方案:

NSRunLoop *runLoop =[NSRunLoop currentRunLoop];
// 如下代碼的目的是爲了保證runloop不死
/*
// 給runLoop添加一個timer
//   NSTimer *timer = [NSTimer timerWithTimeInterval:99999 target:self selector:@selector(demo) userInfo:nil repeats:NO];
//  [runLoop addTimer:timer forMode:NSDefaultRunLoopMode];
*/
// 或者給runLoop添加一個source,通常都是添加source,不添加timer,寫三方框架的大牛都這麼寫 [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run];

至此,一個常駐子線程就已經建立好了,而且能夠接受並處理事件。而且只要是在這個常駐子線程中執行的任務,都是在同一個線程中。

以下是建立常駐子線程的代碼:

#import "ViewController.h"
#import "WSThread.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@property (nonatomic, strong) WSThread *thread; /**< 子線程 */

@end

@implementation ViewController


- (void)viewDidLoad
{
    [super viewDidLoad];
    
    
    self.thread = [[WSThread alloc] initWithTarget:self selector:@selector(demo) object:nil];
    [self.thread start];
}

- (void)demo
{
    // 在子線程執行
    NSLog(@"%s", __func__);
    
    // 注意點: 默認狀況下只要一個線程的任務執行完畢, 那麼這個線程就不能使用了
    // 在self.thread線程中執行test方法
//    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES];
//    while(1);
    
    // 給子線程添加一個RunLoop
    // 注意:
    // 1. currentRunLoop僅僅表明建立了一個NSRunLoop對象, 並無運行RunLoop
    // 2. 一個NSRunLoop中, 若是沒有source或者timer, 那麼NSRunLoop就會退出死循環
    
    NSRunLoop *runLoop =[NSRunLoop currentRunLoop];
    // 如下代碼的目的是爲了保證runloop不死
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runLoop run];
    
    NSLog(@"-----------");
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 主線程
    NSLog(@"%s", __func__);
//    [self.thread start];
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES];
}

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

@end

打印結果:

2015-10-31 17:46:29.780 08-RunLoop應用場景[3855:303176] -[ViewController touchesBegan:withEvent:]
2015-10-31 17:46:29.780 08-RunLoop應用場景[3855:303300] -[ViewController test] <XMGThread: 0x7fe44a53a290>{number = 2, name = (null)}
2015-10-31 17:46:29.959 08-RunLoop應用場景[3855:303176] -[ViewController touchesBegan:withEvent:]
2015-10-31 17:46:29.959 08-RunLoop應用場景[3855:303300] -[ViewController test] <XMGThread: 0x7fe44a53a290>{number = 2, name = (null)}
2015-10-31 17:46:30.121 08-RunLoop應用場景[3855:303176] -[ViewController touchesBegan:withEvent:]
2015-10-31 17:46:30.122 08-RunLoop應用場景[3855:303300] -[ViewController test] <XMGThread: 0x7fe44a53a290>{number = 2, name = (null)}
2015-10-31 17:46:30.266 08-RunLoop應用場景[3855:303176] -[ViewController touchesBegan:withEvent:]
2015-10-31 17:46:30.267 08-RunLoop應用場景[3855:303300] -[ViewController test] <XMGThread: 0x7fe44a53a290>{number = 2, name = (null)}
2015-10-31 17:46:30.431 08-RunLoop應用場景[3855:303176] -[ViewController touchesBegan:withEvent:]
2015-10-31 17:46:30.432 08-RunLoop應用場景[3855:303300] -[ViewController test] <XMGThread: 0x7fe44a53a290>{number = 2, name = (null)}


5.自動釋放池

 程序「即將進入runLoop」會建立自動釋放池,「即將退出runLoop」會銷燬自動釋放池。

即將進入休眠狀態會銷燬以前的自動釋放池,再建立一個新的自動釋放池。

    /*
     _wrapRunLoopWithAutoreleasePoolHandler
     + activities = 0x1 = 1 = 即將進入RunLoop
     + 建立一個自動釋放池
    
    _wrapRunLoopWithAutoreleasePoolHandler  
     + activities = 0xa0 = 160 = 128 + 32
     +  32 即將進入休眠  1.銷燬一個自動釋放池  2.再建立一個新的自動釋放池
     +  128 即將退出RunLoop  銷燬一個自動釋放池
     */
    NSLog(@"%@", [NSRunLoop currentRunLoop]);
    NSLog(@"%d", 1 << 0); // 1
    NSLog(@"%d", 1 << 1); // 2
    NSLog(@"%d", 1 << 2); // 4
    NSLog(@"%d", 1 << 5); // 32
    NSLog(@"%d", 1 << 6); // 64
    NSLog(@"%d", 1 << 7); // 128 

學習RunLoop的資料

蘋果官方文檔https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.htmlCFRunLoopRef是開源的http://opensource.apple.com/source/CF/CF-1151.16/

相關文章
相關標籤/搜索