解密-神秘的RunLoop

配圖

引言

一直以來RunLoop就是個神秘的領(lǐng)域,好多2.3年的開發(fā)者都不能準(zhǔn)確的表述它的作用,說它神秘,其實RunLoop并沒有大家想象中的那么神秘,那么不好理解,本文就帶大家好好剖析一下"神秘的RunLoop"

什么是RunLoop

從字面上看

  • 運行循環(huán)
  • 跑圈
?循環(huán)

基本作用

  • 保持程序的持續(xù)運行(比如主運行循環(huán))
  • 處理App中的各種事件(比如觸摸事件、定時器事件缀台、Selector事件)
  • 節(jié)省CPU資源企巢,提高程序性能:該做事時做事,該休息時休息

存在價值

沒有RunLoop
有RunLoop

main函數(shù)中的RunLoop(主運行循環(huán))

主運行循環(huán)
  • 第14行代碼的UIApplicationMain函數(shù)內(nèi)部就啟動了一個RunLoop
  • 所以UIApplicationMain函數(shù)一直沒有返回周拐,保持了程序的持續(xù)運行
  • 這個默認(rèn)啟動的RunLoop是跟主線程相關(guān)聯(lián)的

RunLoop對象

  • iOS中有2套API來訪問和使用RunLoop

  • Foundation

    • NSRunLoop
  • Core Foundation

    • CFRunLoopRef
  • NSRunLoop和CFRunLoopRef都代表著RunLoop對象

  • NSRunLoop是基于CFRunLoopRef的一層OC包裝,所以要了解RunLoop內(nèi)部結(jié)構(gòu)凰兑,需要多研究CFRunLoopRef層面的API(Core Foundation層面)

RunLoop資料

RunLoop與線程

  • 每條線程都有唯一的一個與之對應(yīng)的RunLoop對象

  • 主線程的RunLoop已經(jīng)自動創(chuàng)建好了妥粟,子線程的RunLoop需要主動創(chuàng)建

  • RunLoop在第一次獲取時創(chuàng)建,在線程結(jié)束時銷毀

獲取RunLoop對象

  • Foundation
[NSRunLoop currentRunLoop]; // 獲得當(dāng)前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
  • Core Foundation
CFRunLoopGetCurrent(); // 獲得當(dāng)前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象

RunLoop相關(guān)類

  • Core Foundation中關(guān)于RunLoop的5個類
  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

注:RunLoop如果沒有這些東西 會直接退出

CFRunLoopModeRef

  • CFRunLoopModeRef代表RunLoop的運行模式

  • 一個 RunLoop 包含若干個 Mode吏够,每個Mode又包含若干個Source/Timer/Observer

  • 每次RunLoop啟動時勾给,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode

  • 如果需要切換Mode锅知,只能退出Loop播急,再重新指定一個Mode進(jìn)入

  • 這樣做主要是為了分隔開不同組的Source/Timer/Observer,讓其互不影響

?相關(guān)類

系統(tǒng)默認(rèn)注冊了5個Mode:(前兩個跟最后一個常用)

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

  • UITrackingRunLoopMode:界面跟蹤 Mode桩警,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響

  • UIInitializationRunLoopMode: 在剛啟動 App 時第進(jìn)入的第一個 Mode昌妹,啟動完成后就不再使用

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

  • kCFRunLoopCommonModes: 這是一個占位用的Mode,不是一種真正的Mode

CFRunLoopSourceRef

  • 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 的線程。

函數(shù)調(diào)用棧
?函數(shù)調(diào)用棧

CFRunLoopTimerRef

  • CFRunLoopTimerRef是基于時間的觸發(fā)器

  • 基本上說的就是NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影響

  • GCD的定時器不受RunLoop的Mode影響

CFRunLoopObserverRef

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

  • 可以監(jiān)聽的時間點有以下幾個

?可監(jiān)聽狀態(tài)
使用
- (void)observer
{
    // 創(chuàng)建observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"----監(jiān)聽到RunLoop狀態(tài)發(fā)生改變---%zd", activity);
    });

    // 添加觀察者:監(jiān)聽RunLoop的狀態(tài)
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

    // 釋放Observer
    CFRelease(observer);
}

特別注意
/*
    CF的內(nèi)存管理(Core Foundation)
    1.凡是帶有Create康聂、Copy、Retain等字眼的函數(shù)胞四,創(chuàng)建出來的對象恬汁,都需要在最后做一次release
    * 比如CFRunLoopObserverCreate
    2.release函數(shù):CFRelease(對象);
 */

RunLoop處理邏輯

- 官方版

?官方版
?邏輯

- 網(wǎng)友整理版

網(wǎng)友版

