RunLoop淺析

(這也是以前寫的, 主要做個(gè)記錄方便隨時(shí)查閱, 不對的地方請指正!)

RunLoop雖然在平時(shí)開發(fā)過程中使用不多, 但是是非常重要的, 往往能夠解決關(guān)鍵性問題, 比如計(jì)時(shí)器突然不準(zhǔn), 頁面滑動(dòng)有時(shí)會(huì)卡頓等問題, 都可以用RunLoop來解決, 本篇文章是總結(jié)性和實(shí)踐性文章, 主要做個(gè)記錄, 方便自己開發(fā)中查閱.

什么是RunLoop?

RunLoop從字面量意思看就是一個(gè)運(yùn)行循環(huán), 其實(shí)是一個(gè)事件處理的循環(huán), 用來不停的調(diào)度任務(wù)和處理輸入事件. RunLoop 內(nèi)部其實(shí)是一個(gè)do-while大循環(huán), 在這個(gè)循環(huán)里處理輸入事件, 比如:點(diǎn)擊事件, 滑動(dòng)屏幕, 定時(shí)器等, 當(dāng)處理完一個(gè)任務(wù)后RunLoop進(jìn)入休眠, 有任務(wù)時(shí)又喚醒RunLoop處理事件.

簡言之, RunLoop是管理線程各類輸入事件的對象.

基本作用:

  1. 保持程序的持續(xù)運(yùn)行. 如果線程中沒有RunLoop, 線程執(zhí)行完任務(wù)隊(duì)列中的任務(wù)后, 就會(huì)退出. 所以app的主線程必定有一個(gè)RunLoop, 一直讓主線程處理不退出狀態(tài), 除非系統(tǒng)或者手動(dòng)讓RunLoop停止工作, 此時(shí)主線程退出, app掛掉.
  2. 處理app中的各類輸入事件. 比如:點(diǎn)擊事件,觸摸事件, 方法調(diào)用(seletor)事件, 定時(shí)器等, RunLoop就是一直在等待處理這些事件.
  3. 節(jié)省CPU資源. 雖然RunLoop 是一個(gè)大循環(huán), 但是不同于while(1)死循環(huán), 當(dāng)有任務(wù)的事件會(huì)喚醒RunLoop執(zhí)行任務(wù), 沒有任務(wù)時(shí)休眠RunLoop.

在我們程序的main函數(shù)中, 返回UIApplicationMain方法調(diào)用的返回值, 這個(gè)方法程序運(yùn)行時(shí)是不會(huì)被返回的, 因?yàn)樵谶@個(gè)方法內(nèi)部會(huì)創(chuàng)建一個(gè)RunLoop, 維持主線程的持續(xù)運(yùn)行.

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([CZPAppDelegate class]));
    }
}

類似于下面的偽代碼:

int main(int argc, char * argv[]) {
    BOOL isRunning = YES;
    
    do {
        // 通知觀察者告知RunLoop的狀態(tài)
        // ...
        // 處理各類事件
        // ...
        // 休眠, 等待被喚醒
        // 喚醒
        
        // 各類條件是否滿足??
        BOOL conditions;
        if (conditions) {
            isRunning = YES;
        }else {
            isRunning = NO;
        }
        
    } while (isRunning);
    
    return 0;
}

RunLoop對象

RunLoop是管理線程輸入事件的對象

  • Core Foundation框架中使用 CFRunLoopRef. 是純C寫的代碼, 所以是線程安全的.
  • Foundation框架中使用NSRunLoop, 是封裝的 CFRunLoopRef中, 不是線程安全的. 兩者都代表RunLoop對象, 是可以等價(jià)轉(zhuǎn)換的.

要想了解RunLoop很有必要知道底層的實(shí)現(xiàn), 蘋果公司開源了這個(gè)部分代碼, 其中跟RunLoop相關(guān)的就兩個(gè)文件: CFRunLoop.h, CFRunLoop.c.

CFRunLoopRef開源代碼下載地址
http://opensource.apple.com/source/CF/CF-1151.16/

獲取 RunLoop 對象

  1. 獲取當(dāng)前 RunLoop 對象:

    // NSRunloop
     NSRunLoop * runloop = [NSRunLoop currentRunLoop];
     
    // CFRunLoopRef
    CFRunLoopRef runloop =   CFRunLoopGetCurrent();
    
  2. 獲取主線程的 RunLoop 對象:

    // NSRunloop
     NSRunLoop * runloop = [NSRunLoop mainRunLoop];
    
    // CFRunLoopRef
     CFRunLoopRef runloop =   CFRunLoopGetMain();
    

