iOS NSRunloop詳解

什么是Runloop

Runloop即運(yùn)行循環(huán)启妹。為什么你的APP放在那里不去動(dòng)它课兄,在某個(gè)時(shí)間點(diǎn)去操作它啄清,它還會(huì)給你反饋。就是因?yàn)镽unloop的存在吧碾。
總結(jié)一下凰盔,因?yàn)镽unloop的存在,保證你的程序不會(huì)死倦春。

主要負(fù)責(zé)什么户敬?
  1. 使程序一直運(yùn)行并接受用戶輸入
  2. 決定程序在何時(shí)處理一些Event
  3. 調(diào)用解耦(Message Queue)
  4. 節(jié)省CPU時(shí)間(沒(méi)事的時(shí)候閑著,有事的時(shí)候處理)
誰(shuí)依賴NSRunloop
  1. NSTimer
  2. UIEvent
  3. autorelease
  4. NSObject(NSDelaydPerforming)
  5. NSObject(NSThreadPerformAddtion)
  6. CADisplayLink
  7. CATransition
  8. CAAnimation
  9. dispatch_get_main_queue()
  10. AFNetworking(NSURLConnection)
  11. ...

主線程幾乎所有的函數(shù)都從以下的6個(gè)之1的調(diào)起

__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__

構(gòu)成元素

Snip20160907_437.png

因?yàn)镹SRunloop是對(duì)CFRunloop的封裝睁本,所以這里只看CFRunLoop就可以了尿庐。

CFRunLoopTimer的封裝

系統(tǒng)提供的NSTimer、CADisplayLink呢堰、performSelector等都是對(duì)CFRunLoopTimer的封裝抄瑟。

CFRunLoopSource

Source是RunLoop的數(shù)據(jù)源抽象類(用OC的話來(lái)講就是protocol)。
RunLoop定義了兩個(gè)版本的Source,分別是Source0和Source1枉疼。

  1. Source0:處理APP內(nèi)部事件皮假、APP自己負(fù)責(zé)管理(觸發(fā)),如UIEvent骂维、CFSocket
  2. Source1:由RunLoop和內(nèi)核管理惹资,Mach Port驅(qū)動(dòng),如CFMachPort航闺、CFMessagePort
CFRunLoopObserver

觀察者褪测,向外部報(bào)告RunLoop當(dāng)前狀態(tài)的更改

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即將進(jìn)入Loop
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即將處理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
    kCFRunLoopAfterWaiting  = (1UL << 6), // 剛從休眠中喚醒
    kCFRunLoopExit          = (1UL << 7), // 即將退出Loop
};

框架中很多機(jī)制都由CFRunLoopObserver觸發(fā),比如CAAnimation
舉例:

self.navigationController pushViewController:<#(nonnull UIViewController *)#> animated:<#(BOOL)#>

當(dāng)程序執(zhí)行完這行代碼時(shí)潦刃,我們可以看到經(jīng)歷push動(dòng)畫(huà)之后侮措,到達(dá)了一個(gè)新的界面。
但其實(shí)并不是執(zhí)行完這行代碼就出現(xiàn)了Push的動(dòng)畫(huà)福铅。
其實(shí)萝毛,執(zhí)行這段代碼時(shí)不會(huì)立刻就掉push動(dòng)畫(huà),而是要RunLoop循環(huán)一圈收集所有的Animation操作滑黔,匯集起來(lái)一起去調(diào)笆包。

CFRunLoopObserver與AutoreleasePool

對(duì)象的釋放并不是在{}括號(hào)結(jié)束环揽。而是稍微延遲了一點(diǎn)。
堆棧如下:

_wrapRunLoopAutoreleasePoolHandler
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__

UIKit通過(guò)RunLoopOberser在RunLoop兩次Sleep間對(duì)AutoreleasePool進(jìn)行Pop和Push庵佣,將這次Loop產(chǎn)生的Autorelease對(duì)象釋放歉胶。
也就是RunLoop跑一圈沒(méi)事了就睡,被喚醒了再跑下一圈巴粪,在兩次sleep之間對(duì)自動(dòng)釋放池進(jìn)行釋放通今。

CFRunLoopMode

注意