注:進(jìn)入RunLoop前 會判斷模式是否為空,為空直接退出


RunLoop應(yīng)用

  • NSTimer
  • ImageView顯示
  • PerformSelector
  • 常駐線程
  • 自動釋放池

1.NSTimer(最常見RunLoop使用)

- (void)timer
{
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    // 定時器只運行在NSDefaultRunLoopMode下,一旦RunLoop進(jìn)入其他模式辜伟,這個定時器就不會工作
    //    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

    // 定時器只運行在UITrackingRunLoopMode下氓侧,一旦RunLoop進(jìn)入其他模式,這個定時器就不會工作
    //    [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

    // 定時器會跑在標(biāo)記為common modes的模式下
    // 標(biāo)記為common modes的模式:UITrackingRunLoopMode和NSDefaultRunLoopMode兼容
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)timer2
{
    // 調(diào)用了scheduledTimer返回的定時器导狡,已經(jīng)自動被添加到當(dāng)前runLoop中约巷,而且是NSDefaultRunLoopMode
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    // 修改模式
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

場景還原

拖拽時模式由NSDefaultRunLoopMode 進(jìn)入 UITrackingRunLoopMode

此時如下圖: NSTimer 不再響應(yīng) 圖片停止輪播

NSDefaultRunLoopMode模式
NSRunLoopCommonModes 模式下兩種模式都可運行

此時如下圖: NSTimer 在兩個模式下都可正常運行

new.gif

2.ImageView

需求:當(dāng)用戶在拖拽時(UI交互時)不顯示圖片,拖拽完成時顯示圖片

方法1 監(jiān)聽UIScrollerView滾動 (通過UIScrollViewDelegate監(jiān)聽,此處不再舉例)
方法2 RunLoop 設(shè)置運行模式
 // 只在NSDefaultRunLoopMode模式下顯示圖片
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

3.PerformSelector

PerformSelector

inModes:設(shè)置運行模式

4.常駐線程 (重要)

應(yīng)用場景:經(jīng)常在后臺進(jìn)行耗時操作,如:監(jiān)控聯(lián)網(wǎng)狀態(tài),掃描沙盒等 不希望線程處理完事件就銷毀,保持常駐狀態(tài)

首先創(chuàng)建一個線程
- (void)viewDidLoad {
 [super viewDidLoad]; 
//創(chuàng)建一個線程 
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(createRunloopByNormal) object:nil] ;
// self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(createRunloopByCFObserver) object:nil] ;
// self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(createRunloopByCFTimer) object:nil] ;
// self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(createRunloopByCFSource) object:nil] ; 
//啟動線程
[self.thread start];
}
第一種(推薦)
開啟
  - (void)run
{
    @autoreleasepool {
        
        //addPort:添加端口(就是source)  forMode:設(shè)置模式
        [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        
        //開啟runloop
        [[NSRunLoop currentRunLoop] run];
    }

 /*
  //另外兩種啟動方式
    [NSDate distantFuture]:遙遠(yuǎn)的未來  這種寫法跟上面的run是一個意思
    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
    不設(shè)置模式
    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
  */
}
測試一下線程是否退出
- (IBAction)btnClick:(id)sender { 
    NSLog(@"-----btnClick--------"); 
   [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void)test{ 
//能打印說明沒有退出
NSLog(@"----->Test");
}
退出-退出當(dāng)前線程
[NSThread exit];
第二種(奇葩法)
優(yōu)點:退出RunLoop比較方便-定義個標(biāo)記 while(flag){...}
- (void)run
{
    while (1) {
        [[NSRunLoop currentRunLoop] run];
    }
}

5.自動釋放池

在休眠前(kCFRunLoopBeforeWaiting)進(jìn)行釋放,處理事件前創(chuàng)建釋放池,中間創(chuàng)建的對象會放入釋放池

特別注意:

在啟動RunLoop之前建議用 @autoreleasepool {...}包裹

意義:創(chuàng)建一個大釋放池,釋放{}期間創(chuàng)建的臨時對象,一般好的框架的作者都會這么做

- (void)execute
{
    @autoreleasepool {
        NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    }
}

題外話:

以后為了增加用戶體驗 在用戶UI交互的時候 不做事件處理 我們可以把需要做的操作放到NSDefaultRunLoopMode

補充:GCD定時器

一般的NSTimer定時器因為受到RunLoop,會存在時間不準(zhǔn)時的情況.
上文有提到GCD不受RunLoop影響,下面簡單的說一下它的使用

/** 定時器(這里不用帶*,因為dispatch_source_t就是個類旱捧,內(nèi)部已經(jīng)包含了*) */
@property (nonatomic, strong) dispatch_source_t timer;

int count = 0;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 獲得隊列
//    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    dispatch_queue_t queue = dispatch_get_main_queue();

    // 創(chuàng)建一個定時器(dispatch_source_t本質(zhì)還是個OC對象)
    self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);

    // 設(shè)置定時器的各種屬性(幾時開始任務(wù)独郎,每隔多長時間執(zhí)行一次)
    // GCD的時間參數(shù),一般是納秒 NSEC_PER_SEC(1秒 == 10的9次方納秒)
    // 何時開始執(zhí)行第一個任務(wù)
    // dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC) 比當(dāng)前時間晚3秒
    dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
    uint64_t interval = (uint64_t)(1.0 * NSEC_PER_SEC);
    dispatch_source_set_timer(self.timer, start, interval, 0);

    // 設(shè)置回調(diào)
    dispatch_source_set_event_handler(self.timer, ^{
        NSLog(@"------------%@", [NSThread currentThread]);
        count++;

//        if (count == 4) {
//            // 取消定時器
//            dispatch_cancel(self.timer);
//            self.timer = nil;
//        }
    });

    // 啟動定時器
    dispatch_resume(self.timer);
}


