iOS RunLoop詳解

一.RunLoop介紹

  • 1.概念

RunLoop是一個運(yùn)行循環(huán),正是因?yàn)镽unLoop,IOS才可以保持程序的持續(xù)運(yùn)行匿醒,處理App中的各種事件,并且可以節(jié)省CPU資源缠导,提高性能(因?yàn)镽unLoop可以做到工作休息兩不誤)廉羔。因?yàn)橐话銇碇v,一個線程在處理完一個任務(wù)以后就會退出僻造。

線程與RunLoop

  • 每條線程都有唯一的一個與之對應(yīng)的RunLoop對象
  • 主線程的RunLoop已經(jīng)自動創(chuàng)建好了憋他,子線程的RunLoop需要通過系統(tǒng)提供的方法進(jìn)行獲取
  • 獲取RunLoop以后,如果沒有事件源和Timer事件或者沒有設(shè)置RunLoop運(yùn)行模式髓削,RunLoop會在獲取以后立即銷毀举瑰;如果超時,RunLoop也會被銷毀蔬螟。
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}
UIApplicationMain內(nèi)部就開啟了一個RunLoop此迅,這個函數(shù)是沒有返回值的,因?yàn)镽unLoop是一個運(yùn)行循環(huán)。如果此處改為:
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return 0;
    }
}
App在啟動以后耸序,就會結(jié)束運(yùn)行忍些。
  • 2.如何訪問RunLoop對象

  • Foundation
    NSRunLoop
    [NSRunLoop currentRunLoop];//獲取當(dāng)前線程RunLoop,如果當(dāng)前線程RunLoop未創(chuàng)建坎怪,則創(chuàng)建罢坝。
    [NSRunLoop mainRunLoop];//獲取主線程RunLoop
  • Core Foundation
    CFRunLoopRef
    CFRunLoopGetCurrent();//獲取當(dāng)前線程RunLoop,如果當(dāng)前線程RunLoop未創(chuàng)建搅窿,則創(chuàng)建嘁酿。
    CFRunLoopGetMain();//獲取主線程RunLoop

NSRunLoop是CFRunLoopRef的OC封裝

  • 3.RunLoop相關(guān)類介紹

  • CFRunLoopRef【RunLoop對象】
  • CFRunLoopModeRef【RunLoop的運(yùn)行模式】
  • CFRunLoopSourceRef【RunLoop要處理的事件源】
  • CFRunLoopTimerRef【Timer事件】
  • CFRunLoopObserverRef【RunLoop觀察者】
RunLoop.jpg

一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer男应。每次調(diào)用 RunLoop 的主函數(shù)時闹司,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode沐飘。如果需要切換 Mode游桩,只能退出 Loop,再重新指定一個 Mode 進(jìn)入耐朴。這樣做主要是為了分隔開不同組的 Source/Timer/Observer借卧,讓其互不影響。

  • CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的運(yùn)行模式(下面列舉5種)

NSDefaultRunLoopMode(Cocoa)/kCFRunLoopDefaultMode(Core Foundation):App的默認(rèn)Mode筛峭,通常主線程是在這個Mode下運(yùn)行
UITrackingRunLoopMode:界面跟蹤 Mode铐刘,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
UIInitializationRunLoopMode: 在剛啟動 App 時第進(jìn)入的第一個 Mode影晓,啟動完成后就不再使用
GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode镰吵,通常用不到
NSRunLoopCommonModes(Cocoa)/kCFRunLoopCommonModes(Core Foundation): 這是一個占位用的Mode,不是一種真正的Mode
  • 一個 RunLoop 包含若干個 Mode俯艰,每個Mode又包含若干個Source/Timer/Observer
  • 每次RunLoop啟動時捡遍,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode
  • 如果需要切換Mode竹握,只能退出Loop画株,再重新指定一個Mode進(jìn)入
    這樣做主要是為了分隔開不同組的Source/Timer/Observer,讓其互不影響
  • kCFRunLoopCommonModes是一種模式組合啦辐,IOS系統(tǒng)中默認(rèn)包含了kCFRunLoopDefaultMode和UITrackingRunLoopMode,系統(tǒng)會分別注冊這兩種模式谓传,還可以通過CFRunLoopAddCommonMode()將自定義Mode放到kCFRunLoopCommonModes中。
  • CFRunLoopSourceRef
  • 按照官方文檔的分類
    Port-Based Sources (基于端口,跟其他線程交互,通過內(nèi)核發(fā)布的消息)
    Custom Input Sources (自定義)
    Cocoa Perform Selector Sources (performSelector…方法)
  • 按照函數(shù)調(diào)用棧的分類
    Source0:非基于Port的
    Source1:基于Port的

