iOS-RunLoop初識

前言

iOS開發(fā)中,RunLoop就是個神秘的領(lǐng)域洒试,很多2~3年的開發(fā)者都不能準確的描述它的具體含義倍奢,甚至可能從來都沒有接觸過該方面的技術(shù),或者項目中隱約用到過(定時器)垒棋,但是不知道是其的作用娱挨,問題一解決就不在深究了,不過也有情可原捕犬,畢竟大部分開發(fā)者還處于基本功能的構(gòu)建,很少涉及性能的優(yōu)化酵镜。

RunLoop之所以神秘碉碉,個人認為,系統(tǒng)能夠利用RunLoop 實現(xiàn)自動釋放池淮韭、延遲回調(diào)垢粮、觸摸事件、屏幕刷新等功能靠粪。實現(xiàn)這種技術(shù)的關(guān)鍵點在于:如何管理事件/消息蜡吧,如何讓線程在沒有處理消息時休眠以避免資源占用毫蚓、在有消息到來時立刻被喚醒。

RunLoop的基本概念

簡介
RunLoop從字面上來看:運行循環(huán)昔善,或者是跑圈元潘。
#基本作用:
1、保持程序的持續(xù)運行(比如主運行循環(huán))
2君仆、處理App中的各種事件(比如觸摸事件翩概、定時器事件、Selector事件)
3返咱、節(jié)省CPU資源钥庇,提高性能。
通常來講咖摹,一個線程一次只能執(zhí)行一個任務(wù)评姨,當線程中的任務(wù)執(zhí)行完成后,線程就會自動退出萤晴。如果我們需要一個長運行機制吐句,讓線程能夠隨時處理事件并且保證不退出,通常我們的做法硫眯,邏輯是這樣的:

function  loop {
     initialize();
     BOOL running = YES;
    do{
           // 執(zhí)行各種任務(wù)蕴侧,處理各種事件
      }while (running);
     return 0;
 }

上述函數(shù)中,通過一個循環(huán)來執(zhí)行任務(wù)两入,根據(jù)事件的回調(diào)和觀察者的屬性來改更改running的值净宵,有效的使得函數(shù)中的任務(wù)長久的運行。簡單的說裹纳,這就是程序的運行模式择葡。

眾所周知,iOS應(yīng)用啟動后剃氧,不會正常的自動退出敏储。這就是因為iOS系統(tǒng)擁有的RunLoop機制的作用,當應(yīng)用啟動后朋鞍,主線程的RunLoop默認開啟已添,從而應(yīng)用進入一個死循環(huán),阻止了程序在運行完畢之后退出滥酥。

int main(int argc, char * argv[]) {
@autoreleasepool {
    //這個函數(shù)永遠不會有返回值
    return UIApplicationMain(argc, argv, nil, 
          NSStringFromClass([AppDelegate class]));
      }  
}

1.在UIApplicationMain函數(shù)內(nèi)部就啟動了一個RunLoop
2.UIApplicationMain函數(shù)一直沒有返回更舞,保持了程序的持續(xù)運行
3.這個默認啟動的RunLoop是主線程關(guān)聯(lián)的
4.一個線程對應(yīng)一個RunLoop,主線程的RunLoop默認已經(jīng)啟動
5.子線程的RunLoop得手動啟動(調(diào)用運行方法)
6.RunLoop只能選擇一個模式啟動坎吻,如果當前模式中沒有任Source缆蝉,Timer,Observer,那么就直接退出RunLoop

RunLoop在循環(huán)過程中監(jiān)聽著port事件和timer事件刊头,當前線程有任務(wù)時黍瞧,喚醒當當線程去執(zhí)行任務(wù),任務(wù)執(zhí)行完成以后原杂,使當前線程進入休眠狀態(tài)印颤。

RunLoop與線程

