RunLoop 了解一下

RunLoop , 運(yùn)行循環(huán), App 可以在程序運(yùn)行過(guò)程中做一些事情.

RunLoop 是什么?

為了說(shuō)明, 我們分別用 Xcode 創(chuàng)建兩個(gè)項(xiàng)目, 一個(gè)是 Command Tool, 一個(gè)是Single View App, 眾所周知, 運(yùn)行 Command Tool 程序, 只會(huì)在控制臺(tái)輸出結(jié)果, 并且只是一次性的, 運(yùn)行 App, 程序會(huì)借助 模擬器/真機(jī) 運(yùn)行.

這兩者最大的區(qū)別在于, 在 main.m 文件中

Command Tool
int main(int argc, char * argv[]) {
   return 0
}
App
int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

App 之所以能在模擬器/真機(jī)中長(zhǎng)期保持運(yùn)行 狀態(tài), 而不會(huì)終止, 在于

UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]))

原因:

  • UIApplicationMain() 內(nèi)部會(huì)創(chuàng)建一個(gè) runloop. 使得程序不會(huì)馬上退出, 而是保持保持運(yùn)行狀態(tài).
  • 這里面會(huì)處理App的各種事件(定時(shí)器事件, 用戶交互事件等)

RunLoop對(duì)象

iOS中有兩套API來(lái)訪問(wèn)和使用Runloop.

  1. Foundation: NSRunLoop
  2. Core Foundation: CFRunLoopRef
// viewDidLoad 這個(gè)方法是在主線程中調(diào)用的, 當(dāng)前線程就是主線程
// 所以 mainRunLoop, currentRunLoop獲得的 runloop 對(duì)象的地址是一樣的.
NSLog(@"%p, %p", [NSRunLoop mainRunLoop], [NSRunLoop currentRunLoop]);
// 0x600003748600, 0x600003748600
    
NSLog(@"%p, %p", CFRunLoopGetMain(), CFRunLoopGetCurrent());
// 0x600002f4c900, 0x600002f4c900

NSRunLoop 是基于 CFRunLoopRef 的一層OC包裝, 官方開(kāi)源了Core Foundation 的源碼實(shí)現(xiàn).

在源碼中, 我們查看一下 CFRunLoopGetCurrent() 到底做了什么?



過(guò)程:

  1. 調(diào)用 _CFRunLoopGet0(), 并傳入?yún)?shù) 當(dāng)前線程.
  2. 其中 __CFRunLoops , 是存放以 pthread 為key, RunLoop 為 value 的字典.
  3. 如果從字典中未找到 Runloop對(duì)象, 則 調(diào)用 __CFRunLoopCreate 為這條線程創(chuàng)建新的RunLoop , 并存儲(chǔ)到字典中.

由此我們知道了Runloop 和 線程 的關(guān)系

  • 每條線程都有與之對(duì)應(yīng)的 RunLoop 對(duì)象.
  • 線程剛創(chuàng)建的時(shí)候是沒(méi)有 Runloop 的, 程序在運(yùn)行的過(guò)程中, 會(huì)為這條線程創(chuàng)建對(duì)應(yīng)的 RunLoop 對(duì)象, RunLoop 隨著線程結(jié)束而銷(xiāo)毀
  • 線程runloop 分別以鍵值對(duì)的形式存儲(chǔ)在字典中, 方便程序管理.

Core Foundation中關(guān)于RunLoop的5個(gè)類(lèi)

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

這是 CFRunLoopRef 的實(shí)現(xiàn), 圖中摘取了幾個(gè)比較在意的成員變量.


