iOS知識梳理:RunLoop

weixin_33670713發表於2017-09-19

相關連線:
深入理解RunLoop
IOS---例項化講解RunLoop
iOS知識點整理-RunLoop
RunLoop的前世今生

RunLoop的概念

RunLoop是一個時間處理環,系統利用這個時間處理環來安排事物,協調輸入的各種時間.RunLoop的目的是讓你的執行緒在有工作的時候忙碌,沒有工作的時候休眠(和執行緒相關)

一般來講,一個執行緒只能執行一個任務,執行完成後執行緒就會退出.....但是實際情況是,我們希望有一個機制,,讓執行緒能隨時處理時間但並不退出....當我們給他傳送退出指令是它才退出...

在iOS中RunLoop就是用來實現這種機制的...這種機制的關鍵點在於:如何處理時間和訊息,讓執行緒在沒有處理訊息的時候休眠以避免資源佔用,在有訊息到來時立刻被喚醒

RunLoop實際上就是一個物件...這個物件管理其需要處理的時間和訊息, 並提供了一個入口函式來執行上訴邏輯....執行緒執行了這個函式後...就會一直處於這個函式內部 "接收訊息 ->等待 -> 處理"的迴圈中,知道這個迴圈結束(比如傳入quit訊息),函式返回..

iOS提供了兩個這樣的物件 NSRunLoop和CFRunLoopRef

RunLoop和執行緒的關係

執行緒和RunLoop之間是一一對應的,其關係是儲存在一個全域性的Dictionary裡....執行緒建立之後是沒有RunLoop的(主執行緒除外),RunLoop的建立時發生在第一次獲取時.