RunLoop,正如其名污尉,loop表示某種循環(huán)膀哲,和run放在一起就表示一直在運行著的循環(huán)。實際上被碗,RunLoop和線程是緊密相連的某宪,可以這樣說run loop是為了線程而生,沒有線程锐朴,它就沒有存在的必要兴喂。RunLoop是線程的基礎(chǔ)架構(gòu)部分,Cocoa和CoreFundation框架都提供了RunLoop的相關(guān)接口焚志,方便配置和管理線程的RunLoop衣迷。每個線程,包括程序的主線程(main thread)都有與之相應(yīng)的run loop對象酱酬,主線程的RunLoop對象是默認開啟的壶谒,子線程的run loop對象需要程序員手動開啟。

源碼中可以看出膳沽,線程和 RunLoop 之間是一一對應(yīng)的汗菜,其關(guān)系是保存在一個全局的 Dictionary 里。線程剛創(chuàng)建時并沒有 RunLoop挑社,如果你不主動獲取陨界,那它一直都不會有。RunLoop 的創(chuàng)建是發(fā)生在第一次獲取時痛阻,RunLoop 的銷毀是發(fā)生在線程結(jié)束時菌瘪。你只能在一個線程的內(nèi)部獲取其 RunLoop(主線程除外)。

RunLoop與autorelease pool

從程序啟動到加載完成是一個完整的運行循環(huán)阱当,然后會停下來俏扩,等待用戶交互,用戶的每一次交互都會啟動一次運行循環(huán)弊添,來處理用戶所有的點擊事件动猬,觸摸事件。我們都知道: 所有autorelease的對象表箭,在出了作用域之后,會被自動添加到最近創(chuàng)建的自動釋放池中。但是如果每次都放進應(yīng)用程序的main.m中的autoreleasepool中免钻,遲早有被撐滿的一刻彼水。這個過程中必須有一個釋放的動作。在一次完整的運行循環(huán)結(jié)束之前慨蛙,會被銷毀撞反。

Autorelease對象出了作用域之后惹挟,會被添加到最近一次創(chuàng)建的自動釋放池中,并會在當前的runloop迭代結(jié)束時釋放盯桦。

在使用手動的內(nèi)存管理方式的項目中,會經(jīng)常用到很多自動釋放的對象渤刃,如果這些對象不能夠被即時釋放掉拥峦,會造成內(nèi)存占用量急劇增大。Run loop就為我們做了這樣的工作卖子,每當一個運行循環(huán)結(jié)束的時候略号,它都會釋放一次autorelease pool,同時pool中的所有自動釋放類型變量都會被釋放掉洋闽。

RunLoop常用的類

首先要知道iOS里面有兩套API可以訪問和使用RunLoop:
1玄柠、Foundation --->  NSRunLoop
2、Core Foundation ---> CFRunLoopRef

CoreFoundation 里面關(guān)于 RunLoop 有5個類:
1诫舅、CFRunLoopRef
2羽利、CFRunLoopModeRef
3、CFRunLoopSourceRef
4刊懈、CFRunLoopTimerRef
5这弧、CFRunLoopObserverRef

上面兩套都可以使用,但是要知道CFRunLoopRef是用c語言寫的俏讹,是開源的当宴,相比于NSRunLoop更加底層,而NSRunLoop其實是對CFRunLoopRef的一個簡單的封裝泽疆。便于使用而已户矢。這樣說來,顯然CFRunLoopRef的性能要高一點殉疼。

另外梯浪,我們不能在一個線程中去操作另外一個線程的run loop對象,那很可能會造成意想不到的后果瓢娜。不過幸運的是CoreFundation中的不透明類CFRunLoopRef是線程安全的挂洛,而且兩種類型的run loop完全可以混合使用。Cocoa中的NSRunLoop類可以通過實例方法:

  • (CFRunLoopRef)getCFRunLoop;
    獲取對應(yīng)的CFRunLoopRef類眠砾,來達到線程安全的目的虏劲。

CFRunLoopSourceRef

Run loop接收輸入事件來自兩種不同的來源:輸入源(input source)和定時源(timer source)。

事件源(input source)

按照官方文檔的分類

Port-Based Sources (基于端口,跟其他線程交互,通過內(nèi)核發(fā)布的消息)
Custom Input Sources (自定義)
Cocoa Perform Selector Sources (performSelector…方法)

按照函數(shù)調(diào)用棧的分類

Source0:非基于Port的
Source1:基于Port的