RunLoop在同一段時(shí)間只能且必須在一種特定Mode下Run。
更換Mode時(shí)肛根,需要停止當(dāng)前Loop辫塌,然后重啟新Mode。
Mode是iOS滑動(dòng)順暢的關(guān)鍵派哲。

類型
  1. NSDefaultRunLoopMode
    默認(rèn)狀態(tài)(空閑狀態(tài))臼氨,比如點(diǎn)擊按鈕都是這個(gè)狀態(tài)
  2. UITrackingRunLoopMode
    滑動(dòng)時(shí)的Mode。比如滑動(dòng)UIScrollView時(shí)芭届。
  3. UIInitializationRunLoopMode
    私有的储矩,APP啟動(dòng)時(shí)。就是從iphone桌面點(diǎn)擊APP的圖標(biāo)進(jìn)入APP到第一個(gè)界面展示之前褂乍,在第一個(gè)界面顯示出來(lái)后持隧,UIInitializationRunLoopMode就被切換成了NSDefaultRunLoopMode。
  4. NSRunLoopCommonModes
    它是NSDefaultRunLoopMode和UITrackingRunLoopMode的集合逃片。結(jié)構(gòu)類似于一個(gè)數(shù)組屡拨。在這個(gè)mode下執(zhí)行其實(shí)就是兩個(gè)mode都能執(zhí)行而已。
    典型的應(yīng)用場(chǎng)景這樣:當(dāng)前界面有開(kāi)啟一個(gè)NSTimer褥实,并且滑動(dòng)UIScrollView洁仗。正常開(kāi)啟NSTimer后,滑動(dòng)UIScrollView時(shí)它是不滑動(dòng)的性锭。解決辦法就是把這個(gè)timer加入到當(dāng)前的RunLoop,并把RunLoop的mode設(shè)置為NSRunLoopCommonModes叫胖。這樣就可以保證不管你是NSDefaultRunLoopMode里跑草冈,還是UITrackingRunLoopMode里跑,這個(gè)timer都可以執(zhí)行瓮增。
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.0625
                                                  target:self
                                                selector:@selector(progressChange)
                                                userInfo:nil
                                                 repeats:YES];
    
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];

當(dāng)你開(kāi)始滑動(dòng)UIScrollView時(shí)怎棱,RunLoop的mode狀態(tài)變化如下:

NSDefaultRunLoopMode -> UITrackingRunLoopMode -> NSDefaultRunLoopMode

開(kāi)始滑動(dòng)時(shí),第一次mode的切換會(huì)把NSDefaultRunLoopMode停掉绷跑。然后開(kāi)啟新的UITrackingRunLoopMode拳恋。當(dāng)滑動(dòng)停止時(shí),由UITrackingRunLoopMode切換回NSDefaultRunLoopMode砸捏,這時(shí)UITrackingRunLoopMode被停止谬运,又切換回了老的NSDefaultRunLoopMode(這個(gè)老的NSDefaultRunLoopMode應(yīng)該是重新開(kāi)始的)隙赁。

RunLoop和GCD的關(guān)系

RunLoop和GCD的關(guān)系,準(zhǔn)確來(lái)說(shuō)是只要使用了dispatch_get_main_queue()梆暖,就與RunLoop有了關(guān)系伞访。

因?yàn)镚CD中dispatch到main queue的block被分發(fā)到main RunLoop執(zhí)行。

RunLoop的掛起和喚醒

我寫(xiě)了個(gè)demo轰驳,運(yùn)行厚掷,然后點(diǎn)擊debug欄的暫停,查看堆棧级解,如下:

Snip20160907_438.png
  1. 指定用于喚醒的mach_port接口
  2. 調(diào)用mach_msg監(jiān)聽(tīng)喚醒端口冒黑,被喚醒前,系統(tǒng)內(nèi)核將這個(gè)線程掛起勤哗,停留在mach_msg_trap狀態(tài)
  3. 由另一個(gè)線程(或另一個(gè)進(jìn)程中的某個(gè)線程)向內(nèi)核發(fā)送這個(gè)端口的msg后抡爹,trap狀態(tài)被喚醒,RunLoop繼續(xù)開(kāi)始干活俺陋。

RunLoop迭代執(zhí)行順序

偽代碼:

