runloop詳解

1.什么是RunLoop
RunLoop的字面意思是運(yùn)行循環(huán),是在程序在運(yùn)行過程中保持循環(huán)做一些事情趣竣,也就是保持程序的持續(xù)運(yùn)行。每條線程都有唯一的一個與之對應(yīng)的runloop對象卫袒,主線程的Runloop已經(jīng)自己創(chuàng)建好单匣,子線程的runloop需要主動創(chuàng)建宝穗。RunLoop在第一次獲取時創(chuàng)建迹冤,在線程結(jié)束時銷毀。主線程的runloop是默認(rèn)開啟的泡徙,iOS應(yīng)用程序里面,程序啟動后會調(diào)用main函數(shù)莉兰,main函數(shù)會調(diào)用UIApplicationMain()函數(shù)礁竞,這個方法的主線程會設(shè)置一個runloop對象。

2.應(yīng)用范疇
定時器
PerformSelector
GCD ASYN MAIN QUEUE
事件響應(yīng)模捂、手勢識別、界面刷新
網(wǎng)絡(luò)請求
自動釋放池

3.RunLoop的主要作用
保持程序的持續(xù)運(yùn)行综看;
處理App中的各種事件(比如:觸摸事件岖食、定時器事件、Selector事件)
節(jié)省CPU資源泡垃,提高程序性能:該做事時做事,該休息時休息

4.RunLoop的獲取

@property (class, readonly, strong) NSRunLoop *currentRunLoop;
@property (class, readonly, strong) NSRunLoop *mainRunLoop API_AVAILABLE(macos(10.5), ios(2.0), watchos(2.0), tvos(9.0));

5.RunLoop與線程的關(guān)系
每條線程都有唯一一個與之對應(yīng)的RunLoop對象
RunLoop保存在一個全局的Dictionary里忠寻,線程作為key澎剥,RunLoop作為value
線程剛創(chuàng)建時并沒有RunLoop對象,RunLoop會在第一次獲取它時創(chuàng)建
RunLoop會在線程結(jié)束時自動銷毀
主線程的RunLoop已經(jīng)默認(rèn)獲取并開啟哑姚,子線程是 默認(rèn)沒有開啟RunLoop

6.RunLoop的模式
NSDefaultRunLoopMode 默認(rèn)狀態(tài),空間狀態(tài)倡蝙,默認(rèn)主線程在此mode下
UITrackingRunLoopMode 滑動ScrollView
UIInitializationRunLoopMode 私有绞佩,App啟動時使用猪钮,啟動后就不在使用
NSRunLoopCommonModes 這是一個占位用的mode胆建,作為標(biāo)記defalt和tracking的mode用
GSEventReceiveRunLoopMode 接受系統(tǒng)事件的內(nèi)部Mode,通常用不到

7.RunLoop相關(guān)的類
CFRunLoopRef:runloop
CFRunLoopModeRef:runloop的運(yùn)行模式
CFRunLoopSourceRef:事件產(chǎn)生的地方
CFRunLoopTimerRef:基于事件的觸發(fā)器
CGRunLoopObserverRef:觀察者笆载,每個Observer都包含了一個回調(diào),當(dāng)RunLoop的狀態(tài)發(fā)生變化時腻要,觀察者就能通過回調(diào)接收到這個變化

這五個類的關(guān)系如下圖:


image.png

RunLoop啟動時只能選擇一個Mode涝登,作為currentMode,如果需要切換mode胀滚,需要退出當(dāng)前的RunLoop,重新選擇一個mode進(jìn)入咙好。如果RunLoop沒有任何的sources0褐荷、source1嘹悼、timer、observer杨伙,那么RunLoop會馬上退出。

sources0:包含觸摸事件處理抖苦、performSelector:OnThread

sources1:基于Port端口的線程間通信米死、系統(tǒng)事件的捕捉(觸摸事件是由source1捕捉,包裝成事件由source0來處理)

timer:NSTimer峦筒、performSelector:withObject:afterDelay:

observer:用于監(jiān)聽RunLoop狀態(tài)、UI刷新(beforwaiting)卤材、自動釋放池(beforewaiting)

8.CGRunLoopObserverRef相關(guān)的狀態(tài)

觀測的時間點有這幾個:

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    監(jiān)聽全部狀態(tài)改變  

