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