SetupThisRunLoopRunTimeoutTimer();  //by GCD timer
do{
    __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
    __CFRunLoopDoObservers(kCFRunLoopBeforeSources);

    __CFRunLoopDoBlocks();
    __CFRunLoopDoSource0();

    CheckIfExistMessagesInMainDispatchQueue();    //GCD

    __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
    var wakeUpPort = SleepAndWaitForWakingUpPorts();
    //mach_msg_trap
    //Zzz...
    //Received mach_msg , wake up
    __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
    //Handle msgs
    if(wakeUpPort == timerPort){
        __CFRunLoopDoTimers();
    }else if(wakeUpPort == mainDispatchQueuePort) {
        //GCD
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_()
    }else {
          __CFRunLoopDoSource1();
    }
    __CFRunLoopDOBlocks();
}while(!stop && !timeout);

代碼解讀

//首先do..while循環(huán)不能是一個(gè)死循環(huán),所以在這里設(shè)置一個(gè)過(guò)期時(shí)間
//這件事是GCD干的豁延,用來(lái)檢測(cè)do..while循環(huán)跑了多久
SetupThisRunLoopRunTimeoutTimer();

//開(kāi)始跑循環(huán)
do{
    //告訴observer我要跑timer了
    __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
    //告訴observer我要跑source了
    __CFRunLoopDoObservers(kCFRunLoopBeforeSources);

    __CFRunLoopDoBlocks();
    //程序跑到這里會(huì)查詢Source0有什么消息
    __CFRunLoopDoSource0();

    //詢問(wèn)GCD你有沒(méi)有存在主線程的東西需要我?guī)湍阏{(diào)
    CheckIfExistMessagesInMainDispatchQueue();  //GCD

    //告訴observer我要睡了,RunLoop進(jìn)入到掛起狀態(tài)
    __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);

    //進(jìn)入trap狀態(tài)腊状,程序跑到這里就卡在這不動(dòng)了诱咏,等待被某個(gè)Port喚醒
    var wakeUpPort = SleepAndWaitForWakingUpPorts();

    //被喚醒后,告訴observer我被喚醒了
    __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);

    //假如是被timer喚醒的
    if(wakeUpPort == timerPort){
        //就去循環(huán)遍歷和timer有關(guān)的回調(diào)
        __CFRunLoopDoTimers();
    }else if(wakeUpPort == mainDispatchQueuePort) {
        //如果是主線程的GCD把我喚醒的缴挖,那RunLoop就知道GCD要讓它做事了袋狞,然后就取調(diào)GCD的這些事件
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_()
    }else {
          //如果都不是,就是Source1映屋,Source1是基于Port事件的苟鸯,比如網(wǎng)絡(luò)某個(gè)端口來(lái)數(shù)據(jù)了,就會(huì)把RunLoop喚醒棚点,去對(duì)來(lái)的數(shù)據(jù)進(jìn)行處理
          __CFRunLoopDoSource1();
    }
    __CFRunLoopDoBlocks();
}while(!stop && !timeout);
//判斷條件:有沒(méi)有被外部干掉 && 到了過(guò)期時(shí)間
//如果過(guò)期時(shí)間不手動(dòng)進(jìn)行設(shè)置的話早处,默認(rèn)值是一個(gè)很大的值,可能是Int_Max

AFNetworking是如何玩轉(zhuǎn)RunLoop的