CFRunLoopModeRef 代表 RunLoop 的運(yùn)行模式
常用到的有兩種

  • kCFRunLoopDefaultMode (Mode的名字)
    App的默認(rèn)Mode, 通常主線程是在這個(gè)Mode下運(yùn)行的

  • UITrackingRunLoopMode (Mode的名字)
    界面追蹤Mode, 用于ScrollView 追蹤觸摸滑動(dòng), 保證界面滑動(dòng)時(shí)不受其他Mode影響

  1. RunLoop 啟動(dòng)時(shí)只能選擇其中的一個(gè) Mode, 作為 currentMode.
  2. 如果需要切換 Mode, 只能退出當(dāng)前 Loop, 再重新選擇一個(gè) Mode 進(jìn)入.
  3. 不同 Model 的 Source0/Source1/Timer/Observer 分隔開(kāi)來(lái), 互不影響.
  4. 如果 Mode 中沒(méi)有任何 Source0/Source1/Timer/Observer, RunLoop會(huì)立馬退出.
  • Source0: 觸摸事件處理, performSelector: OnThread 等.
  • Source1: 基于 Port 的線程間通信, 處理系統(tǒng)事件捕捉等.
  • Timers: NSTimer操作, performSelector:withObject:afterDelay:等
  • Observers: 監(jiān)聽(tīng)RunLoop的狀態(tài), UI刷新(BeforeWaiting), Autorelease pool(BeforeWaiting).
    當(dāng)設(shè)置完view的背景色時(shí), 這段代碼不會(huì)立即生效, 而是等待 RunLoop 即將休眠的時(shí)候, 刷新界面
Mode Name Description
Default NSDefaultRunLoopMode (Cocoa) kCFRunLoopDefaultMode (Core Foundation) 默認(rèn)模式是用于大多數(shù)操作的模式. 大多數(shù)情況下雷滚,您應(yīng)該使用此模式啟動(dòng)運(yùn)行循環(huán)并配置輸入源.
Connection NSConnectionReplyMode (Cocoa) Cocoa將此模式與NSConnection對(duì)象結(jié)合使用以監(jiān)視回復(fù). 很少使用此模式.
Modal NSModalPanelRunLoopMode (Cocoa) Cocoa使用此模式來(lái)識(shí)別用于模態(tài)面板的事件.
Event tracking NSEventTrackingRunLoopMode (Cocoa) Cocoa使用此模式在鼠標(biāo)拖動(dòng)循環(huán)和其他種類(lèi)的用戶界面跟蹤循環(huán)期間限制傳入事件. (拖動(dòng)scrollView)
Common modes NSRunLoopCommonModes (Cocoa) kCFRunLoopCommonModes (Core Foundation) 這是一組可配置的常用模式. 將輸入源與此模式相關(guān)聯(lián)也會(huì)將其與組中的每個(gè)模式相關(guān)聯(lián). 對(duì)于Cocoa應(yīng)用程序, 此集合默認(rèn)包括默認(rèn), 模態(tài)和事件跟蹤模式. Core Foundation最初只包含默認(rèn)模式. 您可以使用CFRunLoopAddCommonMode函數(shù)將自定義模式添加到集合中.

詳解RunLoop

前面我們從源碼層面了解RunLoop, 現(xiàn)在我們從整體再來(lái)看.


運(yùn)行循環(huán)和各種源的概念結(jié)構(gòu)

有幾點(diǎn)我們需要注意:

    1. RunLoop 從兩種不同類(lèi)型的源接收事件
    • Input sources 提供異步事件, 通常來(lái)自另外一個(gè)線程的消息,
    • Timer sources 提供同步事件, 發(fā)生在預(yù)定時(shí)間, 或重復(fù)間隔.
    1. RunLoop Modes 是要監(jiān)視的 Input sources 和 Timer sources 的集合恢共,以及要通知的RunLoop observer的集合.
    • 每次運(yùn)行 RunLoop 時(shí), 都顯示/隱式 指定特定的運(yùn)行模式.
    • 模式是根據(jù)事件的來(lái)源而不是事件的類(lèi)型進(jìn)行區(qū)分的, 比如不會(huì)使用模式僅匹配鼠標(biāo)按下事件或僅匹配鍵盤(pán)事件.
    1. Input sources 中通常有兩類(lèi).
    • 基于端口的 Input source 監(jiān)視應(yīng)用程序的 Mach 端口, 它是由內(nèi)核自動(dòng)發(fā)出信號(hào).
    • 自定義 Input source 處理自定義事件源, 它必須由另一個(gè)線程手動(dòng)發(fā)信號(hào)給自定義源.
  • Cocoa 還定義了一個(gè)自定義輸入源, Cocoa Perform Selector Sources, 它允許我們?cè)谌魏尉€程上執(zhí)行選擇器, 并且執(zhí)行其選擇器后將其自身從 RunLoop 中移除.
    1. runUtilDate: 是 NSRunLoop 類(lèi)的對(duì)象方法, 用來(lái)運(yùn)行 RunLoop.
    1. handlePort:, customSrc:, mySelector:, timeFired 是來(lái)自不同的源的事件(消息).
  • Timer sources 在將來(lái)的預(yù)設(shè)時(shí)間將事件同步傳遞給你的線程蓝谨。定時(shí)器是線程通知自己做某事的一種方式.