? Source0 只包含了一個回調(diào)(函數(shù)指針),它并不能主動觸發(fā)事件柒巫。使用時励堡,你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個 Source 標記為待處理堡掏,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop应结,讓其處理這個事件。
? Source1 包含了一個 mach_port 和一個回調(diào)(函數(shù)指針)泉唁,被用于通過內(nèi)核和其他線程相互發(fā)送消息鹅龄。這種 Source 能主動喚醒 RunLoop 的線程。

定時源(timer source)

定時源在預(yù)設(shè)的時間點同步方式傳遞消息亭畜,這些消息都會發(fā)生在特定時間或者重復(fù)的時間間隔扮休。定時源則直接傳遞消息給處理例程,不會立即退出run loop贱案。

需要注意的是肛炮,盡管定時器可以產(chǎn)生基于時間的通知,但它并不是實時機制宝踪。和輸入源一樣侨糟,定時器也和你的run loop的特定模式相關(guān)。如果定時器所在的模式當前未被run loop監(jiān)視瘩燥,那么定時器將不會開始直到run loop運行在相應(yīng)的模式下秕重。類似的,如果定時器在run loop處理某一事件期間開始厉膀,定時器會一直等待直到下次run loop開始相應(yīng)的處理程序溶耘。如果run loop不再運行,那定時器也將永遠不啟動服鹅。

創(chuàng)建定時器源有兩種方法凳兵,
方法一:

NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:4.0
                                          target:self                                                                           
                                       selector:@selector(backgroundThreadFire:) 
                                       userInfo:nil
                                        repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timerforMode:NSDefaultRunLoopMode];

方法二:

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

CFRunLoopTimerRef

CFRunLoopTimerRef 是基于時間的觸發(fā)器,它和 NSTimer 是toll-free bridged 的企软,可以混用庐扫。其包含一個時間長度和一個回調(diào)(函數(shù)指針)。當其加入到 RunLoop 時仗哨,RunLoop會注冊對應(yīng)的時間點形庭,當時間點到時,RunLoop會被喚醒以執(zhí)行那個回調(diào)厌漂。

  • NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影響
  • GCD的定時器不受RunLoop的Mode影響

開發(fā)中萨醒,我們需要一個定時操作,來循環(huán)的執(zhí)行任務(wù)苇倡,如果這個任務(wù)是不耗時的富纸,我們可以選擇NSTimer和GCD囤踩,如果任務(wù)是耗時的,我們通常會在子線程創(chuàng)建一個定時器晓褪,來執(zhí)行耗時任務(wù)高职,當選擇NSTimer時計時,定時器里的任務(wù)不會執(zhí)行辞州,原因是當前線程對相應(yīng)的RunLoop并沒有開啟,創(chuàng)建線程的函數(shù)執(zhí)行完成之后寥粹,這個線程就被銷毀了(可以用繼承CustomThread : NSThread变过,重寫dealloc方法來驗證),此時涝涤,你可能想用一個全局的對象來保存線程媚狰,任然不能解決問題,全局的對象描述指向線程的那個引用地址阔拳,這和線程的實體沒有關(guān)系崭孤,并不能代替一個實在的線程。
解決方案:開啟當前線程的RunLoop糊肠,當不需要任務(wù)的時候關(guān)閉

 //創(chuàng)建一個線程
 CustomThread *thread = [[CustomThread alloc] initWithBlock:^{
        NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timerAction:) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer  forMode:NSRunLoopCommonModes];
        [[NSRunLoop currentRunLoop] run];
    while (!_isfinished) {
        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.0001]];
    }
    NSLog(@"線程開始了");
}];
//開啟線程
[thread start];
- (void)timerAction:(id) objc
 {
      while (_isfinished) {
      [NSThread exit];//不需要時辨宠,關(guān)閉當前線程
  }
  NSLog(@"定時器開啟了");
}

GCD執(zhí)行,不存在上述問題货裹,因為嗤形,系統(tǒng)內(nèi)部默認給我們開啟當前線程的RunLoop,性能更加穩(wěn)定弧圆,還不存在內(nèi)存泄露赋兵。

