什么是Runloop
Runloop即運(yùn)行循環(huán)启妹。為什么你的APP放在那里不去動(dòng)它课兄,在某個(gè)時(shí)間點(diǎn)去操作它啄清,它還會(huì)給你反饋。就是因?yàn)镽unloop的存在吧碾。
總結(jié)一下凰盔,因?yàn)镽unloop的存在,保證你的程序不會(huì)死倦春。
主要負(fù)責(zé)什么户敬?
- 使程序一直運(yùn)行并接受用戶輸入
- 決定程序在何時(shí)處理一些Event
- 調(diào)用解耦(Message Queue)
- 節(jié)省CPU時(shí)間(沒(méi)事的時(shí)候閑著,有事的時(shí)候處理)
誰(shuí)依賴NSRunloop
- NSTimer
- UIEvent
- autorelease
- NSObject(NSDelaydPerforming)
- NSObject(NSThreadPerformAddtion)
- CADisplayLink
- CATransition
- CAAnimation
- dispatch_get_main_queue()
- AFNetworking(NSURLConnection)
- ...
主線程幾乎所有的函數(shù)都從以下的6個(gè)之1的調(diào)起
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__
構(gòu)成元素
因?yàn)镹SRunloop是對(duì)CFRunloop的封裝睁本,所以這里只看CFRunLoop就可以了尿庐。
CFRunLoopTimer的封裝
系統(tǒng)提供的NSTimer、CADisplayLink呢堰、performSelector等都是對(duì)CFRunLoopTimer的封裝抄瑟。
CFRunLoopSource
Source是RunLoop的數(shù)據(jù)源抽象類(用OC的話來(lái)講就是protocol)。
RunLoop定義了兩個(gè)版本的Source,分別是Source0和Source1枉疼。
- Source0:處理APP內(nèi)部事件皮假、APP自己負(fù)責(zé)管理(觸發(fā)),如UIEvent骂维、CFSocket
- Source1:由RunLoop和內(nèi)核管理惹资,Mach Port驅(qū)動(dòng),如CFMachPort航闺、CFMessagePort
CFRunLoopObserver
觀察者褪测,向外部報(bào)告RunLoop當(dāng)前狀態(tài)的更改
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};
框架中很多機(jī)制都由CFRunLoopObserver觸發(fā),比如CAAnimation
舉例:
self.navigationController pushViewController:<#(nonnull UIViewController *)#> animated:<#(BOOL)#>
當(dāng)程序執(zhí)行完這行代碼時(shí)潦刃,我們可以看到經(jīng)歷push動(dòng)畫(huà)之后侮措,到達(dá)了一個(gè)新的界面。
但其實(shí)并不是執(zhí)行完這行代碼就出現(xiàn)了Push的動(dòng)畫(huà)福铅。
其實(shí)萝毛,執(zhí)行這段代碼時(shí)不會(huì)立刻就掉push動(dòng)畫(huà),而是要RunLoop循環(huán)一圈收集所有的Animation操作滑黔,匯集起來(lái)一起去調(diào)笆包。
CFRunLoopObserver與AutoreleasePool
對(duì)象的釋放并不是在{}括號(hào)結(jié)束环揽。而是稍微延遲了一點(diǎn)。
堆棧如下:
_wrapRunLoopAutoreleasePoolHandler
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
UIKit通過(guò)RunLoopOberser在RunLoop兩次Sleep間對(duì)AutoreleasePool進(jìn)行Pop和Push庵佣,將這次Loop產(chǎn)生的Autorelease對(duì)象釋放歉胶。
也就是RunLoop跑一圈沒(méi)事了就睡,被喚醒了再跑下一圈巴粪,在兩次sleep之間對(duì)自動(dòng)釋放池進(jìn)行釋放通今。
CFRunLoopMode
注意
RunLoop在同一段時(shí)間只能且必須在一種特定Mode下Run。
更換Mode時(shí)肛根,需要停止當(dāng)前Loop辫塌,然后重啟新Mode。
Mode是iOS滑動(dòng)順暢的關(guān)鍵派哲。
類型
- NSDefaultRunLoopMode
默認(rèn)狀態(tài)(空閑狀態(tài))臼氨,比如點(diǎn)擊按鈕都是這個(gè)狀態(tài) - UITrackingRunLoopMode
滑動(dòng)時(shí)的Mode。比如滑動(dòng)UIScrollView時(shí)芭届。 - UIInitializationRunLoopMode
私有的储矩,APP啟動(dòng)時(shí)。就是從iphone桌面點(diǎn)擊APP的圖標(biāo)進(jìn)入APP到第一個(gè)界面展示之前褂乍,在第一個(gè)界面顯示出來(lái)后持隧,UIInitializationRunLoopMode就被切換成了NSDefaultRunLoopMode。 - NSRunLoopCommonModes
它是NSDefaultRunLoopMode和UITrackingRunLoopMode的集合逃片。結(jié)構(gòu)類似于一個(gè)數(shù)組屡拨。在這個(gè)mode下執(zhí)行其實(shí)就是兩個(gè)mode都能執(zhí)行而已。
典型的應(yīng)用場(chǎng)景這樣:當(dāng)前界面有開(kāi)啟一個(gè)NSTimer褥实,并且滑動(dòng)UIScrollView洁仗。正常開(kāi)啟NSTimer后,滑動(dòng)UIScrollView時(shí)它是不滑動(dòng)的性锭。解決辦法就是把這個(gè)timer加入到當(dāng)前的RunLoop,并把RunLoop的mode設(shè)置為NSRunLoopCommonModes叫胖。這樣就可以保證不管你是NSDefaultRunLoopMode里跑草冈,還是UITrackingRunLoopMode里跑,這個(gè)timer都可以執(zhí)行瓮增。
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.0625
target:self
selector:@selector(progressChange)
userInfo:nil
repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
當(dāng)你開(kāi)始滑動(dòng)UIScrollView時(shí)怎棱,RunLoop的mode狀態(tài)變化如下:
NSDefaultRunLoopMode -> UITrackingRunLoopMode -> NSDefaultRunLoopMode
開(kāi)始滑動(dòng)時(shí),第一次mode的切換會(huì)把NSDefaultRunLoopMode停掉绷跑。然后開(kāi)啟新的UITrackingRunLoopMode拳恋。當(dāng)滑動(dòng)停止時(shí),由UITrackingRunLoopMode切換回NSDefaultRunLoopMode砸捏,這時(shí)UITrackingRunLoopMode被停止谬运,又切換回了老的NSDefaultRunLoopMode(這個(gè)老的NSDefaultRunLoopMode應(yīng)該是重新開(kāi)始的)隙赁。
RunLoop和GCD的關(guān)系
RunLoop和GCD的關(guān)系,準(zhǔn)確來(lái)說(shuō)是只要使用了dispatch_get_main_queue()梆暖,就與RunLoop有了關(guān)系伞访。
因?yàn)镚CD中dispatch到main queue的block被分發(fā)到main RunLoop執(zhí)行。
RunLoop的掛起和喚醒
我寫(xiě)了個(gè)demo轰驳,運(yùn)行厚掷,然后點(diǎn)擊debug欄的暫停,查看堆棧级解,如下:
- 指定用于喚醒的mach_port接口
- 調(diào)用mach_msg監(jiān)聽(tīng)喚醒端口冒黑,被喚醒前,系統(tǒng)內(nèi)核將這個(gè)線程掛起勤哗,停留在mach_msg_trap狀態(tài)
- 由另一個(gè)線程(或另一個(gè)進(jìn)程中的某個(gè)線程)向內(nèi)核發(fā)送這個(gè)端口的msg后抡爹,trap狀態(tài)被喚醒,RunLoop繼續(xù)開(kāi)始干活俺陋。
RunLoop迭代執(zhí)行順序
偽代碼:
SetupThisRunLoopRunTimeoutTimer(); //by GCD timer
do{
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks();
__CFRunLoopDoSource0();
CheckIfExistMessagesInMainDispatchQueue(); //GCD
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
var wakeUpPort = SleepAndWaitForWakingUpPorts();
//mach_msg_trap
//Zzz...
//Received mach_msg , wake up
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
//Handle msgs
if(wakeUpPort == timerPort){
__CFRunLoopDoTimers();
}else if(wakeUpPort == mainDispatchQueuePort) {
//GCD
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_()
}else {
__CFRunLoopDoSource1();
}
__CFRunLoopDOBlocks();
}while(!stop && !timeout);
代碼解讀
//首先do..while循環(huán)不能是一個(gè)死循環(huán),所以在這里設(shè)置一個(gè)過(guò)期時(shí)間
//這件事是GCD干的豁延,用來(lái)檢測(cè)do..while循環(huán)跑了多久
SetupThisRunLoopRunTimeoutTimer();
//開(kāi)始跑循環(huán)
do{
//告訴observer我要跑timer了
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
//告訴observer我要跑source了
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
__CFRunLoopDoBlocks();
//程序跑到這里會(huì)查詢Source0有什么消息
__CFRunLoopDoSource0();
//詢問(wèn)GCD你有沒(méi)有存在主線程的東西需要我?guī)湍阏{(diào)
CheckIfExistMessagesInMainDispatchQueue(); //GCD
//告訴observer我要睡了,RunLoop進(jìn)入到掛起狀態(tài)
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
//進(jìn)入trap狀態(tài)腊状,程序跑到這里就卡在這不動(dòng)了诱咏,等待被某個(gè)Port喚醒
var wakeUpPort = SleepAndWaitForWakingUpPorts();
//被喚醒后,告訴observer我被喚醒了
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
//假如是被timer喚醒的
if(wakeUpPort == timerPort){
//就去循環(huán)遍歷和timer有關(guān)的回調(diào)
__CFRunLoopDoTimers();
}else if(wakeUpPort == mainDispatchQueuePort) {
//如果是主線程的GCD把我喚醒的缴挖,那RunLoop就知道GCD要讓它做事了袋狞,然后就取調(diào)GCD的這些事件
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_()
}else {
//如果都不是,就是Source1映屋,Source1是基于Port事件的苟鸯,比如網(wǎng)絡(luò)某個(gè)端口來(lái)數(shù)據(jù)了,就會(huì)把RunLoop喚醒棚点,去對(duì)來(lái)的數(shù)據(jù)進(jìn)行處理
__CFRunLoopDoSource1();
}
__CFRunLoopDoBlocks();
}while(!stop && !timeout);
//判斷條件:有沒(méi)有被外部干掉 && 到了過(guò)期時(shí)間
//如果過(guò)期時(shí)間不手動(dòng)進(jìn)行設(shè)置的話早处,默認(rèn)值是一個(gè)很大的值,可能是Int_Max
AFNetworking是如何玩轉(zhuǎn)RunLoop的
+ (void)networkRequestThreadEntryPoint:(id)_unuserd object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
//為了不讓runloop run起來(lái)沒(méi)事干導(dǎo)致消失
//所以給runloop加了一個(gè)NSMachPort瘫析,給它一個(gè)mode去監(jiān)聽(tīng)
//實(shí)際上port什么也沒(méi)干砌梆,就是讓runloop一直在等,目的就是讓runloop一直活著
//這是一個(gè)創(chuàng)建常駐服務(wù)線程的好方法
NSRunloop *runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runloop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkReuqestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread =
[[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
一個(gè)TableView延遲加載圖片的新思路
以前是怎么解決的贬循?
通過(guò)UITableView的代理方法咸包,判斷如果處于滑動(dòng)狀態(tài)就不去設(shè)置cell上的圖片,如果沒(méi)有處于滑動(dòng)狀態(tài)就取設(shè)置cell上的圖片杖虾。
而現(xiàn)在通過(guò)Runloop就有一個(gè)十分簡(jiǎn)便的方法烂瘫。
//在cell里面把設(shè)置圖片的事情在NSDefaultRunloopMode里面去做。
//當(dāng)主線程的tableview不再滑動(dòng)的時(shí)候就會(huì)去設(shè)置圖片
UIImage *dowloadImage = ...;
[self.iconImageView performSelector:@selector(setImage:) withObject:dowloadImage afterDelay:0 inModes:@[NSDefaultRunloopMode]];
這樣去設(shè)置圖片就簡(jiǎn)便了很多奇适,不用再去判斷tableview的代理方法坟比。代碼也會(huì)很清爽芦鳍。