補(bǔ)充說(shuō)明: Loop Observer
與在發(fā)生適當(dāng)?shù)漠惒交蛲绞录r(shí)觸發(fā)的源不同铺罢,RunLoop observer 在執(zhí)行 RunLoop 期間, 在特殊位置觸發(fā).


RunLoop的多種狀態(tài):

  • kCFRunLoopEntry: 即將進(jìn)入 RunLoop
  • kCFRunLoopBeforeTimers: 即將處理 Timer
  • kCFRunLoopBeforeSources: 即將處理 Sources
  • kCFRunLoopBeforeWaiting: RunLoop 即將休眠
  • kCFRunLoopAfterWaiting: RunLoop 即將喚醒
  • kCFRunLoopExit: 即將退出RunLoop

RunLoop的事件處理

每次運(yùn)行 RunLoop 時(shí), 線程的RunLoop都會(huì)處理掛起的事件, 并且為任何附加的觀察者生成通知. (App一啟動(dòng), 會(huì)自動(dòng)在主線程設(shè)置并運(yùn)行RunLoop, 稱(chēng)之為 主循環(huán))

  1. Notify observers: 進(jìn)入運(yùn)行循環(huán).
  2. Notify observers: 即將處理 Timer.
  3. Notify observers: 即將處理Sources
  4. 處理Source0: 觸發(fā)任何準(zhǔn)備觸發(fā)的基于非端口的輸入源, 跳到第 9 步:
  5. 處理Source1: (如果基于端口的輸入源準(zhǔn)備就緒并等待觸發(fā)), 就跳到第 9 步:
  6. Notify observers: 線程即將休眠(等待消息喚醒)
  7. Notify observers: 線程結(jié)束休眠(被下面的消息喚醒)
    • 處理Timer
    • 處理Source1: 事件到達(dá)基于端口的輸入源
    • RunLoop 被明確喚醒
    • 為 RunLoop 設(shè)置的超時(shí)值到期
  8. Notify observers: 線程剛剛醒來(lái).
  9. 處理 Blocks:
    • 如果輸入源被觸發(fā)瓢棒,則傳遞事件.
    • 如果觸發(fā)了用戶定義的計(jì)時(shí)器,則處理計(jì)時(shí)器事件并重新RunLoop。轉(zhuǎn)到第2步.
    • 如果運(yùn)行循環(huán)被明確喚醒但尚未超時(shí),請(qǐng)重新RunLoop, 轉(zhuǎn)到第2步
  10. Notify observers: RunLoop 已退出

使用 RunLoop

我們需要顯示運(yùn)行 RunLoop 的唯一時(shí)機(jī)是為應(yīng)用程序創(chuàng)建輔助線程, 對(duì)于輔助線程, 如果確定需要運(yùn)行循環(huán), 那么需要配置并運(yùn)行它.

  • 在線程上使用 Timer.
  • 保持線程以執(zhí)行定期任務(wù)(線程迸ブ耄活).
  • 使用端口或自定義輸入源與其他線程通信.

1. 解決NSTimer在滑動(dòng)時(shí)停止工作的問(wèn)題

NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    NSLog(@"==>%d",_count++);
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