Source0: event事件芹关,只含有回調(diào)续挟,需要先調(diào)用CFRunLoopSourceSignal(source),將這個 Source 標(biāo)記為待處理侥衬,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop诗祸。
Source1: 包含了一個 mach_port 和一個回調(diào)跑芳,被用于通過內(nèi)核和其他線程相互發(fā)送消息,能主動喚醒 RunLoop 的線程。

  • CFRunLoopTimerRef

CFRunLoopTimerRef是基于時間的觸發(fā)器
基本上說的就是NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影響
GCD的定時器不受RunLoop的Mode影響

  • CFRunLoopObserverRef

CFRunLoopObserverRef是觀察者直颅,能夠監(jiān)聽RunLoop的狀態(tài)改變

可以監(jiān)聽的RunLoop狀態(tài)
/* Run Loop Observer Activities */
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
    kCFRunLoopAllActivities = 0x0FFFFFFFU//以上所有狀態(tài)
};

監(jiān)聽RunLoop狀態(tài)示例:

    //新建子線程
- (void)viewDidLoad {
    [super viewDidLoad];
    //新建子線程
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadTask) object:nil];
    [self.thread start];
}

- (void)subThreadTask {
    //創(chuàng)建觀察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        switch (activity) {
            case kCFRunLoopEntry:
            {
                NSLog(@"即將進(jìn)入RunLoop");
            }
                break;
            case kCFRunLoopBeforeTimers:
            {
                NSLog(@"即將處理Timer");
            }
                break;
            case kCFRunLoopBeforeSources:
            {
                NSLog(@"即將處理Source");
            }
                break;
            case kCFRunLoopBeforeWaiting:
            {
                NSLog(@"即將進(jìn)入休眠");
            }
                break;
            case kCFRunLoopAfterWaiting:
            {
                NSLog(@"即將從休眠中喚醒");
            }
                break;
            case kCFRunLoopExit:
            {
                NSLog(@"即將退出RunLoop");
            }
                break;
            default:
                break;
        }
    });
    //添加觀察者
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
    
    //釋放資源
    CFRelease(observer);
    
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    self.timer = [NSTimer timerWithTimeInterval:2.0f target:self selector:@selector(timerActon) userInfo:nil repeats:YES];
    [runLoop addTimer:self.timer forMode:NSDefaultRunLoopMode];
    [runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:4.0f]];
}

- (void)timerActon {
    NSLog(@"定時器工作了");
}

運(yùn)行程序控制臺輸出:
2018-05-24 09:32:04.495445+0800 Test[1059:57057] 即將進(jìn)入RunLoop
2018-05-24 09:32:04.499081+0800 Test[1059:57057] 即將處理Timer
2018-05-24 09:32:04.502540+0800 Test[1059:57057] 即將處理Source
2018-05-24 09:32:04.503239+0800 Test[1059:57057] 即將進(jìn)入休眠
2018-05-24 09:32:06.497401+0800 Test[1059:57057] 即將從休眠中喚醒
2018-05-24 09:32:06.497973+0800 Test[1059:57057] 定時器工作了
2018-05-24 09:32:06.498469+0800 Test[1059:57057] 即將處理Timer
2018-05-24 09:32:06.498736+0800 Test[1059:57057] 即將處理Source
2018-05-24 09:32:06.499872+0800 Test[1059:57057] 即將進(jìn)入休眠
2018-05-24 09:32:08.500044+0800 Test[1059:57057] 即將從休眠中喚醒
2018-05-24 09:32:08.500392+0800 Test[1059:57057] 定時器工作了
2018-05-24 09:32:08.500790+0800 Test[1059:57057] 即將退出RunLoop
注:因?yàn)槎〞r器是每兩秒鐘調(diào)用一次博个,子線程的runLoop在4秒鐘以后會銷毀,所以定時器會輸出兩次功偿。4秒鐘以后盆佣,runLoop會銷毀,在銷毀以前觀察者會收到"即將推出RunLoop"的狀態(tài)通知械荷。