注意:

創(chuàng)建 RunLoop 對象不是通過 alloc init 的方式創(chuàng)建的, 是直接獲取, 沒有的話就會(huì)創(chuàng)建. 主線程中也可以通過方式一來獲取 RunLoop 對象.

RunLoop 與線程

線程和RunLoop的關(guān)系:

  • 每條線程都有唯一的一個(gè)RunLoop 對象.
  • 主線程的RunLoop在程序啟動(dòng)時(shí)自動(dòng)創(chuàng)建好了, 子線程的需要手動(dòng)創(chuàng)建.
  • RunLoop 在第一次獲取時(shí)創(chuàng)建, 線程銷毀時(shí)銷毀.

線程和RunLoop的對應(yīng)關(guān)系保存在一個(gè)全局的字典中, 通過key-value保持. 下面是CFRunLoop.m中的源代碼, 無論是主線程還是子線程在獲取RunLoop的時(shí)候會(huì)調(diào)用_CFRunLoopGet0函數(shù), 該函數(shù)判斷該線程RunLoop對象, 有則返回, 沒有則創(chuàng)建并保存.

// 創(chuàng)建字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 創(chuàng)建主線程runloop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主線程runloop
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);

// 從字典中獲取子線程的runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
// 如果子線程的runloop不存在,那么就為該線程創(chuàng)建一個(gè)對應(yīng)的runloop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 把當(dāng)前子線程和對應(yīng)的runloop保存到字典中
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}

RunLoop 運(yùn)行原理

Runloop運(yùn)行原理圖中可以看到, 一個(gè)線程中的RunLoop接受 source 源的輸入事件和 timer 定時(shí)器事件, 當(dāng)有這些事件發(fā)生時(shí), 就會(huì)喚醒 RunLoop 執(zhí)行相應(yīng)任務(wù).

運(yùn)行原理圖

RunLoop 相關(guān)類

有5個(gè)相關(guān)類:

  • CFRunloopRef
  • CFRunloopModeRef: Runloop的運(yùn)行模式
  • CFRunloopSourceRef: Runloop要處理的事件源
  • CFRunloopTimerRef: Timer事件
  • CFRunloopObserverRef: Runloop的觀察者(監(jiān)聽者)

Runloop和相關(guān)類之間的關(guān)系圖:

關(guān)系圖

CFRunloopModeRef代表RunLoop的運(yùn)行模式, 每次運(yùn)行只能處于一個(gè)模式下, 每個(gè)mode下有很多source, timer, observer, 如果要切換模式, 只能退出當(dāng)前循環(huán), 重新指定一個(gè)新的mode后再次進(jìn)入循環(huán).
RunLoop 這么設(shè)計(jì)其實(shí)是為了避免各個(gè)mode下的source, timer, observer相互影響.

CFRunloopModeRef有五種mode:

  • kCFRunLoopDefaultMode: App的默認(rèn)Mode,通常主線程是在這個(gè)Mode下運(yùn)行.

  • UITrackingRunLoopMode: 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他 Mode 影響.

  • UIInitializationRunLoopMode: 在剛啟動(dòng) App 時(shí)第進(jìn)入的第一個(gè) Mode,啟動(dòng)完成后就不再使用.

  • GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到

  • kCFRunLoopCommonModes: 這是一個(gè)占位用的Mode,不是一種真正的Mode.

CFRunloopSourceRef分為兩種:

  • source0: 非基于Port的, 把事件告訴RunLoop, 需要手動(dòng)激活.
  • Source1: 通過系統(tǒng)內(nèi)核來喚醒.
  • 可以通過打斷點(diǎn)的方式查看一個(gè)方法的函數(shù)調(diào)用棧

CFRunLoopObserverRef觀察者,監(jiān)聽RunLoop的狀態(tài):