監(jiān)聽runloop的observer的狀態(tài)如下:

#import "ViewController.h"
 
@interface ViewController ()
 
@end
 
@implementation ViewController
 
void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"kCFRunLoopBeforeTimers");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"kCFRunLoopBeforeSources");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"kCFRunLoopBeforeWaiting");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"kCFRunLoopAfterWaiting");
            break;
        case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit");
            break;
        default:
            break;
    }
}
 
- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 創(chuàng)建Observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
    // 添加Observer到RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    // 釋放
    CFRelease(observer);
}
 
@end

通過上面的代碼扇丛,可以了解runloop模式的切換术吗,以及可以添加定時器查看模式的切換帆精。

9.RunLoop的運(yùn)行邏輯

image.png

注意:GCD很多時候是不依賴與RunLoop來實現(xiàn)的实幕,當(dāng)在子線程做耗時操作,在主線程刷新時昆庇,才會依賴于RunLoop來實現(xiàn)。

查看源碼的大概邏輯如下:

image.png

當(dāng)runloop休眠的時候拱撵,是從用戶態(tài)切換到了內(nèi)核態(tài)表蝙,當(dāng)有消息喚醒時,就從內(nèi)核態(tài)再切換到用戶態(tài)中集索。

10.RunLoop與NSTimer的關(guān)系
NSTimer創(chuàng)建之后汇跨,需要添加到Runloop中才會工作,RunLoop的主線程默認(rèn)是默認(rèn)的模式穷遂,而子線程的RunLoop是默認(rèn)沒有開啟的。


image.png
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo

這個方法內(nèi)部會自動創(chuàng)建定時器對象添加到當(dāng)前的runloop盅惜,并且制定運(yùn)行模式為默認(rèn)的忌穿,
如果想修改運(yùn)行模式,可修改成:

//如果想利用上面的那種方法伴网,修改運(yùn)行模式,可修改成如下
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

如果定時器添加到主線程中沸伏,則不需要開始runloop,定時器就可以工作红选,但是如果姆另,直接添加到子線程中喇肋,需要手動開啟:

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
 
    [self performSelectorInBackground:@selector(RunLoopModeAndTimer) withObject:nil];
 
}
 
