CFRunLoop
- 這篇文章是在看了sunnyxx大神的線下分享后整理的學(xué)習(xí)筆記廷雅,感謝sunnyxx大神的分享,學(xué)習(xí)路上再接再厲。
- sunnyxx大神的自動(dòng)算高工具UITableView-FDTemplateLayoutCell堵漱,里面有RunLoop的使用技巧。
https://github.com/forkingdog/UITableView-FDTemplateLayoutCell
概念
事件循環(huán)
每個(gè)線程都有一個(gè)RunLoop對(duì)象涣仿,但是只有主線程的RunLoop是開(kāi)啟的勤庐。子線程中的RunLoop默認(rèn)是不被創(chuàng)建的,在子線程中當(dāng)我們調(diào)用
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
獲取RunLoop對(duì)象的時(shí)候好港,就會(huì)創(chuàng)建RunLoop一個(gè)線程可以開(kāi)啟多個(gè)RunLoop愉镰,只不過(guò)都是嵌套在最大的RunLoop中
作用
使程序一直運(yùn)行并接收用戶的輸入
決定程序在何時(shí)處理哪些事件
調(diào)用解耦(主調(diào)方產(chǎn)生很多事件,不用等到被調(diào)方處理完事件之后钧汹,才能執(zhí)行其他操作)
節(jié)省CPU時(shí)間(當(dāng)程序啟動(dòng)后丈探,什么都沒(méi)有執(zhí)行的話,就不用讓CPU來(lái)消耗資源來(lái)執(zhí)行崭孤,直接進(jìn)入睡眠狀態(tài))
模擬RunLoop
int main(int argc, char * argv[]) {
while (程序在運(yùn)行中) {
runloop睡覺(jué)呢
起床了类嗤,有事干了(喚醒runloop)
runloop干活中
}
return 0;
}
構(gòu)成元素
每一個(gè)RunLoop都包含若干個(gè)CFRunLoopMode
在同一時(shí)間,只能在一種Mode下面執(zhí)行
當(dāng)需要切換Mode的時(shí)候辨宠,就必須退出當(dāng)前的RunLoop。重新啟動(dòng)一個(gè)
系統(tǒng)默認(rèn)的有以下5種模式
CFRunLoopDefaultMode: 這個(gè)是默認(rèn) Mode货裹,也是空閑狀態(tài)嗤形。主線程通常在這個(gè) Mode 下運(yùn)行的。
UITrackingRunLoopMode: ScrollView滾動(dòng)時(shí)候的模式弧圆。
UIInitializationRunLoopMode: 在剛啟動(dòng)程序時(shí)進(jìn)入的第一個(gè) Mode赋兵,啟動(dòng)完成后就不再使用。
GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部的Mode搔预,這個(gè)Mode由GraphicsServices調(diào)用在CFRunLoopRunSpecific前面霹期。通常用不到。
CFRunLoopCommonModes: 這是一個(gè)數(shù)組拯田,包括了第1和第2種模式历造。
- CFRunLoopMode的應(yīng)用舉例
當(dāng)我們?cè)谧鰣D片輪播器的時(shí)候,如果使用的是kCFRunLoopDefaultMode那么當(dāng)ScrollView滾動(dòng)的時(shí)候,RunLoop模式就會(huì)切換為UITrackingRunLoopMode吭产,這時(shí)候NSTimer就沒(méi)法執(zhí)行侣监,這時(shí)候我們可以使用kCFRunLoopCommonModes,就可以解決這個(gè)問(wèn)題臣淤。
CFRunLoopMode又包含若干個(gè)CFRunLoopSource\ CFRunLoopTimer\ CFRunLoopObserver
CFRunLoopSource
RunLoop的數(shù)據(jù)源抽象類(類似于OC中的protocol)
RunLoop定義了兩個(gè)版本的source:Source0 和 Source1
Source0:處理的是App內(nèi)部的事件橄霉、App自己負(fù)責(zé)管理,如按鈕點(diǎn)擊事件等邑蒋。
Source1:由RunLoop和內(nèi)核管理姓蜂,Mach Port驅(qū)動(dòng),如CFMachPort医吊、CFMessagePort
- CFRunLoopTimer的封裝有(只是舉例幾個(gè))
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval: (NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
CFRunLoopObserver
作用:告知外界RunLoop狀態(tài)的更改
有以下?tīng)顟B(tài)
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// 進(jìn)入RunLoop開(kāi)始跑了
kCFRunLoopEntry = (1UL << 0),
// 將要執(zhí)行timer了
kCFRunLoopBeforeTimers = (1UL << 1),
// 將要執(zhí)行Source了
kCFRunLoopBeforeSources = (1UL << 2),
// 將要進(jìn)入睡眠
kCFRunLoopBeforeWaiting = (1UL << 5),
// 被喚醒
kCFRunLoopAfterWaiting = (1UL << 6),
// 退出
kCFRunLoopExit = (1UL << 7),
// 全部的狀態(tài)
kCFRunLoopAllActivities = 0x0FFFFFFFU
}
- CFRunLoopObserver的應(yīng)用舉例
- CFRunLoopObserver與Autorelease Pool
CFRunLoopObserver 監(jiān)視到kCFRunLoopEntry(將要進(jìn)入Loop)的時(shí)候覆糟,會(huì)調(diào)用_objc_autoreleasePoolPush() 創(chuàng)建自動(dòng)釋放池。
CFRunLoopObserver 監(jiān)視到kCFRunLoopBeforeWaiting(將要進(jìn)入休眠) 時(shí)調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池遮咖;kCFRunLoopExit(即將退出Loop) 時(shí)會(huì)調(diào)用 _objc_autoreleasePoolPop() 來(lái)釋放自動(dòng)釋放池滩字。
- 重繪視圖
蘋(píng)果為了保證界面的流暢性,(1)不會(huì)重繪屬性(frame等)沒(méi)有改變的視圖(2)只發(fā)送一次drawRect:消息御吞。
當(dāng)相關(guān)的視圖對(duì)象接收到設(shè)置屬性的消息的時(shí)候麦箍,就會(huì)將自己標(biāo)記為要重繪。RunLoop會(huì)收集所有等待重繪制的視圖陶珠,蘋(píng)果會(huì)注冊(cè)一個(gè)CFRunLoopObserver來(lái)監(jiān)聽(tīng)kCFRunLoopBeforeWaiting事件挟裂,當(dāng)事件觸發(fā)的時(shí)候,就會(huì)對(duì)所有等待重繪的視圖對(duì)象發(fā)送drawRect:消息揍诽。
RunLoop的掛起和喚醒
- 當(dāng)RunLoop處于空閑狀態(tài)或者點(diǎn)擊了暫停的時(shí)候,RunLoop就被掛起诀蓉,具體步驟
(1) 指定用于再次喚醒的端口(mach_port)
(2) 調(diào)用mach_msg
監(jiān)聽(tīng)喚醒端口。內(nèi)核調(diào)用mach_msg_trap
讓RunLoop處于mach_msg_trap
狀態(tài)暑脆,RunLoop就會(huì)掛起渠啤,等待激活。就像一段代碼中有scanf函數(shù)添吗,必須要接收一個(gè)輸入一樣沥曹,不輸入就不會(huì)繼續(xù)往下執(zhí)行。這里要區(qū)別于sleep碟联〖嗣溃或者像是Notification,當(dāng)有post的時(shí)候,才會(huì)被喚醒鲤孵。
(3)由另一線程(或者另一個(gè)進(jìn)程中的某個(gè)線程)向內(nèi)核發(fā)送這個(gè)端口的msg后壶栋,trap狀態(tài)就會(huì)被喚醒,RunLoop就繼續(xù)工作
RunLoop的實(shí)現(xiàn)
// 底層的實(shí)現(xiàn)函數(shù)
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled){
// 配置RunLoop的Mode
SetupCFRunLoopMode()
// 通知 Observers 將要進(jìn)入 Loop
__CFRunLoopDoObservers(kCFRunLoopEntry);
// 通過(guò)GCD設(shè)置RunLoop的超時(shí)時(shí)間
SetupThisRunLoopRunTimeoutTimer();
// RunLoop開(kāi)始處理事件 do while 循環(huán)
do {
// 通知 Observers 將執(zhí)行timer
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
// 通知 Observers 將執(zhí)行Source0
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
// 執(zhí)行blocks
__CFRunLoopDoBlocks();
// 執(zhí)行Source0
__CFRunLoopDoSource0();
// 問(wèn) GCD 主線程有沒(méi)有需要執(zhí)行的東西
CheckIfExistMessagesInMainDispatchQueue();
// 通知 Observers 將進(jìn)入睡眠
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
/* 指定 喚醒端口
監(jiān)聽(tīng) mach_msg 會(huì)停在這里
進(jìn)入 mach_msg_trap 狀態(tài)
睡眠中...
*/
var wakeUpPort = SleepAndWaitForWakingUpPorts();
// 接收到 消息 通知Observers RunLoop被喚醒了
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// 處理事件
if (wakeUpPort == timerPort) {
// 喚醒端口是 timerPort 執(zhí)行timer回調(diào) /* DOES CALLOUT */
__CFRunLoopDoTimers();
} else if (wakeUpPort == mainDispatchQueuePort) {
// 喚醒端口 執(zhí)行mainQueue里面的調(diào)用
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
} else {
// 喚醒端口 執(zhí)行Source1回調(diào)
__CFRunLoopDoSource1();
}
// 執(zhí)行 blocks
__CFRunLoopDoBlocks()普监;
// 當(dāng)事件處理完了贵试、被強(qiáng)制停止了琉兜、超時(shí)了、Mode是空的時(shí)候就會(huì)退出 循環(huán)
} while (!stop && isStopped !timeout && !ModeIsEmpty );
// 通知 Observers 將退出Loop
__CFRunLoopDoObservers(kCFRunLoopExit);
}
其中var wakeUpPort = SleepAndWaitForWakingUpPorts();
這句偽代碼可以看作是RunLoop的核心锡移。內(nèi)部實(shí)現(xiàn)簡(jiǎn)化為這樣:先調(diào)用__CFRunLoopServiceMachPort() ——> 里面會(huì)調(diào)用mach_msg()
函數(shù) 然后會(huì)卡在這里呕童,等待接收消息來(lái)喚醒RunLoop。直到下面的某個(gè)條件被觸發(fā)才被喚醒:
time_out 超時(shí)時(shí)間到了
有一個(gè)Source事件
timer的時(shí)間到了
RunLoop 調(diào)用mach_msg()
函數(shù)去接收消息淆珊,如果沒(méi)有其他 mach_port
發(fā)送消息過(guò)來(lái)夺饲,內(nèi)核就會(huì)將線程置于等待狀態(tài),直到接收到msg施符。就好比我們?cè)谝粋€(gè)函數(shù)中往声,調(diào)用了scanf()函數(shù)來(lái)接收輸入一樣,只有收到了輸入信息戳吝,代碼才能繼續(xù)向下執(zhí)行浩销,否則會(huì)一直卡在那里。
- GCD 和 RunLoop
在RunLoop的內(nèi)部實(shí)現(xiàn)中听哭,用到了很多GCD的東西慢洋。比如剛剛開(kāi)始run的時(shí)候,通過(guò)DISPATCH_SOURCE_TYPE_TIMER
該類型的dispatch_source
設(shè)置了RunLoop的超時(shí)時(shí)間陆盘。還可以在上面RunLoop實(shí)現(xiàn)的偽代碼中看到__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
普筹,只要是dispatch到main_queue的,CoreFoundation 都會(huì)調(diào)用這個(gè)函數(shù)隘马,之后太防,libdispatch.dylib 就會(huì)執(zhí)行回調(diào)。
RunLoop實(shí)踐
-
AFNetworking中RunLoop的創(chuàng)建
在AFN中當(dāng)使用 NSURLConnection 去執(zhí)行網(wǎng)絡(luò)操作的時(shí)候酸员,會(huì)遇到還沒(méi)有收到服務(wù)器的回調(diào)蜒车,線程就已經(jīng)退出了。為了解決這一問(wèn)題幔嗦,作者使用到了RunLoop酿愧。下面是AFN中的一段代碼:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
上面這段代碼在AFURLConnectionOperation.m
中的 162 行。
這是創(chuàng)建一個(gè)常駐服務(wù)線程的好方法崭添。比如寓娩,當(dāng)我們的程序要提供語(yǔ)音服務(wù)的時(shí)候,就可以創(chuàng)建一個(gè)專門(mén)為語(yǔ)音功能服務(wù)的線程呼渣,當(dāng)需要語(yǔ)音服務(wù)的時(shí)候,這個(gè)線程就可以來(lái)執(zhí)行寞埠。
- 一個(gè)TableView延遲加載圖片的新思路
當(dāng)cell上有需要從網(wǎng)絡(luò)獲取的圖片的時(shí)候屁置,我們滾動(dòng)tableView,異步線程會(huì)去加載圖片仁连,加載完成后主線程就會(huì)設(shè)置cell的圖片蓝角,這個(gè)時(shí)候就會(huì)出現(xiàn)卡的現(xiàn)象阱穗。一般的解決方案是調(diào)用tableView的代理方法,判斷tableView是否正在滑動(dòng)使鹅,如果在滑動(dòng)揪阶,就不設(shè)置圖片,等停止滑動(dòng)后再去設(shè)置cell的圖片患朱。用Runloop能更簡(jiǎn)單的解決這個(gè)問(wèn)題鲁僚。我們可以根據(jù)RunLoop不同Mode下,執(zhí)行不同的事件來(lái)解決這個(gè)問(wèn)題思路如下:當(dāng)設(shè)置圖片的時(shí)候裁厅,讓其在 CFRunLoopDefaultMode 下進(jìn)行冰沙。當(dāng)滾動(dòng)tableView的時(shí)候,RunLoop是在 UITrackingRunLoopMode 這個(gè)Mode下执虹,就不會(huì)設(shè)置圖片拓挥,當(dāng)停止的時(shí)候,就會(huì)設(shè)置圖片袋励。
UIImage *downloadedImage = ...;
[self.avatarImageView performSelector:@selector(setImage:)
withObject:downloadedImage
afterDelay:0
inModes:@[NSDefaultRunLoopMode]];
- 讓Crash的App回光返照
App崩潰的發(fā)生分兩種情況:
(1) program received signal:SIGABRT SIGABRT
一般是過(guò)度release 或者 發(fā)送 unrecogized selector導(dǎo)致侥啤。
(2) EXC_BAD_ACCESS
是訪問(wèn)已被釋放的內(nèi)存導(dǎo)致,野指針錯(cuò)誤。
由 SIGABRT 引起的Crash 是系統(tǒng)發(fā)這個(gè)signal給App茬故,程序收到這個(gè)signal后盖灸,就會(huì)把主線程的RunLoop殺死,程序就Crash了 該例只針對(duì) SIGABRT引起的Crash有效均牢。
- Signal: 是Unix糠雨、類Unix等操作系統(tǒng)中進(jìn)程間通訊的一種方式,用來(lái)通知一個(gè)事件發(fā)生徘跪。當(dāng)一個(gè)singal發(fā)送給進(jìn)程甘邀,操作系統(tǒng)就會(huì)中斷進(jìn)程的正常控制流程垮庐,如果在進(jìn)程中定義了信號(hào)的處理函數(shù)松邪,那么這個(gè)函數(shù)就會(huì)被執(zhí)行,因此我們可以注冊(cè)signal哨查,并指定收到signal后要執(zhí)行的函數(shù)
為了讓App回光返照逗抑,我們需要來(lái)捕獲 libsystem_sim_c.dylib
調(diào)用 abort() 函數(shù)發(fā)出的程序終止信號(hào),然后讓其執(zhí)行我們定義的處理signal的方法寒亥。在方法中邮府,我們需要開(kāi)啟一個(gè)RunLoop,保持主線程不退出。
// 創(chuàng)建RunLoop
CFRunLoopRef runLoop = CFRunLoopGetCurrent();
// 設(shè)置Mode
NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runLoop));
// 彈窗告知 程序掛了
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"程序崩潰了" message:@"崩潰信息" delegate:nil cancelButtonTitle:@"取消" otherButtonTitles:nil];
[alertView show];
while (1) {
for (NSString *mode in allModes) {
// 快速的切換 Mode 就能處理滾動(dòng)溉奕、點(diǎn)擊等事件
CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
}
}
備注
有哪些地方理解的不對(duì)褂傀,希望大神們能夠指出,感激不盡加勤。