目錄
- Runloop
- RunLoop 與線程
- 個人理解總結
- 應用場景
1. 什么是RunLoop
基本作用
- 保持程序的持續運行(do-while循環,使app不斷運行)
- 處理App中的各種事件(觸摸、定時器、Selector)
- 節省CPU資源、提高程序性能:該做事的時候做事,該休息的時候休息。
RunLoop基本運行流程
運行邏輯總結:一個線程對應一個runLoop,主線程的runloop是程序一啟動,默認就創建一個runloop,創建好了之后就會給它添加一些默認的模式,每個模式里面會有很多的 source /timer/observer ,添加好這些模式后,observer就會監聽主線程的runloop,進入runloop后,就開始處理事件,先處理timer,再處理source0,source0處理完之后再處理source1,當把這些所有的事件反復的處理完之后,如果沒有事件了,那么runloop就會進入睡眠狀態,當用戶又觸發了新的事件,就會喚醒runloop,喚醒runloop后回到第二步,重新處理新的timer,新的source0,新的source1,處理完后就睡眠,一直反復,當我們把程序關閉或者強退,這個時候observer就會監聽都runloop退出了。
簡單說就是:
先進入 RunLoop,處理系統默認事件,觸發事件的時候,RunLoop 醒來處理 timer、source0、source1,處理完再睡覺。
運行循環本質
線程在執行中的休眠和激活就是由RunLoop對象進行管理的
Runloop 輪詢用來響應事件,runloop里的任務串行執行,容易受堵塞
main 函數中的 RunLoop
UIApplicationMain函數內部就啟動了一個RunLoop,所以UIApplicationMain 函數一直沒有返回,保持了程序的持續運行。這個默認啟動的 RunLoop 是跟主線程相關聯的
2. RunLoop 與線程
一條線程對應一個 RunLoop,主線程的 RunLoop 只要程序已啟動就會默認創建并與主線程綁定好,RunLoop 底層的實現是通過字典的形式來將 線程 和 RunLoop 來綁定的,RunLoop 可以理解為懶加載,子線程的 RunLoop 可以調用 currentRunLoop,先從字典里面根據子線程取,如果沒有就會去創建并與子線程綁定,保存到字典當中。每個 RunLoop 里面有很多的 Mode,每個 Mode 里面又有很多的source、timer、observer。RunLoop 在同一時刻只能執行一種 Mode,當執行這種 Mode 的時候,只有這種 Mode 中的source、timer、observer 有效,別的 Mode 無效,這樣做是為了避免邏輯的混亂。
- 每條線程都有唯一的一個與之對應的 RunLoop 對象
- 主線程的 RunLoop 自動創建好了,子線程的 RunLoop 需要主動創建
- RunLoop 在第一次獲取時創建,在線程結束時銷毀
3. 獲取RunLoop 對象
Foundation
//獲得當前線程的 RunLoop 對象
[NSRunLoop currentRunLoop];
//獲得主線程的 RunLoop 對象
[NSRunLoop mainRunLoop];
Core Foundation
//當前RunLoop
CFRunLoopGetCurrent();
//主線程 RunLoop
CFRunLoopGetMain();
源:分為輸入源和定時源。必須將至少其中一個添加到Runloop中,才能保證Runloop不立即退出;當你創建輸入源的時候,需要將其分配給 runloop 中的一個或多個模式;模式只會在特定事件影響監聽的源。
- 輸入源:
- 自定義輸入源-source0 用戶操作觸摸事件源。使用回調函數來配置自定義輸入源
- 基于端口的輸入源-source1 接受分發系統事件。不需要直接創建輸入源。只要簡單的創建對象,并使用 NSPort 的方法將該端口添加到 Ruhnloop 中
- 定時源:產生基于時間的通知,但它并不是實時機制。和輸入源一樣,定時器也和 runloop 的特定模式相關。
CoreFoundation中關于RunLoop的5個類
- CFRunLoopRef:運行循環對象,也就是它自身
- CFRunLoopModeRef:指定runloop的運行模式。作用:給事件源分組,避免互相影響,邏輯混亂。運行模式1個runLoop可以有很多個Mode,1個Mode可以有很多個Source,Observer,Timer,但是在同一時刻只能同時執行一種Mode
- CFRunLoopSourceRef:輸入源
- CFRunLoopTimerRef:定時源,定時器;必須加入到runloop
- CFRunLoopObserverRef(觀察者,觀察是否有事件)
系統默認注冊了 5個Mode:
- kCFRunLoopDefaultMode:App的默認Mode,通常主線程是在這個 Mode 下運行的
- UITrackingRunLoopMode:界面跟蹤 Mode,用于ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他Mode 影響
- UIInitializationRunLoopMode:在剛啟動 App 時第進入的第一個Mode,啟動完成之后就不再使用。
- GSEventReceiveRunLoopMode:接收系統時間的內部 Mode,通常用不到。
- kCFRunLoopCommonModes(比較特殊):這時一個占位用的 Mode,不是一種真正的 Mode。
RunLoop觀察者介紹:
- CFRunLoopObserverRef是觀察者,能夠監聽RunLoop的狀態改變
- Observer是監聽RunLoop狀態的,CoreFunction向線程添加runloop observers來監聽事件,意在監聽事件發生時來做處理。
- 線程除了處理輸入源,RunLoop也會生成關于Run Loop行為的通知(notification)。RunLoop觀察者(Run-Loop Observers)可以收到這些通知,并在線程上面使用他們來作額外的處理;如果RunLoop沒有任何源需要監視的話,它會在你啟動之際立馬退出。
在每次運行開啟RunLoop的時候,所在線程的RunLoop會自動處理之前未處理的事件,并且通知相關的觀察者。
具體的順序如下:
- 通知觀察者RunLoop已經啟動
- 通知觀察者即將要開始的定時器
- 通知觀察者任何即將啟動的非基于端口的源
- 啟動任何準備好的非基于端口的源
- 如果基于端口的源準備好并處于等待狀態,立即啟動;并進入步驟9
- 通知觀察者線程進入休眠狀態
- 將線程置于休眠知道任一下面的事件發生:
- 某一事件到達基于端口的源
- 定時器啟動
- RunLoop設置的時間已經超時
- RunLoop被顯示喚醒
- 通知觀察者線程將被喚醒
- 處理未處理的事件
- 如果用戶定義的定時器啟動,處理定時器事件并重啟RunLoop。進入步驟2
- 如果輸入源啟動,傳遞相應的消息
- 如果RunLoop被顯示喚醒而且時間還沒超時,重啟RunLoop。進入步驟2
- 通知觀察者RunLoop結束。(自動釋放池)
** RunLoop底層實現原理**
RunLoop 底層的實現是通過字典的形式來將 線程 和 RunLoop 來綁定的,RunLoop 可以理解為懶加載,子線程的 RunLoop 可以調用 currentRunLoop,先從字典里面根據子線程取,如果沒有就會去創建并與子線程綁定,保存到字典當中。每個 RunLoop 里面有很多的 Mode,每個 Mode 里面又有很多的source、timer、observer。RunLoop 在同一時刻只能執行一種 Mode,當執行這種 Mode 的時候,只有這種 Mode 中的source、timer、observer 有效,別的 Mode 無效,這樣做是為了避免邏輯的混亂。
3. 個人理解總結
runloop就是一個do-while循環;
runloop就是用來接受事件源,管理線程,安排線程處理事件。線程是執行任務的。app需要持續運行,如果在主線程里,不開啟runloop,就會關閉app。所以程序啟動的時候,在創建主線程的時候,runloop也被系統創建了,來保持app持續運行、安排線程處理事件;
它以dic的形式跟線程綁定在一起,key是線程,value是它的runloop。創建方式是懶加載;
子線程開啟時,如果沒有獲取runloop,執行完任務就會銷毀,如果你想讓線程不自動銷毀,可以獲取runloop,讓runloop安排線程添加源(輸入源,計時器源)并執行任務。在添加了 source 以后,你可以給 runloop 添加 observers 來監測 runloop 的不同的執行的狀態。注意如果不添加源,runloop會立馬退出。
runloop有5個大類:
1. 自身對象;
2. mode:指定事件處理模式 ;在設置 RunLoopMode 以后,你的 RunLoop 就會自動過濾和其他 Mode 相關的事件源,而只監視和當前設置 Mode 相關的源(以及通知相關的觀察者)。
3. souce:事件(用戶操作,系統事件);
4. timer:計時器
5. Observer:給 RunLoop 注冊觀察者 Observer,以便監控 RunLoop 的運行過程
mode補充:
主要有三個mode對象:
默認是主線程下的有以下作用:等待喚醒;安排工作優先級順序; 大多數工作中默認的運行方式。
第二個,使用這個Mode去跟蹤來自用戶交互的事件,比如UITableView上下滑動,當scroll滑動時,切換為trackmode,讓scroll優先級提高,其他事件優先級排后,保證流暢 ;
第三個基于上面兩個的混合體:什么時候用,即讓scroll流暢運行,也讓timer事件得到回調得以運行
Runloop退出
移除runloop的輸入源和定時器也可能導致run loop退出
使用方法:
開辟線程,獲取runloop
自定義事件源或使用系統端口NSPort,添加到runloop;
指定mode給事件分組;
添加觀察者,監聽狀態;
當事件的模式與消息循環的模式匹配的時候,消息才會運行:讓線程即將休眠時,執行任務;接受到用戶觸摸事件時,切換mode,暫停任務,保證流暢。
4. 應用場景
1. 定時器
- NSTimer+NSRunLoop:容易受線程堵塞影響(此文主要講解這個)
- GCD定時器:GCD 創建的好處,不受 RunLoopMode 的影響。
NSTimer:
就是CFRunLoopTimerRef。
主要用于計時器的工作,當創建完計時器,必須要把它加入runloop中才能進行正常回調,
提問1.那么為什么要將它加入runloop?
回答:這是由于計時器的功能決定的,計時器要不斷的運行休眠切換,是持續性的行為。如果不放在runloop中,它無法持續進行,只有runloop才能安排線程什么時候處理這個事件。通過自身的observer類不斷監聽,來進行回調休眠回調休眠。
提問2.NSTimer有什么需要注意的地方或者說有什么缺點?
回答:需要注意循環引用問題:因為NSTimer強持有target(為什么要強持有target:為了在運行中怕它被銷毀,事件的不斷持續運行時,所以要強持有),在once的情況下,一般沒有問題;但當repeat=YES時,如果我們不主動調用invalid方法,它會在強持有target的情況下無限進行下去,造成內存泄漏。
解決辦法:
- 手動調用invalid方法并置為nil
- 構造一個中間類,提供傳入對象和方法的接口,NSTimer對此對象進行強持有。而此對象會自己銷毀,進而不會永遠被NSTimer所持有造成內存泄漏。
提問3. 在cell上使用NSTimer顯示倒計時,如何即保障滑動流暢又保持數據實時更新并顯示
回答:創建timer,手動加入到runloop中,指定mode為commonmode模式。
2. ImageView顯示
另外還有一個trick是當tableview的cell從網絡異步加載圖片, 加載完成后在主線程刷新顯示圖片, 這時滑動tableview會造成卡頓. 通常的思路是tableview滑動的時候延遲加載圖片, 等停止滑動時再顯示圖片. 這里我們可以通過RunLoop來實現.
[self.cellImageView performSelector:@sector(setImage:)
withObject:downloadedImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
當NSRunLoop為NSDefaultRunLoopMode的時候tableview肯定停止滑動了, why? 因為如果還在滑動中, RunLoop的mode應該是UITrackingRunLoopMode.
3. PerformSelector:
[self performSelector:@selector(download:) withObject:url afterDelay:1.0f];
- 當調用 NSObject 的 performSelector:afterDelay:后,實際上內部會創建一個 Timer 并添加到當前線程的 RunLoop 中,所以如果當前線程沒有 RunLoop,則這個方法會失效。在調用時的當前線程的runloop的default模式中運行。相當于在default中加了個定時器
4. 常駐線程
創建一個線程來處理耗時且頻繁的操作,例如即時聊天音頻的壓縮,或者經常下載,避免頻繁開啟線程以便提高性能, AFNetWorking就是如此。
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
5. 利用Runloop預處理多個cell高度
原理:滑動的時候,主線程中的runloop,會將默認的mode(處理事件)切換為trackmode,也就是說屏蔽了其他源中的事件(一種 mode對應一種事件源),保障滑動流暢。
預處理cell高度:分解成多個runloop source任務,不能在同一個runloop中迭代執行,因為會造成ui卡頓,這時就需要手動向 RunLoop 中添加 Source 任務。可以使用performer的方法,自定義事件源sourceo0任務,通過這個方法加入到指定線程的runloop中,并指定mode,在給定的 Mode 下執行,若指定的 RunLoop 處于休眠狀態,則喚醒它處理事件。創建觀察者監聽runloop的狀態。于是,我們用一個可變數組裝載當前所有需要“預緩存”的 index path,每個 RunLoopObserver 回調時都把第一個任務拿出來分發。這樣,每個任務都被分配到下個“空閑” RunLoop 迭代中執行,其間但凡有滑動事件開始,Mode 切換成 UITrackingRunLoopMode,所有的“預緩存”任務的分發和執行都會自動暫定,最大程度保證滑動流暢。
runloop狀態:當用戶停止滑動的時候,喚醒runloop,切換到默認mode,讓其執行計算事件;當用戶滑動的時候,通知runloop,切換trackmode,讓其停止計算任務,并休眠。
[self performSelector:@selector(opCellheight:) onThread:sunThread withObject:url waitUntilDone:YES modes:array];
6. 滑動與圖片刷新
- 滑動與圖片刷新:當tableView的cell上有需要從網絡獲取的圖片的時候,滾動tableView,異步線程回去加載圖片,加載完成后主線程會設置cell的圖片,但是會造成卡頓。可以設置圖片的任務在CFRunloopDefaultMode下進行,當滾動tableView的時候,Runloop切換到UITrackingRunLoopMode,不去設置圖片,而是而是當停止的時候,再去設置圖片。
[self performSelector:@selector(download:) withObject:url afterDelay:0 inModes:NSDefaultRunLoopMode];
場景5跟6解決方法思想差不多,但是場景5的方法創建了多個source任務!更高效