RunLoop面試題

經(jīng)常會有喜歡裝B的面試官,面試的時候?就喜歡問RunLoop,其實?他真的會嗎? 說不定他自己都不太理解
下面我對有關(guān)RunLoop的面試做一個簡單的總結(jié),也算是對全文一個總結(jié)

  • 什么是RunLoop?

  • 從字面上看:運行循環(huán)枚赡、跑圈

  • 其實它內(nèi)部就是do-while循環(huán),在這個循環(huán)內(nèi)部不斷的處理各種任務(wù)(比如Source氓癌、Timer、Observer)

  • 一個線程對應(yīng)一個RunLoop,主線程的RunLoop默認(rèn)已經(jīng)啟動,子線程的RunLoop需要手動啟動(調(diào)用run方法)

  • RunLoop只能選擇一個Mode啟動,如果當(dāng)前Mode中沒有任何Soure贫橙、Timer贪婉、Observer,那么就直接退出RunLoop

  • 在開發(fā)中如何使用RunLoop?什么應(yīng)用場景?

  • 開啟一個常駐線程(讓一個子線程不進(jìn)入消亡狀態(tài),等待其他線程發(fā)來消息,處理其他事件)

    • 在子線程中開啟一個定時器
    • 在子線程中進(jìn)行一些長期監(jiān)控
  • 可以控制定時器在特定模式下執(zhí)行

  • 可以讓某些事件(行為、任務(wù))在特定模式下執(zhí)行

  • 可以添加Observer監(jiān)聽RunLoop的狀態(tài),比如監(jiān)聽點擊事件的處理(在所有點擊事件之前做一些事情)

最后

之前發(fā)布的文章寫得不是很完整,我又花了兩天時間重新做了梳理,還有什么不足之處,歡迎大家指出,我會第一時間更新.

特別感謝

感謝@Delpan提出的寶貴意見,文章已經(jīng)增加了source0/source1的解釋

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末料皇,一起剝皮案震驚了整個濱河市谓松,隨后出現(xiàn)的幾起案子星压,更是在濱河造成了極大的恐慌践剂,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件娜膘,死亡現(xiàn)場離奇詭異逊脯,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)竣贪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進(jìn)店門军洼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人演怎,你說我怎么就攤上這事匕争。” “怎么了爷耀?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵甘桑,是天一觀的道長。 經(jīng)常有香客問我,道長跑杭,這世上最難降的妖魔是什么铆帽? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮德谅,結(jié)果婚禮上爹橱,老公的妹妹穿的比我還像新娘。我一直安慰自己窄做,他們只是感情好愧驱,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著浸策,像睡著了一般冯键。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上庸汗,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天惫确,我揣著相機(jī)與錄音,去河邊找鬼蚯舱。 笑死改化,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的枉昏。 我是一名探鬼主播陈肛,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼兄裂!你這毒婦竟也來了句旱?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤晰奖,失蹤者是張志新(化名)和其女友劉穎谈撒,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體匾南,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡啃匿,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蛆楞。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片溯乒。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖豹爹,靈堂內(nèi)的尸體忽然破棺而出裆悄,到底是詐尸還是另有隱情,我是刑警寧澤臂聋,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布光稼,位于F島的核電站崖技,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏钟哥。R本人自食惡果不足惜迎献,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望腻贰。 院中可真熱鬧吁恍,春花似錦、人聲如沸播演。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽写烤。三九已至翼闽,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間洲炊,已是汗流浹背感局。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留暂衡,地道東北人询微。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像狂巢,于是被迫代替她去往敵國和親撑毛。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

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