iOS知識梳理:RunLoop

相關連接:
深入理解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的狀態發生變化時,觀察者就能通過回調接收到這個變化.

RunLoop的狀態變化

3.RunLoop的運行機制

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 下面一直不停的跑,保證主線程不會退出,我們的應用也就存活下來了。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容