原因:
RunLoop 處理事件的默認(rèn) Mode 是 Default, 當(dāng) app 同時(shí)有計(jì)時(shí)器事件和scrollView滾動(dòng)事件時(shí), 優(yōu)先處理 scrollView 滾動(dòng)事件(Event tracking Mode), 處理完才會(huì)再來(lái)處理計(jì)時(shí)器事件.
解決辦法:
計(jì)時(shí)器事件Common Mode 綁定, RunLoop 內(nèi)部會(huì)自動(dòng)切換 Tracking Mode 和 Default Mode, 來(lái)處理計(jì)時(shí)器事件 和 scrollView 滾動(dòng)事件, 使得兩者看似同時(shí)在工作.

2. 線程保活
LCThread 類(lèi)是一個(gè)繼承自 NSThread 的類(lèi), 在里面我們實(shí)現(xiàn)了 dealloc 方法, 為了監(jiān)測(cè)線程是否被銷(xiāo)毀的情況.

self.thread = [[LCThread alloc] initWithBlock:^{
        // 一直在運(yùn)行. 線程逼噬牛活
        NSLog(@"----begin----%s", __func__);

        // 當(dāng)前runloop開(kāi)始睡眠, 當(dāng)前線程被阻塞了       
        [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        
        NSLog(@"----end----%s", __func__);
}];
// 啟動(dòng)此線程
[self.thread start];

保證線程不立刻被銷(xiāo)毀, 我們?cè)诖似陂g制定任務(wù)
比如: 點(diǎn)擊屏幕. 打印此線程

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{    
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:YES];
}

-(void)test
{
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
}

手動(dòng)釋放線程

- (void)stopThread{
    [self performSelector:@selector(stop) onThread:self.thread withObject:nil waitUntilDone:YES];
}
-(void)stop
{    
    // 停止RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());
    NSLog(@"%s %@", __func__, [NSThread currentThread]);
    
    // 清空線程
    self.thread = nil;
}

3. 監(jiān)控界面卡頓
通過(guò) RunLoop observer 來(lái)監(jiān)控目標(biāo) RunLoop 的狀態(tài), 如果頻繁出現(xiàn) kCFRunLoopBeforeSources, kCFRunLoopAfterWaiting, 檢測(cè)出現(xiàn)次數(shù), timeCount, 超過(guò)指定次數(shù)可認(rèn)為App卡頓 .
因?yàn)檫@兩個(gè)狀態(tài)是要去處理事件的狀態(tài).

參考
Apple官方文檔-RunLoop

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末魏颓,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子吱晒,更是在濱河造成了極大的恐慌甸饱,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件枕荞,死亡現(xiàn)場(chǎng)離奇詭異柜候,居然都是意外死亡搞动,警方通過(guò)查閱死者的電腦和手機(jī)躏精,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)鹦肿,“玉大人矗烛,你說(shuō)我怎么就攤上這事÷崂#” “怎么了瞭吃?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)涣旨。 經(jīng)常有香客問(wèn)我歪架,道長(zhǎng),這世上最難降的妖魔是什么霹陡? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任和蚪,我火速辦了婚禮,結(jié)果婚禮上烹棉,老公的妹妹穿的比我還像新娘攒霹。我一直安慰自己,他們只是感情好浆洗,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布催束。 她就那樣靜靜地躺著,像睡著了一般伏社。 火紅的嫁衣襯著肌膚如雪抠刺。 梳的紋絲不亂的頭發(fā)上塔淤,一...
    開(kāi)封第一講書(shū)人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音速妖,去河邊找鬼凯沪。 笑死,一個(gè)胖子當(dāng)著我的面吹牛买优,可吹牛的內(nèi)容都是我干的妨马。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼杀赢,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼烘跺!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起脂崔,我...
    開(kāi)封第一講書(shū)人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤滤淳,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后砌左,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體脖咐,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年汇歹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了屁擅。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡产弹,死狀恐怖派歌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情痰哨,我是刑警寧澤胶果,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站斤斧,受9級(jí)特大地震影響早抠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜撬讽,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一蕊连、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧锐秦,春花似錦咪奖、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春昧捷,著一層夾襖步出監(jiān)牢的瞬間闲昭,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工靡挥, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留序矩,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓跋破,卻偏偏與公主長(zhǎng)得像簸淀,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子毒返,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

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