Runloop
實(shí)現(xiàn)了線程內(nèi)部的事件循環(huán)常侣。一個(gè)線程通常一次只能執(zhí)行一個(gè)任務(wù)蜡饵,任務(wù)執(zhí)行完成線程就會(huì)退出,但是有時(shí)候胳施,我們希望線程能夠一直處理事件不退出溯祸,或者說(shuō)有事件就執(zhí)行,沒(méi)事件就等待事件巾乳。就比如主線程您没,app能隨時(shí)接收消息鸟召,處于接收消息 -> 等待 -> 處理的循環(huán)中
Runloop與線程的關(guān)系
1胆绊、Runloop與線程一一對(duì)應(yīng)
2、線程創(chuàng)建時(shí)沒(méi)有Runloop欧募,Runloop的創(chuàng)建發(fā)生在第一次獲取時(shí)压状,類似懶加載
3、Runloop的銷毀發(fā)生在線程結(jié)束時(shí)
4跟继、主線程的Runloop默認(rèn)開啟
5种冬、Runloop用于管理線程中要處理的事件和消息,并有一個(gè)入口函數(shù)舔糖,在線程執(zhí)行完后能一直處于接收消息 -> 等待 -> 處理的循環(huán)中娱两,直至線程結(jié)束
iOS中的NSRunloop和CFRunloopRef
Cocoa中的NSRunLoop類并不是線程安全的,我們不能在一個(gè)線程中去操作另外一個(gè)線程的run loop對(duì)象金吗,那很可能會(huì)造成意想不到的后果十兢。不過(guò)幸運(yùn)的是CoreFundation中的不透明類CFRunLoopRef是線程安全的趣竣,而且兩種類型的run loop完全可以混合使用。Cocoa中的NSRunLoop類可以通過(guò)實(shí)例方法:
- (CFRunLoopRef)getCFRunLoop;
獲取對(duì)應(yīng)的CFRunLoopRef類旱物,來(lái)達(dá)到線程安全的目的
Runloop如何工作
Runloop的內(nèi)部結(jié)構(gòu)
- CoreFoundation下的Runloop
typedef CFStringRef CFRunLoopMode CF_EXTENSIBLE_STRING_ENUM;
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopSource * CFRunLoopSourceRef;
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoopObserver * CFRunLoopObserverRef;
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
struct __CFRunLoopMode {
CFStringRef _name; // Mode Name, 例@"kCFRunLoopDefaultMode"
CFMutableSetRef _sources0; // Set
CFMutableSetRef _sources1; // Set
CFMutableArrayRef _observers; // Array
CFMutableArrayRef _timers; // Array
...
};
struct __CFRunLoop {
CFMutableSetRef _commonModes; // Set
CFMutableSetRef _commonModeItems; // Set
CFRunLoopModeRef _currentMode; // Current Runloop Mode
CFMutableSetRef _modes; // Set
...
};
Runloop的Mode
我們都知道Runloop有幾個(gè)常用的模式遥缕,比如:NSDefaultRunLoopMode\NSRunLoopCommonModes\UITrackingRunLoopMode,每個(gè)Mode包含一組source\observer\timer, 又稱為Mode item宵呛,要知道不同mode的下单匣,Runloop的工作機(jī)制是不完全相同的,所以宝穗,每個(gè)Runloop對(duì)象中會(huì)有很多mode
1户秤、Runloop啟動(dòng)時(shí),只能選擇一個(gè)mode進(jìn)行配置
2逮矛、當(dāng)mode要切換虎忌,Runloop會(huì)退出,選擇新的mode重新啟動(dòng)橱鹏,這樣做主要是為了分隔開不同組的 Source/Timer/Observer膜蠢,讓其互不影響
3、每個(gè)mode會(huì)有一個(gè)name用于區(qū)分莉兰,一組observer觀察Runloop的狀態(tài)挑围,另外就是事件源sources和timer,封裝事件消息
4糖荒、當(dāng)mode切換時(shí)杉辙,Runloop會(huì)將commonModeItems的source\observer\timer,同步到具有common屬性的mode中CFRunLoopSourceRef 是事件產(chǎn)生的地方捶朵。Source有兩個(gè)版本:Source0 和 Source1蜘矢。
Source0 只包含了一個(gè)回調(diào)(函數(shù)指針),它并不能主動(dòng)觸發(fā)事件综看。使用時(shí)品腹,你需要先調(diào)用 CFRunLoopSourceSignal(source),將這個(gè) Source 標(biāo)記為待處理红碑,然后手動(dòng)調(diào)用 CFRunLoopWakeUp(runloop) 來(lái)喚醒 RunLoop舞吭,讓其處理這個(gè)事件。
Source1 包含了一個(gè) mach_port 和一個(gè)回調(diào)(函數(shù)指針)析珊,被用于通過(guò)內(nèi)核和其他線程相互發(fā)送消息羡鸥。這種 Source 能主動(dòng)喚醒 RunLoop 的線程,其原理在下面會(huì)講到忠寻。CFRunLoopTimerRef 是基于時(shí)間的觸發(fā)器惧浴,它和 NSTimer 是toll-free bridged 的,可以混用奕剃。其包含一個(gè)時(shí)間長(zhǎng)度和一個(gè)回調(diào)(函數(shù)指針)衷旅。當(dāng)其加入到 RunLoop 時(shí)哑姚,RunLoop會(huì)注冊(cè)對(duì)應(yīng)的時(shí)間點(diǎn),當(dāng)時(shí)間點(diǎn)到時(shí)芜茵,RunLoop會(huì)被喚醒以執(zhí)行那個(gè)回調(diào)叙量。
CFRunLoopObserverRef 是觀察者,每個(gè) Observer 都包含了一個(gè)回調(diào)(函數(shù)指針)九串,當(dāng) RunLoop 的狀態(tài)發(fā)生變化時(shí)绞佩,觀察者就能通過(guò)回調(diào)接受到這個(gè)變化≈砼ィ可以觀測(cè)的時(shí)間點(diǎn)有以下幾個(gè):
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
應(yīng)用場(chǎng)景舉例:主線程的 RunLoop 里有兩個(gè)預(yù)置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode品山。這兩個(gè) Mode 都已經(jīng)被標(biāo)記為"Common"屬性。DefaultMode 是 App 平時(shí)所處的狀態(tài)烤低,TrackingRunLoopMode 是追蹤 ScrollView 滑動(dòng)時(shí)的狀態(tài)肘交。當(dāng)你創(chuàng)建一個(gè) Timer 并加到 DefaultMode 時(shí),Timer 會(huì)得到重復(fù)回調(diào)扑馁,但此時(shí)滑動(dòng)一個(gè)TableView時(shí)涯呻,RunLoop 會(huì)將 mode 切換為 TrackingRunLoopMode,這時(shí) Timer 就不會(huì)被回調(diào)腻要,并且也不會(huì)影響到滑動(dòng)操作复罐。
有時(shí)你需要一個(gè) Timer,在兩個(gè) Mode 中都能得到回調(diào)雄家,一種辦法就是將這個(gè) Timer 分別加入這兩個(gè) Mode效诅。還有一種方式,就是將 Timer 加入到頂層的 RunLoop 的 "commonModeItems" 中趟济。"commonModeItems" 被 RunLoop 自動(dòng)更新到所有具有"Common"屬性的 Mode 里去乱投。
CFRunLoop對(duì)外暴露的管理 Mode 接口只有下面2個(gè):
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
Mode 暴露的管理 mode item 的接口有下面幾個(gè):
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
你只能通過(guò) mode name 來(lái)操作內(nèi)部的 mode,當(dāng)你傳入一個(gè)新的 mode name 但 RunLoop 內(nèi)部沒(méi)有對(duì)應(yīng) mode 時(shí)顷编,RunLoop會(huì)自動(dòng)幫你創(chuàng)建對(duì)應(yīng)的 CFRunLoopModeRef戚炫。對(duì)于一個(gè) RunLoop 來(lái)說(shuō),其內(nèi)部的 mode 只能增加不能刪除勾效。
蘋果公開提供的 Mode 有兩個(gè):kCFRunLoopDefaultMode (NSDefaultRunLoopMode) 和 UITrackingRunLoopMode嘹悼,你可以用這兩個(gè) Mode Name 來(lái)操作其對(duì)應(yīng)的 Mode叛甫。
同時(shí)蘋果還提供了一個(gè)操作 Common 標(biāo)記的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes)层宫,你可以用這個(gè)字符串來(lái)操作 Common Items,或標(biāo)記一個(gè) Mode 為 "Common"其监。使用時(shí)注意區(qū)分這個(gè)字符串和其他 mode name萌腿。
AutoreleasePool
App啟動(dòng)后,蘋果在主線程 RunLoop 里注冊(cè)了兩個(gè) Observer抖苦,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler()毁菱。
第一個(gè) Observer 監(jiān)視的事件是 Entry(即將進(jìn)入Loop)米死,其回調(diào)內(nèi)會(huì)調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動(dòng)釋放池。其 order 是-2147483647贮庞,優(yōu)先級(jí)最高峦筒,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前。
第二個(gè) Observer 監(jiān)視了兩個(gè)事件: BeforeWaiting(準(zhǔn)備進(jìn)入休眠) 時(shí)調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池窗慎;Exit(即將退出Loop) 時(shí)調(diào)用 _objc_autoreleasePoolPop() 來(lái)釋放自動(dòng)釋放池物喷。這個(gè) Observer 的 order 是 2147483647,優(yōu)先級(jí)最低遮斥,保證其釋放池子發(fā)生在其他所有回調(diào)之后峦失。
在主線程執(zhí)行的代碼,通常是寫在諸如事件回調(diào)术吗、Timer回調(diào)內(nèi)的尉辑。這些回調(diào)會(huì)被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著,所以不會(huì)出現(xiàn)內(nèi)存泄漏较屿,開發(fā)者也不必顯示創(chuàng)建 Pool 了隧魄。
事件響應(yīng)
蘋果注冊(cè)了一個(gè) Source1 (基于 mach port 的) 用來(lái)接收系統(tǒng)事件,其回調(diào)函數(shù)為 __IOHIDEventSystemClientQueueCallback()。
當(dāng)一個(gè)硬件事件(觸摸/鎖屏/搖晃等)發(fā)生后,首先由 IOKit.framework 生成一個(gè) IOHIDEvent 事件并由 SpringBoard 接收漱贱。這個(gè)過(guò)程的詳細(xì)情況可以參考這里憔辫。SpringBoard 只接收按鍵(鎖屏/靜音等),觸摸胯努,加速,接近傳感器等幾種 Event,隨后用 mach port 轉(zhuǎn)發(fā)給需要的App進(jìn)程辉川。隨后蘋果注冊(cè)的那個(gè) Source1 就會(huì)觸發(fā)回調(diào),并調(diào)用 _UIApplicationHandleEventQueue() 進(jìn)行應(yīng)用內(nèi)部的分發(fā)拴测。
_UIApplicationHandleEventQueue() 會(huì)把 IOHIDEvent 處理并包裝成 UIEvent 進(jìn)行處理或分發(fā)乓旗,其中包括識(shí)別 UIGesture/處理屏幕旋轉(zhuǎn)/發(fā)送給 UIWindow 等。通常事件比如 UIButton 點(diǎn)擊集索、touchesBegin/Move/End/Cancel 事件都是在這個(gè)回調(diào)中完成的屿愚。
手勢(shì)識(shí)別
當(dāng)上面的 _UIApplicationHandleEventQueue() 識(shí)別了一個(gè)手勢(shì)時(shí),其首先會(huì)調(diào)用 Cancel 將當(dāng)前的 touchesBegin/Move/End 系列回調(diào)打斷务荆。隨后系統(tǒng)將對(duì)應(yīng)的 UIGestureRecognizer 標(biāo)記為待處理妆距。
蘋果注冊(cè)了一個(gè) Observer 監(jiān)測(cè) BeforeWaiting (Loop即將進(jìn)入休眠) 事件,這個(gè)Observer的回調(diào)函數(shù)是 _UIGestureRecognizerUpdateObserver()函匕,其內(nèi)部會(huì)獲取所有剛被標(biāo)記為待處理的 GestureRecognizer娱据,并執(zhí)行GestureRecognizer的回調(diào)。
當(dāng)有 UIGestureRecognizer 的變化(創(chuàng)建/銷毀/狀態(tài)改變)時(shí)盅惜,這個(gè)回調(diào)都會(huì)進(jìn)行相應(yīng)處理中剩。
界面更新
當(dāng)在操作 UI 時(shí)忌穿,比如改變了 Frame、更新了 UIView/CALayer 的層次時(shí)结啼,或者手動(dòng)調(diào)用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后掠剑,這個(gè) UIView/CALayer 就被標(biāo)記為待處理,并被提交到一個(gè)全局的容器去郊愧。
蘋果注冊(cè)了一個(gè) Observer 監(jiān)聽(tīng) BeforeWaiting(即將進(jìn)入休眠) 和 Exit (即將退出Loop) 事件澡腾,回調(diào)去執(zhí)行一個(gè)很長(zhǎng)的函數(shù):
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。這個(gè)函數(shù)里會(huì)遍歷所有待處理的 UIView/CAlayer 以執(zhí)行實(shí)際的繪制和調(diào)整糕珊,并更新 UI 界面动分。
定時(shí)器
NSTimer 其實(shí)就是 CFRunLoopTimerRef,他們之間是 toll-free bridged 的红选。一個(gè) NSTimer 注冊(cè)到 RunLoop 后澜公,RunLoop 會(huì)為其重復(fù)的時(shí)間點(diǎn)注冊(cè)好事件。例如 10:00, 10:10, 10:20 這幾個(gè)時(shí)間點(diǎn)喇肋。RunLoop為了節(jié)省資源坟乾,并不會(huì)在非常準(zhǔn)確的時(shí)間點(diǎn)回調(diào)這個(gè)Timer。Timer 有個(gè)屬性叫做 Tolerance (寬容度)蝶防,標(biāo)示了當(dāng)時(shí)間點(diǎn)到后甚侣,容許有多少最大誤差。
如果某個(gè)時(shí)間點(diǎn)被錯(cuò)過(guò)了间学,例如執(zhí)行了一個(gè)很長(zhǎng)的任務(wù)殷费,則那個(gè)時(shí)間點(diǎn)的回調(diào)也會(huì)跳過(guò)去,不會(huì)延后執(zhí)行低葫。就比如等公交详羡,如果 10:10 時(shí)我忙著玩手機(jī)錯(cuò)過(guò)了那個(gè)點(diǎn)的公交,那我只能等 10:20 這一趟了嘿悬。
CADisplayLink 是一個(gè)和屏幕刷新率一致的定時(shí)器(但實(shí)際實(shí)現(xiàn)原理更復(fù)雜实柠,和 NSTimer 并不一樣,其內(nèi)部實(shí)際是操作了一個(gè) Source)善涨。如果在兩次屏幕刷新之間執(zhí)行了一個(gè)長(zhǎng)任務(wù)窒盐,那其中就會(huì)有一幀被跳過(guò)去(和 NSTimer 相似),造成界面卡頓的感覺(jué)钢拧。在快速滑動(dòng)TableView時(shí)蟹漓,即使一幀的卡頓也會(huì)讓用戶有所察覺(jué)。
PerformSelecter
當(dāng)調(diào)用 NSObject 的 performSelecter:afterDelay: 后娶靡,實(shí)際上其內(nèi)部會(huì)創(chuàng)建一個(gè) Timer 并添加到當(dāng)前線程的 RunLoop 中牧牢。所以如果當(dāng)前線程沒(méi)有 RunLoop,則這個(gè)方法會(huì)失效姿锭。
當(dāng)調(diào)用 performSelector:onThread: 時(shí)塔鳍,實(shí)際上其會(huì)創(chuàng)建一個(gè) Timer 加到對(duì)應(yīng)的線程去,同樣的呻此,如果對(duì)應(yīng)線程沒(méi)有 RunLoop 該方法也會(huì)失效轮纫。
關(guān)于GCD
實(shí)際上 RunLoop 底層也會(huì)用到 GCD 的東西,比如 RunLoop 是用 dispatch_source_t 實(shí)現(xiàn)的 Timer焚鲜。但同時(shí) GCD 提供的某些接口也用到了 RunLoop掌唾, 例如 dispatch_async()。
當(dāng)調(diào)用 dispatch_async(dispatch_get_main_queue(), block) 時(shí)忿磅,libDispatch 會(huì)向主線程的 RunLoop 發(fā)送消息糯彬,RunLoop會(huì)被喚醒,并從消息中取得這個(gè) block葱她,并在回調(diào) CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里執(zhí)行這個(gè) block撩扒。但這個(gè)邏輯僅限于 dispatch 到主線程,dispatch 到其他線程仍然是由 libDispatch 處理的吨些。