- (void)RunLoopModeAndTimer {
 
   //該方法內(nèi)部會自動創(chuàng)建的定時器對象添加到當(dāng)前的runloop迹辐,并且指定運(yùn)行模式為默認(rèn)
   [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
   [[NSRunLoop currentRunLoop] run];  //如果不加這一步,點擊界面時间学,定時器是不工作的印荔,因為定時器對象添加到當(dāng)前的runloop中,而當(dāng)前的runloop在子線程中仍律;子線程中的runloop需要手動創(chuàng)建,所以此時定時器不工作善涨。
}
 
- (void)run {
    NSLog(@"run----%@", [NSRunLoop currentRunLoop].currentMode);
}

11.RunLoop的線程辈菰颍活(常駐線程)
正如上文所說,runLoop與線程是一一對應(yīng)的關(guān)系畔师,如果線程結(jié)束牧牢,而且RunLoop沒有任何的sources0、source1伯铣、timer轮纫、observer,那么RunLoop會馬上退出掌唾;如果想讓RunLoop不要立馬退出忿磅,那么就需要添加事件源凭语,RunLoop本身提供了添加事件源的接口,如下:

- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
 
- (void)addPort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;
- (void)removePort:(NSPort *)aPort forMode:(NSRunLoopMode)mode;

一般情況下吨些,我們會選擇 - (void)addPort:(NSPort *)aPort forMode:(NSRunLoopMode)mode; 這種方法來為線程背椿裕活。如果我們在子線程開啟一個RunLoop黔寇,那么我們不應(yīng)該調(diào)用runloop的run方法,這種開發(fā)runloop的方法是無法停止runloop的啡氢。

11.1 三種啟動RunLoop的方式
通過[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()可以獲取當(dāng)前線程的runloop。
啟動一個runloop有以下三種方法:

- (void)run;  
 
- (void)runUntilDate:(NSDate *)limitDate亭枷;
 
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

這三種方式無論通過哪一種方式啟動runloop搀崭,如果沒有一個輸入源或者timer附加于runloop上,runloop就會立刻退出瘤睹。

(1) 第一種方式,runloop會一直運(yùn)行下去驴党,在此期間會處理來自輸入源的數(shù)據(jù)获茬,并且會在NSDefaultRunLoopMode模式下重復(fù)調(diào)用runMode:beforeDate:方法;

(2) 第二種方式恕曲,可以設(shè)置超時時間,在超時時間到達(dá)之前把还,runloop會一直運(yùn)行,在此期間runloop會處理來自輸入源的數(shù)據(jù)吊履,并且也會在NSDefaultRunLoopMode模式下重復(fù)調(diào)用runMode:beforeDate:方法;

(3) 第三種方式练俐,runloop會運(yùn)行一次冕臭,超時時間到達(dá)或者第一個input source被處理,則runloop就會退出辜贵。

前兩種啟動方式會重復(fù)調(diào)用runMode:beforeDate:方法。

11.2 退出RunLoop的方式
第一種啟動方式的退出方法

文檔說鼻由,如果想退出runloop厚棵,不應(yīng)該使用第一種啟動方式來啟動runloop。

如果runloop沒有input sources或者附加的timer婆硬,runloop就會退出。
雖然這樣可以將runloop退出向楼,但是蘋果并不建議我們這么做谐区,因為系統(tǒng)內(nèi)部有可能會在當(dāng)前線程的runloop中添加一些輸入源,所以通過手動移除input source或者timer這種方式宋列,并不能保證runloop一定會退出。

第二種啟動方式runUntilDate:

可以通過設(shè)置超時時間來退出runloop戈鲁。

第三種啟動方式runMode:beforeDate:

通過這種方式啟動嘹叫,runloop會運(yùn)行一次,當(dāng)超時時間到達(dá)或者第一個輸入源被處理罩扇,runloop就會退出。

如果我們想控制runloop的退出時機(jī)喂饥,而不是在處理完一個輸入源事件之后就退出,那么就要重復(fù)調(diào)用runMode:beforeDate:或粮,

具體可以參考蘋果文檔給出的方案捞高,如下:

 NSRunLoop *myLoop  = [NSRunLoop currentRunLoop];
 myPort = (NSMachPort *)[NSMachPort port];
 [myLoop addPort:_port forMode:NSDefaultRunLoopMode];
 
BOOL isLoopRunning = YES; // global
 
while (isLoopRunning && [myLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);



//關(guān)閉runloop的地方
- (void)quitLoop
 {
    isLoopRunning = NO;
    CFRunLoopStop(CFRunLoopGetCurrent());
}

11.3 總之

如果不想退出runloop可以使用第一種方式啟動runloop;
使用第二種方式啟動runloop氢哮,可以通過設(shè)置超時時間來退出型檀;
使用第三種方式啟動runloop,可以通過設(shè)置超時時間或者使用CFRunLoopStop方法來退出胀溺。

11.4 代碼演示


#import "ViewController.h"
#import "YZThread.h"
 
@interface ViewController ()
 
@property (nonatomic, strong) YZThread *thread;
@property (nonatomic, assign) BOOL isStop;
 
@end
 
@implementation ViewController
 
- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    self.thread = [[YZThread alloc] initWithBlock:^{
        NSLog(@"begin----");
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        
        while (!weakSelf.isStop){
            NSLog(@"------");
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        NSLog(@"end----");
    }];
    [self.thread start];
    
}
 
- (IBAction)gotoView:(id)sender {
    ViewController *vc = [[ViewController alloc] init];
    [self.navigationController pushViewController:vc animated:YES];
}
 
 
 
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [self performSelector:@selector(test2) onThread:self.thread withObject:nil waitUntilDone:NO];
}
 
- (void)test2 {
    NSLog(@"打印線程 - %@", [NSThread currentThread]);
}
 
- (void)stop {
    self.isStop = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
}
 
- (void)willMoveToParentViewController:(UIViewController *)parent {
    if (parent == nil) {
        [self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
    }
}
 
- (void)dealloc {
    NSLog(@"viewcontroller --- dealloc");
}
 
@end

需要注意的是仓坞,如果在線程中開啟了一個runloop那么就需要查看是否釋放內(nèi)存了。這種方式容易造成內(nèi)存泄漏或者循環(huán)引用扯躺,內(nèi)存一直不釋放。

另外倍啥,開啟runloop時澎埠,如果選擇NSRunLoopCommonModes 這種模式,那么runloop就是一直在工作蒲稳,而且程序會卡死!JP病祥国!只能選中一種模式進(jìn)入j枪邸!灼擂!
————————————————

版權(quán)聲明:本文為博主原創(chuàng)文章觉至,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議,轉(zhuǎn)載請附上原文出處鏈接和本聲明语御。

原文鏈接:https://blog.csdn.net/lyz0925/article/details/103818848

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末沃暗,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子孽锥,更是在濱河造成了極大的恐慌,老刑警劉巖唬涧,帶你破解...
    沈念sama閱讀 211,743評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盛撑,死亡現(xiàn)場離奇詭異,居然都是意外死亡抵卫,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,296評論 3 385
  • 文/潘曉璐 我一進(jìn)店門殖氏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來姻采,“玉大人,你說我怎么就攤上這事婚瓜。” “怎么了巴刻?”我有些...
    開封第一講書人閱讀 157,285評論 0 348
  • 文/不壞的土叔 我叫張陵蛉签,是天一觀的道長茂附。 經(jīng)常有香客問我督弓,道長乒验,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,485評論 1 283
  • 正文 為了忘掉前任狂塘,我火速辦了婚禮鳄厌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘了嚎。我一直安慰自己,他們只是感情好萝勤,可當(dāng)我...
    茶點故事閱讀 65,581評論 6 386
  • 文/花漫 我一把揭開白布呐伞。 她就那樣靜靜地躺著,像睡著了一般伶氢。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蜗巧,一...
    開封第一講書人閱讀 49,821評論 1 290
  • 那天劣砍,我揣著相機(jī)與錄音,去河邊找鬼刑枝。 笑死,一個胖子當(dāng)著我的面吹牛靠娱,可吹牛的內(nèi)容都是我干的掠兄。 我是一名探鬼主播锌雀,決...
    沈念sama閱讀 38,960評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼迅诬,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了侈贷?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,719評論 0 266
  • 序言:老撾萬榮一對情侶失蹤撑蚌,失蹤者是張志新(化名)和其女友劉穎搏屑,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體辣恋,經(jīng)...
    沈念sama閱讀 44,186評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡抑党,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,516評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了底靠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,650評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡壹瘟,死狀恐怖鳄逾,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情雕凹,我是刑警寧澤,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布线欲,位于F島的核電站汽摹,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏逼泣。R本人自食惡果不足惜舟舒,卻給世界環(huán)境...
    茶點故事閱讀 39,936評論 3 313
  • 文/蒙蒙 一嗜憔、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧莺治,春花似錦帚稠、人聲如沸床佳。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,757評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至浪感,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間揭斧,已是汗流浹背峻堰。 一陣腳步聲響...
    開封第一講書人閱讀 31,991評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留捐名,地道東北人。 一個月前我還...
    沈念sama閱讀 46,370評論 2 360
  • 正文 我出身青樓成艘,卻偏偏與公主長得像贺归,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子牧氮,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,527評論 2 349

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

  • 不得不說踱葛,人的惰性是真可怕啊丹莲。從上周六就到寫runLoop的建議開始,星期三告訴自己從星期四開始著手寫這篇博客盯另。然...
    老司機(jī)Wicky閱讀 7,155評論 20 137
  • 本文的相關(guān)題目洲赵,都是查閱網(wǎng)上資料,如有疑問叠萍,歡迎討論! 1苛谷、如果頁面 A 跳轉(zhuǎn)到 頁面 B,A 的 viewDid...
    sy隨緣閱讀 714評論 0 1
  • RunLoop 是 iOS 和 OSX 開發(fā)中非扯楞玻基礎(chǔ)的一個概念锣尉,這篇文章將從 CFRunLoop 的源碼入手,介...
    缽_Right閱讀 814評論 0 1
  • 寫在前面 本文僅是自己學(xué)習(xí)RunLoop的一個記錄坟奥,參考了ibireme大神的 深入理解RunLoop[https...
    蘇東沒有坡閱讀 7,562評論 0 8
  • 前言拇厢,文章是轉(zhuǎn)載的,因為之前收藏的旺嬉,今天突然發(fā)現(xiàn)沒了。不知道什么原因邪媳,簡書搜索不到了,有幾篇同樣轉(zhuǎn)載的雨效,但是代碼沒...
    安靜就好_閱讀 2,621評論 6 42