iOS開發經驗(18)-Runloop

目錄

  1. Runloop
  2. RunLoop 與線程
  3. 個人理解總結
  4. 應用場景
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會自動處理之前未處理的事件,并且通知相關的觀察者。
具體的順序如下:

  1. 通知觀察者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的情況下無限進行下去,造成內存泄漏。
解決辦法:

  1. 手動調用invalid方法并置為nil
  2. 構造一個中間類,提供傳入對象和方法的接口,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任務!更高效

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,237評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,957評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,248評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,356評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,081評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,485評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,534評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,720評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,263評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,025評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,204評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,787評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,461評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,874評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,105評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,945評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,205評論 2 375

推薦閱讀更多精彩內容