二.RunLoop流程

  • 1.官方文檔
    RunLoop官方流程圖

    來自Apple官方文檔翻譯
  • 2.網(wǎng)友對官方文檔的整理
    RunLoop流程

三.RunLoop應(yīng)用

  • 1.NSTimer
    創(chuàng)建一個tableView和一個定時器浸踩,定時器用于顯示當(dāng)前時間朗徊。當(dāng)tableView靜止的時候介袜,定時器正常工作蛮粮。當(dāng)拖拽tableView的時候,定時器停止了工作关拒。
    定時器的創(chuàng)建代碼:
    //創(chuàng)建定時器 并指定RunLoop運(yùn)行模式為NSDefaultRunLoopMode
    self.timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(refreshContentLabel) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
NSDefaultRunLoopMode下的定時器.gif

如果需要在列表滑動時定時器繼續(xù)工作佃蚜,則需要指定RunLoop運(yùn)行模式為NSRunLoopCommonModes

    //創(chuàng)建定時器 并指定RunLoop運(yùn)行模式為NSRunLoopCommonModes
    self.timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(refreshContentLabel) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
NSRunLoopCommonModes下的定時器.gif
  • 2.PerformSelector
    PerformSelector 可以指定在何種RunLoopMode下進(jìn)行工作
    //testAction僅在當(dāng)前RunLoop處于UITrackingRunLoopMode下工作,即ScrollView滾動的時候
    [self performSelector:@selector(testAction) withObject:nil afterDelay:2.0f inModes:@[UITrackingRunLoopMode]];
  • 3.常駐線程
    有時候我們需要讓一個線程庇褂椋活着绊,即一直處于可以執(zhí)行任務(wù)的狀態(tài)。這時候我們就需要用到RunLoop熟尉。
    一般情況归露,一個線程在任務(wù)執(zhí)行完畢以后,是不可以去執(zhí)行其他工作的:
- (IBAction)openSubThreadAndexecutingMethodA:(id)sender {
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(methodA) object:nil];
    [self.thread start];
}

- (IBAction)executingMethodB:(id)sender {
    [self performSelector:@selector(methodAB) onThread:self.thread withObject:nil waitUntilDone:YES];
}

- (void)methodA {
    NSLog(@"方法A被執(zhí)行");
}

- (void)methodAB {
    NSLog(@"方法B被執(zhí)行");
}
點(diǎn)擊按鈕執(zhí)行上述代碼中的方法.png

當(dāng)我們點(diǎn)擊"開啟子線程并執(zhí)行MethodA"進(jìn)行子線程創(chuàng)建斤儿,并且讓MethodA在子線程中執(zhí)行剧包。當(dāng)MethodA執(zhí)行完畢以后,我們?nèi)绻c(diǎn)擊
“讓子線程去執(zhí)行MethodB”按鈕往果,讓MethodB在之前創(chuàng)建的子線程中去執(zhí)行的話疆液,程序就會崩潰。因?yàn)楫?dāng)MethodA被執(zhí)行完畢以后陕贮,子線程已經(jīng)處于finished狀態(tài)堕油,系統(tǒng)會將其釋放。
解決上述問題肮之,讓子線程進(jìn)行常駐掉缺,隨時可以執(zhí)行任務(wù):