/*timer需要全局變量*/
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
_currentTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
dispatch_source_set_timer(_currentTimer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
dispatch_source_set_event_handler(_currentTimer, ^{
     NSLog(@"-----開始前線程:%@----", [NSThread currentThread]);
    dispatch_async(dispatch_get_main_queue(), ^{
        // 在主線程中實現(xiàn)需要的功能
      NSLog(@"-----開始后線程:%@----", [NSThread currentThread]);
    });
});
dispatch_resume(_currentTimer);
/*
 // 掛起定時器(dispatch_suspend 之后的 Timer,是不能被釋放的搔预!會引起崩潰)
 dispatch_suspend(_timer);
 
 // 關(guān)閉定時器
 dispatch_source_cancel(_timer);
 */

選擇 GCD 還是 NSTimer霹期? 參考文章

CFRunLoopObserverRef

CFRunLoopObserverRef 是觀察者,每個 Observer 都包含了一個回調(diào)(函數(shù)指針)拯田,當 RunLoop 的狀態(tài)發(fā)生變化時历造,觀察者就能通過回調(diào)接受到這個變化。

應(yīng)用
//添加runloop監(jiān)聽者
- (void)addRunloopObserver{

//獲取 當前的Runloop ref - 指針
CFRunLoopRef currentRunLoop =  CFRunLoopGetCurrent();
//定義一個RunloopObserver
CFRunLoopObserverRef defaultModeObserver;
//上下文
/*
typedef struct {
    CFIndex version; //版本號 long
    void *  info;//萬能指針勿锅,這里我們要填寫對象(self或者傳進來的對象)
    const void *(*retain)(const void *info);        //填寫&CFRetain
    void    (*release)(const void *info);           //填寫&CGFRelease
    CFStringRef (*copyDescription)(const void *info); //NULL
 } CFRunLoopObserverContext;
 */
CFRunLoopObserverContext context = {
    0,
    (__bridge void *)(self),
    &CFRetain,
    &CFRelease,
    NULL
};
/*
 1 NULL空指針 nil空對象 這里填寫NULL
 2 模式
   typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { 
   kCFRunLoopEntry = (1UL << 0), //即將進入Runloop
   kCFRunLoopBeforeTimers = (1UL << 1), //即將處理NSTimer 
   kCFRunLoopBeforeSources = (1UL << 2), //即將處理Sources 
   kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠 
   kCFRunLoopAfterWaiting = (1UL << 6), //剛從休眠中喚醒 
   kCFRunLoopExit = (1UL << 7), //即將退出runloop 
   kCFRunLoopAllActivities = 0x0FFFFFFFU //所有狀態(tài)改變};
 3 是否重復(fù) - YES
 4 nil 或者 NSIntegerMax - 999
 5 回調(diào)
 6 上下文
 */
//創(chuàng)建觀察者
defaultModeObserver = CFRunLoopObserverCreate(NULL,
                                              kCFRunLoopBeforeWaiting, YES,
                                              NSIntegerMax - 999,
                                              &Callback,
                                              &context);
//添加當前runloop的觀察著
if(defaultModeObserver){
    CFRunLoopAddObserver(currentRunLoop, defaultModeObserver, kCFRunLoopDefaultMode);
    //釋放
    CFRelease(defaultModeObserver);
   }
}
//這里處理耗時操作了
static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
   //通過info橋接為需要的對象
}

CFRunLoopModeRef

系統(tǒng)默認注冊了5個Mode:(前兩個跟最后一個常用)
kCFRunLoopDefaultMode:App的默認Mode帕膜,通常主線程是在這個
Mode下運行
UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸
摸滑動溢十,保證界面滑動時不受其他 Mode 影響
UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 
Mode垮刹,啟動完成后就不再使用
GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到
kCFRunLoopCommonModes: 這是一個占位用的Mode张弛,不是一種真正的Mode

其中 CFRunLoopModeRef 類并沒有對外暴露荒典,只是通過 CFRunLoopRef 的接口進行了封裝酪劫。他們的關(guān)系如下:


RunLoop_0.png

