iOS開發(fā)runloop運行循環(huán)機制學(xué)習(xí)
引言
RunLoop:又叫運行循環(huán)機制,在iOS中的兩大機制之一。并不是只有iOS有Runloop其他語言也有,他們的方式不太一樣,但是核心都是為了解決性能和良好的運行,例如:webJs里Runloop也稱作eventLoop,由于js沒有多線程,在這樣的情況做了一種調(diào)用棧來配合主線程運行。而在iOS里面runloop就不太一樣,因為有多線程的原因,runloop是配合多線程使用的。每一個線程都對應(yīng)一個runloop。
Runloop最核心的事情就是保證程序的持續(xù)運行讓線程在沒有消息的時候休眠,在有消息時喚醒,以提高程序性能。這個機制是依靠系統(tǒng)內(nèi)核來完成的(蘋果操作系統(tǒng)核心組件 Darwin 中的 Mach)
概念:RunLoop 是通過內(nèi)部維護(hù)的事件循環(huán)(Event Loop)來對事件/消息進(jìn)行管理的一個對象。
1、沒有消息處理時,休眠已避免資源占用,由用戶態(tài)切換到內(nèi)核態(tài)(CPU-內(nèi)核態(tài)和用戶態(tài))
2、有消息需要處理時,立刻被喚醒,由內(nèi)核態(tài)切換到用戶態(tài)
main函數(shù)是不會退出的,為什么呢?這個時候就是 UIApplicationMain 內(nèi)部默認(rèn)開啟了主線程的 RunLoop,并執(zhí)行了一段無限循環(huán)的代碼(不是簡單的 for 循 環(huán)或 while 循環(huán))
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
UIApplicationMain 函數(shù)一直沒有返回,而是不斷地接收處理消息以及等待休眠,所以運行程序之后會保 持持續(xù)運行狀態(tài)
// 偽代碼 int main(int argc, char * argv[]) { BOOL running = YES; do { //執(zhí)行的事件 } while(running) return 0; }
一、Runloop的實現(xiàn)機制
RunLoop 通過 mach_msg()函數(shù)接收、發(fā)送消息。它的本質(zhì)是調(diào)用函數(shù) mach_msg_trap(),相當(dāng)于是一 個系統(tǒng)調(diào)用,會觸發(fā)內(nèi)核狀態(tài)切換。
在用戶態(tài)調(diào)用 時會切換到內(nèi)核態(tài);內(nèi)核態(tài)中內(nèi)核 實現(xiàn)的 mach_msg()函數(shù)會完成實際的工作。
二、Runloop 數(shù)據(jù)結(jié)構(gòu)
是 CFRunLoop(CoreFoundation)的封裝,提供了面向?qū)ο蟮?nbsp;API 相關(guān)的主要涉及五個類:
- CFRunLoop:Runloop對象 (由 pthread(線程對象,說明 和線程是一一對應(yīng)的)、currentMode(當(dāng)前所處的運行模式)、 modes(多個運行模式的集合)、 (模式名稱字符串集合)、,Timer,Source 集合)構(gòu)成)
- CFRunLoopMode:運行模式(由 name、source0、source1、observers、timers 構(gòu)成)
- CFRunLoopSource:輸入源/事件源 (分為 source0 和 source1 兩種)
- CFRunLoopTimer:定時源(基于時間的觸發(fā)器,基本上說的就是 NStimer。在預(yù)設(shè)的時間點喚醒 執(zhí)行回調(diào)。因為它是基于 RunLoop 的,因此它不是實時的(就是 NStimer 是不準(zhǔn)確的。 因為只負(fù)責(zé)分發(fā)源的消息。如果 線程當(dāng)前正在處理繁重的任務(wù),就有可能導(dǎo)致 Timer 本次延時,或者少執(zhí)行一次))
- CFRunLoopObserver:觀察者(對相關(guān)事件runloop的狀態(tài)進(jìn)行監(jiān)聽)
CFRunLoopSource分為兩種source0和source1詳解:
- source0:
即非基于 port 的,也就是用戶觸發(fā)的事件。需要手動喚醒線程,將當(dāng)前線程從內(nèi)核態(tài)切換到用戶 態(tài) - source1:
基于 port 的,包含一個mach_port和一個回調(diào),可監(jiān)聽系統(tǒng)端口和通過內(nèi)核和其他線程發(fā)送的消息能主動喚醒Runloop接受分發(fā)系統(tǒng)事件 具備喚醒事件的能力
CFRunLoopObserver監(jiān)聽時間點詳細(xì)事件
- kCFRunLoopEntry RunLoop 準(zhǔn)備啟動
- kCFRunLoopBeforeTimers RunLoop 將要處理一些 Timer 相關(guān)事件
- kCFRunLoopBeforeSources RunLoop 將要處理一些 Source 事件
- kCFRunLoopBeforeWaiting RunLoop 將要進(jìn)行休眠狀態(tài),即將由用戶態(tài)切換到內(nèi)核態(tài)
- kCFRunLoopAfterWaiting RunLoop 被喚醒,即從內(nèi)核態(tài)切換到用戶態(tài)后
- kCFRunLoopExit RunLoop 退出
- kCFRunLoopAllActivities 監(jiān)聽所有狀態(tài)
線程和 RunLoop 一一對應(yīng), RunLoop 和 Mode 是一對多的,Mode 和 source、timer、observer 也是一對多 的
三、實現(xiàn)機制
Runloop運行的大致邏輯是:
通知觀察者 RunLoop 即將啟動。
通知觀察者即將要處理 Timer 事件。
通知觀察者即將要處理 source0 事件。
處理 source0 事件。
如果基于端口的源(Source1)準(zhǔn)備好并處于等待狀態(tài),進(jìn)入步驟 9。
通知觀察者線程即將進(jìn)入休眠狀態(tài)。
將線程置于休眠狀態(tài),由用戶態(tài)切換到內(nèi)核態(tài),直到下面的任一事件發(fā)生才喚醒線程。
- 一個基于 port 的 Source1 的事件。
- 一個 Timer 到時間了。
- RunLoop 自身的超時時間到了。
- 被其他調(diào)用者手動喚醒。
通知觀察者線程將被喚醒。
處理喚醒時收到的事件
- 如果用戶定義的定時器啟動,處理定時器事件并重啟 RunLoop。進(jìn)入步驟 2。
- 如果輸入源啟動,傳遞相應(yīng)的消息。
- 如果 RunLoop 被顯示喚醒而且時間還沒超時,重啟 RunLoop。進(jìn)入步驟 2
通知觀察者 RunLoop 結(jié)束。
觀察者observer 怎么監(jiān)聽Runloop,監(jiān)聽的狀態(tài)
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { // 即將進(jìn)入runloop kCFRunLoopEntry = (1UL << 0), // 即將處理timer kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理source kCFRunLoopBeforeSources = (1UL << 2), // 即將進(jìn)入休眠 kCFRunLoopBeforeWaiting = (1UL << 5), // 休眠后喚醒 kCFRunLoopAfterWaiting = (1UL << 6), // 退出runloop kCFRunLoopExit = (1UL << 7), // runloop所有活動 kCFRunLoopAllActivities = 0x0FFFFFFFU };
四、runloop 和 線程
1、怎么創(chuàng)建一個常駐線程?
- 為當(dāng)前線程開啟一個 RunLoop(第一次調(diào)用 [NSRunLoop currentRunLoop]方法時實際是會先去創(chuàng)建一 個 RunLoop)
- 向當(dāng)前 RunLoop 中添加一個 Port/Source 等維持 RunLoop 的事件循環(huán)(如果 RunLoop 的 mode 中一個 item 都沒有, runloop會退出)
- 啟動該RunLoop
NSRunLoop *runloop = [NSRunLoop currentRunLoop]; [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runloop run];
2、如果我們開辟一個新的線程 加入了定時器的 這個時候定時器是不會執(zhí)行的,我們看下下面的代碼
- (void)viewDidLoad { [super viewDidLoad]; NSLogMeth(@"1") ygweakify(self); dispatch_async(dispatch_get_global_queue(0, 0), ^{ ygstrongify(self); NSLogMeth(@"2") [self performSelector: @selector(test) afterDelay:10]; NSLogMeth(@"3") }); NSLogMeth(@"4") } - (void)test { NSLogMeth(@"5") }
答案是 1423,test 方法并不會執(zhí)行。
原因是如果是帶 afterDelay 的延時函數(shù),會在內(nèi)部創(chuàng)建一個 NSTimer,然后添加到當(dāng)前線程的 RunLoop 中。 也就是如果當(dāng)前線程沒有開啟 RunLoop,該方法會失效。
我們再看另一個
dispatch_async(dispatch_get_global_queue(0, 0), ^{ ygstrongify(self); NSLogMeth(@"2") [NSRunLoop currentRunLoop] run]; [self performSelector: @selector(test) afterDelay:10]; NSLogMeth(@"3") }); NSLogMeth(@"4") }
答案依然是 1423,test 方法并不會執(zhí)行。
原因是如果 RunLoop 的 mode 中一個 item 都沒有,RunLoop 會退出。即在調(diào)用 RunLoop 的 run 方法后,由 于其 mode 中沒有添加任何 item 去維持 RunLoop 的時間循環(huán),RunLoop 隨即還是會退出。 所以我們自己啟動 RunLoop,一定要在添加 item 后 所以我們把 開啟runloop的代碼 放在 延時方法之后 就好了
dispatch_async(dispatch_get_global_queue(0, 0), ^{ ygstrongify(self); NSLogMeth(@"2") [self performSelector: @selector(test) afterDelay:10]; [NSRunLoop currentRunLoop] run]; NSLogMeth(@"3") }); NSLogMeth(@"4") }
這個時候test的方法就執(zhí)行了
3、怎樣保證子線程數(shù)據(jù)回來更新 UI 的時候不打斷用戶的滑動操作?
當(dāng)我們在子請求數(shù)據(jù)的同時滑動瀏覽當(dāng)前頁面,如果數(shù)據(jù)請求成功要切回主線程更新 UI,那么就會影響當(dāng) 前正在滑動的體驗。
我們就可以將更新 UI 事件放在主線程的 上執(zhí)行即可,這樣就會等用戶不再滑動頁 面,主線程 RunLoop 由 切換到 時再去更新 UI
[self performSelectorOnMainThread: @selector(readload) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];
4、NSTimer 在runloop中的關(guān)系
NSTimer其實就是 CFRunLoopTimerRef(基于時間的觸發(fā)器) ,他們之間是tool-free bridged 的。一個 NSTimer 注冊到RunLoop后, RunLoop會為其重復(fù)的時間點注冊好事件。例如 10:00, 10:10, 10:20 這幾個時間點。 RunLoop為了節(jié)省資源,并不會在非常準(zhǔn)確的時間點回調(diào)這個 Timer。Timer 有個屬性叫做Tolerance(寬容度),標(biāo)示了當(dāng)時間點到后,容許有多少最大誤差。
如果某個時間點被錯過了,例如執(zhí)行了一個很長的任務(wù),則那個時間點的回調(diào)也會跳過去,不會延后執(zhí)行。就比如等公交,如果 10:10 時我忙著玩手機錯過了那個點的公交,那我只能等 10:20 這一趟了。
CADisplayLink 是一個和屏幕刷新率一致的定時器(但實際實現(xiàn)原理更復(fù)雜,和 NSTimer 并不一樣, 其內(nèi)部實際是操作了一個 Source)。如果在兩次屏幕刷新之間執(zhí)行了一個長任務(wù),那其中就會有一幀被 跳過去(和NSTimer相似),造成界面卡頓的感覺。在快速滑動 TableView 時,即使一幀的卡頓也會 讓用戶有所察覺。 FaceBook開源的 AsyncDisplayLink 就是為了解決界面卡頓的問題,其內(nèi)部也用 到了 RunLoop
五、異步繪制
異步繪制,就是可以在子線程把需要繪制的圖形,提前在子線程處理好。將準(zhǔn)備好圖像數(shù)據(jù)直接返給主線程使用,這樣可以降低主線程的壓力。(一般情況下我們都是在主線程繪制的大家可以作為了解,特殊情況下在處理研究)
異步繪制的過程:
要通過系統(tǒng)的 [view.delegate displayLayer:] 這個入口來實現(xiàn)異步繪制。
- 代理負(fù)責(zé)生成對應(yīng)的 Bitmap
- 設(shè)置該 Bitmap 為 layer.contents 屬性的值。
以上就是iOS開發(fā)runloop運行循環(huán)機制學(xué)習(xí)的詳細(xì)內(nèi)容,更多關(guān)于iOS runloop運行循環(huán)的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Mysql中文亂碼以及導(dǎo)出為sql語句和Excel問題解決方法[圖文]
這幾天基于Heritrix寫了一個爬蟲,用到mysql,在導(dǎo)入導(dǎo)出數(shù)據(jù)時,遇到一些亂碼問題,好不容易解決了,記錄一下,以備查看2013-04-04淺談innodb_autoinc_lock_mode的表現(xiàn)形式和選值參考方法
下面小編就為大家?guī)硪黄獪\談innodb_autoinc_lock_mode的表現(xiàn)形式和選值參考方法。小編覺得挺不錯的,現(xiàn)在就分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-03-03