+ (void)networkRequestThreadEntryPoint:(id)_unuserd object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        //為了不讓runloop run起來(lái)沒(méi)事干導(dǎo)致消失
        //所以給runloop加了一個(gè)NSMachPort瘫析,給它一個(gè)mode去監(jiān)聽(tīng)
        //實(shí)際上port什么也沒(méi)干砌梆,就是讓runloop一直在等,目的就是讓runloop一直活著
        //這是一個(gè)創(chuàng)建常駐服務(wù)線程的好方法
        NSRunloop *runloop = [NSRunLoop currentRunLoop];
        [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runloop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkReuqestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = 
        [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

一個(gè)TableView延遲加載圖片的新思路

以前是怎么解決的贬循?
通過(guò)UITableView的代理方法咸包,判斷如果處于滑動(dòng)狀態(tài)就不去設(shè)置cell上的圖片,如果沒(méi)有處于滑動(dòng)狀態(tài)就取設(shè)置cell上的圖片杖虾。

而現(xiàn)在通過(guò)Runloop就有一個(gè)十分簡(jiǎn)便的方法烂瘫。

//在cell里面把設(shè)置圖片的事情在NSDefaultRunloopMode里面去做。
//當(dāng)主線程的tableview不再滑動(dòng)的時(shí)候就會(huì)去設(shè)置圖片
UIImage *dowloadImage = ...;
[self.iconImageView performSelector:@selector(setImage:) withObject:dowloadImage afterDelay:0 inModes:@[NSDefaultRunloopMode]];

這樣去設(shè)置圖片就簡(jiǎn)便了很多奇适,不用再去判斷tableview的代理方法坟比。代碼也會(huì)很清爽芦鳍。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市温算,隨后出現(xiàn)的幾起案子怜校,更是在濱河造成了極大的恐慌,老刑警劉巖注竿,帶你破解...
    沈念sama閱讀 216,544評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件茄茁,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡巩割,警方通過(guò)查閱死者的電腦和手機(jī)裙顽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)宣谈,“玉大人愈犹,你說(shuō)我怎么就攤上這事∥懦螅” “怎么了漩怎?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,764評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)嗦嗡。 經(jīng)常有香客問(wèn)我勋锤,道長(zhǎng),這世上最難降的妖魔是什么侥祭? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,193評(píng)論 1 292
  • 正文 為了忘掉前任叁执,我火速辦了婚禮,結(jié)果婚禮上矮冬,老公的妹妹穿的比我還像新娘谈宛。我一直安慰自己,他們只是感情好胎署,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,216評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布吆录。 她就那樣靜靜地躺著,像睡著了一般琼牧。 火紅的嫁衣襯著肌膚如雪径筏。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,182評(píng)論 1 299
  • 那天障陶,我揣著相機(jī)與錄音,去河邊找鬼聊训。 笑死抱究,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的带斑。 我是一名探鬼主播鼓寺,決...
    沈念sama閱讀 40,063評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼勋拟,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了妈候?” 一聲冷哼從身側(cè)響起敢靡,我...
    開(kāi)封第一講書(shū)人閱讀 38,917評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎苦银,沒(méi)想到半個(gè)月后啸胧,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡幔虏,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,543評(píng)論 2 332
  • 正文 我和宋清朗相戀三年纺念,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片想括。...
    茶點(diǎn)故事閱讀 39,722評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡陷谱,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出瑟蜈,到底是詐尸還是另有隱情烟逊,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評(píng)論 5 343
  • 正文 年R本政府宣布铺根,位于F島的核電站宪躯,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏夷都。R本人自食惡果不足惜眷唉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,019評(píng)論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望囤官。 院中可真熱鬧冬阳,春花似錦、人聲如沸党饮。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,671評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)刑顺。三九已至氯窍,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間蹲堂,已是汗流浹背狼讨。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,825評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留柒竞,地道東北人政供。 一個(gè)月前我還...
    沈念sama閱讀 47,729評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親布隔。 傳聞我的和親對(duì)象是個(gè)殘疾皇子离陶,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,614評(píng)論 2 353

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

  • Runloop是iOS和OSX開(kāi)發(fā)中非常基礎(chǔ)的一個(gè)概念衅檀,從概念開(kāi)始學(xué)習(xí)招刨。 RunLoop的概念 -般說(shuō),一個(gè)線程一...
    小貓仔閱讀 993評(píng)論 0 1
  • 前言 最近離職了,可以盡情熬夜寫(xiě)點(diǎn)總結(jié)哀军,不用擔(dān)心第二天上班爽并蛋疼著沉眶,這篇的主角 RunLoop 一座大山,涵蓋的...
    zerocc2014閱讀 12,374評(píng)論 13 67
  • 轉(zhuǎn)自http://blog.ibireme.com/2015/05/18/runloop 深入理解RunLoop ...
    飄金閱讀 983評(píng)論 0 4
  • 原文地址:http://blog.ibireme.com/2015/05/18/runloop/ RunLoop ...
    大餅炒雞蛋閱讀 1,155評(píng)論 0 6
  • 她传藏,下了出租車,吐得一塌糊涂彤守。 夜里11點(diǎn)半還多毯侦,空蕩蕩的路上已經(jīng)沒(méi)有什么人。蟲(chóng)鳴聲具垫,吱吱的叫著侈离,夏夜,除了偶爾的...
    鈅27鈤劉炫閱讀 188評(píng)論 2 0