一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer寺董。每次調(diào)用 RunLoop 的主函數(shù)時覆糟,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode遮咖。如果需要切換 Mode滩字,只能退出 Loop,再重新指定一個 Mode 進入御吞。這樣做主要是為了分隔開不同組的 Source/Timer/Observer麦箍,讓其互不影響。

RunLoop的事件隊列

RunLoop_1.png

從這個事件隊列中可以看出:

①如果是事件到達陶珠,消息會被傳遞給相應(yīng)的處理程序來處理挟裂, runloop處理完當次事件后,run loop會退出揍诽,而不管之前預(yù)定的時間到了沒有诀蓉。你可以重新啟動run loop來等待下一事件。

②如果線程中有需要處理的源暑脆,但是響應(yīng)的事件沒有到來的時候渠啤,線程就會休眠等待相應(yīng)事件的發(fā)生。這就是為什么run loop可以做到讓線程有工作的時候忙于工作饵筑,而沒工作的時候處于休眠狀態(tài)埃篓。

RunLoop 的實際應(yīng)用舉例

AF2.x,創(chuàng)建了一條常駐線程專門處理所有請求的回調(diào)事件根资。

 + (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 啟動前內(nèi)部必須要有至少一個 Timer/Observer/Source架专,所以 AFNetworking 在 [runLoop run] 之前先創(chuàng)建了一個新的 NSMachPort 添加進去了。

參考文章

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末玄帕,一起剝皮案震驚了整個濱河市部脚,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌裤纹,老刑警劉巖委刘,帶你破解...
    沈念sama閱讀 222,378評論 6 516
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鹰椒,居然都是意外死亡锡移,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,970評論 3 399
  • 文/潘曉璐 我一進店門漆际,熙熙樓的掌柜王于貴愁眉苦臉地迎上來淆珊,“玉大人,你說我怎么就攤上這事奸汇∈┓” “怎么了往声?”我有些...
    開封第一講書人閱讀 168,983評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長戳吝。 經(jīng)常有香客問我浩销,道長,這世上最難降的妖魔是什么听哭? 我笑而不...
    開封第一講書人閱讀 59,938評論 1 299
  • 正文 為了忘掉前任慢洋,我火速辦了婚禮,結(jié)果婚禮上陆盘,老公的妹妹穿的比我還像新娘且警。我一直安慰自己,他們只是感情好礁遣,可當我...
    茶點故事閱讀 68,955評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著肩刃,像睡著了一般祟霍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上盈包,一...
    開封第一講書人閱讀 52,549評論 1 312
  • 那天沸呐,我揣著相機與錄音,去河邊找鬼呢燥。 笑死崭添,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的叛氨。 我是一名探鬼主播呼渣,決...
    沈念sama閱讀 41,063評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼寞埠!你這毒婦竟也來了屁置?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,991評論 0 277
  • 序言:老撾萬榮一對情侶失蹤仁连,失蹤者是張志新(化名)和其女友劉穎蓝角,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體饭冬,經(jīng)...
    沈念sama閱讀 46,522評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡使鹅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,604評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了昌抠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片患朱。...
    茶點故事閱讀 40,742評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖扰魂,靈堂內(nèi)的尸體忽然破棺而出麦乞,到底是詐尸還是另有隱情蕴茴,我是刑警寧澤,帶...
    沈念sama閱讀 36,413評論 5 351
  • 正文 年R本政府宣布姐直,位于F島的核電站倦淀,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏声畏。R本人自食惡果不足惜撞叽,卻給世界環(huán)境...
    茶點故事閱讀 42,094評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望插龄。 院中可真熱鬧愿棋,春花似錦、人聲如沸均牢。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,572評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽徘跪。三九已至甘邀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間垮庐,已是汗流浹背松邪。 一陣腳步聲響...
    開封第一講書人閱讀 33,671評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留哨查,地道東北人逗抑。 一個月前我還...
    沈念sama閱讀 49,159評論 3 378
  • 正文 我出身青樓,卻偏偏與公主長得像寒亥,于是被迫代替她去往敵國和親邮府。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,747評論 2 361

推薦閱讀更多精彩內(nèi)容