監(jiān)聽的狀態(tài)是枚舉:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),           //即將進(jìn)入Runloop
    kCFRunLoopBeforeTimers = (1UL << 1),    //即將處理NSTimer
    kCFRunLoopBeforeSources = (1UL << 2),   //即將處理Sources
    kCFRunLoopBeforeWaiting = (1UL << 5),   //即將進(jìn)入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),    //剛從休眠中喚醒
    kCFRunLoopExit = (1UL << 7),            //即將退出runloop
    kCFRunLoopAllActivities = 0x0FFFFFFFU   //所有狀態(tài)改變
};

給RunLoop添加監(jiān)聽者:

//創(chuàng)建一個(gè)runloop監(jiān)聽者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

   NSLog(@"監(jiān)聽runloop狀態(tài)改變---%zd",activity);
});

//為runloop添加一個(gè)監(jiān)聽者
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

// 必須釋放
CFRelease(observer);

CFRunloopTimerRef是定時(shí)器事件, NSTimer是基于CFRunloopTimerRef的封裝, 是基于時(shí)間的觸發(fā)器, 需要把timer加入到RunLoop里面去, RunLoop會(huì)在相應(yīng)的時(shí)間點(diǎn)注冊事件, 等時(shí)間到了觸發(fā)喚醒RunLoop執(zhí)行事件.

定時(shí)器在開發(fā)中使用廣泛, 但是很容易犯錯(cuò), 有兩種方式設(shè)置定時(shí)事件, NSTimerGCD, 兩者的定時(shí)器不同, GCD的更加精準(zhǔn), 不受RunLoop的影響. 而且RunLoop里面設(shè)置循環(huán)過期時(shí)間也是用的GCD的定時(shí)器, 由此可知, RunLoop 會(huì)使用 GCD 的部分功能.

注意:

使用timer 的時(shí)候一定要注意添加到哪種mode模式下, 一般標(biāo)記為NSRunLoopCommonModes下, 也就是說任何模式下都可以正常運(yùn)行定時(shí)器, 否則只能特定模式下運(yùn)行, 當(dāng)RunLoop切換模式后, 定時(shí)器不正常工作.

NSTimer + RunLoop用法:

- (void)timer2 {
   //NSTimer 調(diào)用了scheduledTimer方法,那么會(huì)自動(dòng)添加到當(dāng)前的runloop里面去,而且runloop的運(yùn)行模式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 *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
   
   // 定時(shí)器添加到UITrackingRunLoopMode模式,一旦runloop切換模式,那么定時(shí)器就不工作
   // [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
   
   // 占位模式:common modes標(biāo)記
   // 被標(biāo)記為common modes的模式 kCFRunLoopDefaultMode  UITrackingRunLoopMode
   [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
   
   // NSLog(@"%@",[NSRunLoop currentRunLoop]);
}
    
- (void)run {
   NSLog(@"---run---%@", [NSRunLoop currentRunLoop].currentMode);
}
    
- (IBAction)btnClick {
   NSLog(@"---btnClick---");
}

還有一種辦法是添加兩次:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

GCD 定時(shí)器用法:

//0.創(chuàng)建一個(gè)隊(duì)列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);

//1.創(chuàng)建一個(gè)GCD的定時(shí)器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

//2.設(shè)置定時(shí)器的開始時(shí)間,間隔時(shí)間以及精準(zhǔn)度
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW,3.0 *NSEC_PER_SEC);
//設(shè)置定時(shí)器工作的間隔時(shí)間
uint64_t intevel = 1.0 * NSEC_PER_SEC;

/*
第四個(gè)參數(shù):定時(shí)器的精準(zhǔn)度,如果傳0則表示采用最精準(zhǔn)的方式計(jì)算,如果傳大于0的數(shù)值,則表示該定時(shí)切換i可以接收該值范圍內(nèi)的誤差,通常傳0
該參數(shù)的意義:可以適當(dāng)?shù)奶岣叱绦虻男阅?注意點(diǎn):GCD定時(shí)器中的時(shí)間以納秒為單位
*/
dispatch_source_set_timer(timer, start, intevel, 0 * NSEC_PER_SEC);

//3.設(shè)置定時(shí)器開啟后回調(diào)的方法
dispatch_source_set_event_handler(timer, ^{
   NSLog(@"------%@", [NSThread currentThread]);
});

//4.執(zhí)行定時(shí)器
dispatch_resume(timer);

//注意:dispatch_source_t本質(zhì)上是OC類,在這里是個(gè)局部變量,需要強(qiáng)引用
self.timer = timer;

RunLoop運(yùn)行邏輯