蘋果不允許世界建立RunLoop, 但是可以通過[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()來獲取(如果沒有就會自動建立一個)

RunLoop對外的介面

CoreFondation裡關於RunLoop有4個類:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
RunLoop共包含5個類,但公開的只有Source,Timer,Observer相關的三個類.

1.RunLoop Modes
一個RunLoop包含若干個Mode,每個Mode又包含若干個Source/Timer/Observer每次呼叫RunLoop的函式是,只能指定其中一個mode,這個mode被稱為CurrentMode....如果需要切換Mode..只能退出Loop, 再重新指定一個Mode進入.這樣做是為了分割開不通組的Source/Timer/Observer,讓其相互不影響.

  • kCFDefaultRunLoopMode App的預設Mode,通常主執行緒是在這個Mode下執行
  • UITrackingRunLoopMode 介面跟蹤Mode,用於ScrollView追蹤觸控滑動,保證介面滑動時不受其他Mode影響
  • UIInitializationRunLoopMode 在剛啟動App時第進入的第一個Mode,啟動完成後就不再使用
  • GSEventReceiveRunLoopMode 接受系統事件的內部Mode,通常用不到
  • kCFRunLoopCommonModes 這是一個佔位用的Mode,不是一種真正的Mode

2.RunLoop Timer
基於時間的觸發器,基本上就是說NSTimer
在新建NSTimer之後,要把timer新增到RunLoop中,否則timer不會執行.

NSTimer *timer = [NSTimer timerWithTimerInterval:5.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

add到相應的Mode定時器的方法就只能在相應的Mode下才生效,
也可以把Mode設定為NSRunLoopCommonModes,就可以再預設模式和追蹤模式下都能執行.
如果是通過scheduledTimerWithTimeInterval建立的NSTimer,預設新增到RunLoop的DefaultMode中,所以他會自動執行.

當其加入到RunLoop時,RunLoop會註冊對應的時間點,當時間點到時,RunLoop會被喚醒以執行那個回撥.如果執行緒阻塞或者不在這個Mode下,觸發點將不會執行,一直等到下一個週期時間點的觸發.

3.RunLoop Source
CFRunLoopSourceRef是事件源,比如外部的觸控,點選事件和系統內部程式間的通訊等.
Source有兩個版本:Source0和Source1
Source0:非基於Port的,只包含了一個回撥,它並不能主動觸發時間.使用時,你需要先呼叫CFRunLoopSourceSignal(source),講這個Source標記為待處理,然後手動呼叫CFRunLoopWakeUp(runloop)來喚醒RunLoop.讓其處理這個事件.
Source1:基於Port的,包含了一個mach_port和一個回撥,被用於通過核心和其他執行緒相互傳送訊息.這種Source能主動喚醒RunLoop的執行緒,後面講到AFNetworking建立的常駐執行緒就是線上程中新增一個NSport來實現的.

4.RunLoop Observer
CFRunLoopObserverRef是觀察者,每個Observer都包含一個回撥,當RunLoop的狀態發生變化時,觀察者就能通過回撥接收到這個變化.

1923354-d6d7b1b2bd7e7cb8.png
RunLoop的狀態變化

3.RunLoop的執行機制

1923354-847761d36b5881b3.png
RunLoop的執行機制

RunLoop的實際應用

1.AutoreleasePool

app啟動後,系統啟動主執行緒並建立了RunLoop,在主執行緒裡註冊了兩個observer,回撥都是_wrapRunLoopWithAutoreleasePoolHandle()
第一個observer監聽一個事件:即將進入Loop(kCFRunLoopEntry),
呼叫_objc_autoreleasePoolPush()建立一個棧自動釋放池,這個優先順序最高,保證建立釋放池在其他操作之前
第二個observer監聽兩個事件:準備進入休眠(kCFRunLoopBeforeWaiting)和即將退出Loop(kCFRunLoopExit)
進入休眠釋放舊的池並建立新的池,退出是釋放掉自動釋放池.

在主執行緒中執行程式碼一般都是寫在事件回撥或Timer回撥中的,這些回撥都被加入了主執行緒的自動釋放池中,所以在ARC模式下我們不用關心物件什麼時候釋放,也不用去建立和管理pool.(如果事件不在主執行緒中要注意建立自動釋放池,否則可能會出現記憶體洩漏)

2.NSTimer優化使用

開放中常見的現象,在介面上有個UIScrollView控制元件,如果此時還要一個定時器在執行一個事件,你會發現當你滾動scrollView的時候,定時器會失效..
因為timer用scheduledTimerWithTimeInterval:初始化的時候預設關聯為DefaultMode,在主執行緒UITrackingRunLoopMode優先順序最高,在使用者拖動控制元件時,主執行緒的RunLoop執行在UITrackingRunLoopMode下,因此係統不會立即執行Default Mode下的事件.

解決方法1.把當前timer加入到UITrackingRunLoopMode或者kCFRunLoopCommonModes中.
解決方法2.使用不受RunLoop影響的GCD建立定時器.

3.滑動時載入圖片,滑動不流暢的問題.

使用者滑動scrollview的過程中載入圖片,由於UI的操作都是在主執行緒進行的,會造成滑動不流暢的問題,這個時候我們就需要在滑動的不載入圖片,等滑動操作完成在進行載入圖片的操作.

UIImage *downloadImage = ...
[self.imageView performSelector:@selector(setImage:) 
                        withObject: downloadImage 
                        afterDelay:3.0 
                        inModes:@[NSDefaultRunLoopMode]];

講setImage放到DefaultMode裡確定UITrackingRunLoopMode下該操作不會被執行...

4.網路請求

AFN每次進行的網路操作,開始,暫停,取消操作時,都將相應的執行任務扔進了自己建立的執行緒RunLoop中進行處理,從而避免造成主執行緒的阻塞.

5.處理崩潰讓程式繼續執行

我們都知道,如果App執行遇到 Exception 就會直接崩潰並且退出,其實真正讓應用退出的並不是產生的異常,而是當產生異常時,系統會結束掉當前主執行緒的 RunLoop ,RunLoop 退出主執行緒就退出了,所以應用才會退出。明白這個道理,去完成這個“不可能的任務”就很簡單了。

CFRunLoopRef runLoop = CFRunLoopGetCurrent();
CFArrayRef allModes = CFRunLoopCopyAllModes(runLoop);
while (!isQuit){
    for (NSString *mode in (__bridge NSArray *)allModes) {
        CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
    }
}
CFRelease(allModes);

把上面的程式碼新增到 Exception 的handle方法中,此時建立了一個 RunLoop ,讓這個 RunLoop 在所有的 Mode 下面一直不停的跑,保證主執行緒不會退出,我們的應用也就存活下來了。

相關文章