將上述methodA方法改為:
- (void)methodA {
    //在當(dāng)前子線程中開啟一個RunLoop
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    [runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [runLoop run];
    NSLog(@"方法A被執(zhí)行");
}

此時我們點(diǎn)擊"開啟子線程并執(zhí)行MethodA"創(chuàng)建子線程,并且執(zhí)行methodA戈擒。methodA方法是不會輸出"方法A被執(zhí)行"的眶明。因?yàn)槲覀儎?chuàng)建了一個持續(xù)運(yùn)行的RunLoop。只有RunLoop結(jié)束的時候筐高,此語句才會被輸出搜囱。我們點(diǎn)擊“讓子線程去執(zhí)行MethodB”按鈕去執(zhí)行methodB丑瞧,程序正常運(yùn)行。并且我們?nèi)绻@取此子線程的狀態(tài)蜀肘,它處于正在運(yùn)行的狀態(tài)嗦篱,這就達(dá)到了保活的目的幌缝。在開啟RunLoop的時候灸促,我們需要添加事件源或者Timer,否則RunLoop在獲取以后涵卵,會立馬運(yùn)行結(jié)束浴栽。

  • 4.自動釋放池

第一次創(chuàng)建的時機(jī):即將進(jìn)入runloop的時候【kCFRunLoopEntry】
釋放的時機(jī):runloop進(jìn)入休眠狀態(tài)【kCFRunLoopBeforeWaiting】轿偎,或者退出runLoop時【kCFRunLoopExit】典鸡。

  • 注: 當(dāng)runloop即將休眠的時候會把之前的自動釋放池釋放,然后重新創(chuàng)建一個新的釋放池坏晦。

四.RunLoop參考資料

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末萝玷,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子昆婿,更是在濱河造成了極大的恐慌球碉,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,126評論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件仓蛆,死亡現(xiàn)場離奇詭異睁冬,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)看疙,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評論 3 400
  • 文/潘曉璐 我一進(jìn)店門豆拨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人能庆,你說我怎么就攤上這事施禾。” “怎么了搁胆?”我有些...
    開封第一講書人閱讀 169,941評論 0 366
  • 文/不壞的土叔 我叫張陵弥搞,是天一觀的道長。 經(jīng)常有香客問我丰涉,道長拓巧,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,294評論 1 300
  • 正文 為了忘掉前任一死,我火速辦了婚禮肛度,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘投慈。我一直安慰自己承耿,他們只是感情好冠骄,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,295評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著加袋,像睡著了一般凛辣。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上职烧,一...
    開封第一講書人閱讀 52,874評論 1 314
  • 那天扁誓,我揣著相機(jī)與錄音,去河邊找鬼蚀之。 笑死蝗敢,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的足删。 我是一名探鬼主播寿谴,決...
    沈念sama閱讀 41,285評論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼失受!你這毒婦竟也來了讶泰?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,249評論 0 277
  • 序言:老撾萬榮一對情侶失蹤拂到,失蹤者是張志新(化名)和其女友劉穎痪署,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谆焊,經(jīng)...
    沈念sama閱讀 46,760評論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡惠桃,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,840評論 3 343
  • 正文 我和宋清朗相戀三年浦夷,在試婚紗的時候發(fā)現(xiàn)自己被綠了辖试。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,973評論 1 354
  • 序言:一個原本活蹦亂跳的男人離奇死亡劈狐,死狀恐怖罐孝,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情肥缔,我是刑警寧澤莲兢,帶...
    沈念sama閱讀 36,631評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站续膳,受9級特大地震影響改艇,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜坟岔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,315評論 3 336
  • 文/蒙蒙 一谒兄、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧社付,春花似錦承疲、人聲如沸邻耕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽兄世。三九已至,卻和暖如春啊研,著一層夾襖步出監(jiān)牢的瞬間御滩,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評論 1 275
  • 我被黑心中介騙來泰國打工党远, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留艾恼,地道東北人。 一個月前我還...
    沈念sama閱讀 49,431評論 3 379
  • 正文 我出身青樓麸锉,卻偏偏與公主長得像钠绍,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子花沉,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,982評論 2 361

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