運(yùn)行邏輯就是一個(gè)do-while大循環(huán), 在這個(gè)循環(huán)里面處理事件, 監(jiān)聽RunLoop的狀態(tài), 不停的休眠-喚醒-處理-休眠這個(gè)過程.

處理過程
邏輯圖

RunLoop應(yīng)用

下面是開發(fā)中經(jīng)常使用RunLoop的地方:

NSTimer

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:3 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"---timer block---");
    }];
    
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

控制臺輸出:

2017-02-28 11:17:46.102 test[2831:93953] ---timer block---
2017-02-28 11:17:49.102 test[2831:93953] ---timer block---
2017-02-28 11:17:52.102 test[2831:93953] ---timer block---
2017-02-28 11:17:55.102 test[2831:93953] ---timer block---
2017-02-28 11:17:58.102 test[2831:93953] ---timer block---
2017-02-28 11:18:01.175 test[2831:93953] ---timer block---

ImageView顯示

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];

PerformSelector

有很多 PerformSelector 方法都是需要制定mode, date的, 正如上面所示.

當(dāng)調(diào)用 NSObject 的 performSelecter:afterDelay: 后,實(shí)際上其內(nèi)部會(huì)創(chuàng)建一個(gè) Timer 并添加到當(dāng)前線程的 RunLoop 中。所以如果當(dāng)前線程沒有 RunLoop,則這個(gè)方法會(huì)失效。

當(dāng)調(diào)用 performSelector:onThread: 時(shí),實(shí)際上其會(huì)創(chuàng)建一個(gè) Timer 加到對應(yīng)的線程去,同樣的,如果對應(yīng)線程沒有 RunLoop 該方法也會(huì)失效。

常駐線程

有時(shí)需要在后臺開啟一個(gè)線程持續(xù)的做任務(wù), 比如搜集數(shù)據(jù), 語音喚起等, 無非就是在一個(gè)線程中開啟一個(gè)RunLoop 讓它維持線程的持續(xù)運(yùn)行, 而不是執(zhí)行完任務(wù)后退出.
AFN框架中也是用了:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}
 
+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

注意:

子線程的RunLoop里面至少要有一個(gè)source或者是timer, 只有observer不行的.

自動(dòng)釋放池

自動(dòng)釋放池的第一次創(chuàng)建:
第一次進(jìn)入RunLoop時(shí)候自動(dòng)創(chuàng)建.

自動(dòng)釋放池的第一次銷毀:
RunLoop即將進(jìn)入休眠的時(shí)候

其它情況下創(chuàng)建和銷毀:
喚醒RunLoop的時(shí)候會(huì)創(chuàng)建新的自動(dòng)釋放池
RunLoop銷毀時(shí)銷毀自動(dòng)釋放池

打印RunLoop信息可以看出:

_wrapRunLoopWithAutoreleasePoolHandler  activities = 0x1  1
_wrapRunLoopWithAutoreleasePoolHandler activities = 0xa0 160

160 是 kCFRunLoopBeforeWaiting + kCFRunLoopExit

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • RunLoop 是 iOS 開發(fā)中一個(gè)非?;A(chǔ)而又重要的一個(gè)概念 為什么說它非常重要呢? 它不僅是檢驗(yàn)一個(gè)程序員水...
    小小小阿博er閱讀 887評論 2 19
  • 什么是Runloop 運(yùn)行循環(huán) 跑圈 內(nèi)部類似一個(gè) do-while 循環(huán), 在循環(huán)內(nèi)部不斷處理各種任務(wù) (Sou...
    aLonelyRoot3閱讀 1,458評論 4 33
  • 概述 RunLoop作為iOS中一個(gè)基礎(chǔ)組件和線程有著千絲萬縷的關(guān)系,同時(shí)也是很多常見技術(shù)的幕后功臣。盡管在平時(shí)多...
    陽明AI閱讀 1,151評論 0 17
  • 1 Runloop機(jī)制原理 深入理解RunLoop http://www.cocoachina.com/ios/2...
    Kevin_Junbaozi閱讀 4,239評論 4 30
  • 說明iOS中的RunLoop使用場景1.保持線程的存活,而不是線性的執(zhí)行完任務(wù)就退出了<1>不開啟RunLoop的...
    野生塔塔醬閱讀 6,918評論 15 109

友情鏈接更多精彩內(nèi)容