iOS刨根問底-深入理解RunLoop
概述
RunLoop作為iOS中一個基礎(chǔ)組件和線程有著千絲萬縷的關(guān)系搁料,同時也是很多常見技術(shù)的幕后功臣。盡管在平時多數(shù)開發(fā)者很少直接使用RunLoop腻要,但是理解RunLoop可以幫助開發(fā)者更好的利用多線程編程模型,同時也可以幫助開發(fā)者解答日常開發(fā)中的一些疑惑涝登。本文將從RunLoop源碼著手雄家,結(jié)合RunLoop的實際應(yīng)用來逐步解開它的神秘面紗。
開源的RunloopRef
通常所說的RunLoop指的是NSRunloop或者CFRunloopRef胀滚,CFRunloopRef是純C的函數(shù)趟济,而NSRunloop僅僅是CFRunloopRef的OC封裝,并未提供額外的其他功能咽笼,因此下面主要分析CFRunloopRef顷编,蘋果已經(jīng)開源了CoreFoundation源代碼,因此很容易找到CFRunloop源代碼剑刑。
從代碼可以看出CFRunloopRef其實就是__CFRunloop這個結(jié)構(gòu)體指針(按照OC的思路我們可以將RunLoop看成一個對象)媳纬,這個對象的運行才是我們通常意義上說的運行循環(huán),核心方法是__CFRunloopRun()施掏,為了便于閱讀就不再直接貼源代碼钮惠,放一段偽代碼方便大家閱讀:
int32_t __CFRunLoopRun()
{
// 通知即將進入runloop
__CFRunLoopDoObservers(KCFRunLoopEntry);
do
{
// 通知將要處理timer和source
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
// 處理非延遲的主線程調(diào)用
__CFRunLoopDoBlocks();
// 處理Source0事件
__CFRunLoopDoSource0();
if (sourceHandledThisLoop) {
__CFRunLoopDoBlocks();
}
/// 如果有 Source1 (基于port) 處于 ready 狀態(tài),直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息七芭。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort();
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)素挽。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
// GCD dispatch main queue
CheckIfExistMessagesInMainDispatchQueue();
// 即將進入休眠
__CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
// 等待內(nèi)核mach_msg事件
mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
// 等待。抖苦。毁菱。
// 從等待中醒來
__CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
// 處理因timer的喚醒
if (wakeUpPort == timerPort)
__CFRunLoopDoTimers();
// 處理異步方法喚醒,如dispatch_async
else if (wakeUpPort == mainDispatchQueuePort)
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
// 處理Source1
else
__CFRunLoopDoSource1();
// 再次確保是否有同步的方法需要調(diào)用
__CFRunLoopDoBlocks();
} while (!stop && !timeout);
// 通知即將退出runloop
__CFRunLoopDoObservers(CFRunLoopExit);
}
源代碼盡管不算太長,但是如果不太熟悉的話面對這么一堆不知道做什么的函數(shù)調(diào)用還是會給人一種神秘感锌历。但是現(xiàn)在可以不用逐行閱讀贮庞,后面慢慢解開這層神秘面紗。現(xiàn)在只要了解上面的偽代碼知道核心的方法__CFRunLoopRun內(nèi)部其實是一個do while循環(huán)究西,這也正是Runloop運行的本質(zhì)窗慎。執(zhí)行了這個函數(shù)以后就一直處于“等待-處理”的循環(huán)之中,直到循環(huán)結(jié)束。只是不同于我們自己寫的循環(huán)它在休眠時幾乎不會占用系統(tǒng)資源遮斥,當(dāng)然這是由于系統(tǒng)內(nèi)核負(fù)責(zé)實現(xiàn)的峦失,也是Runloop精華所在。
隨著Swift的開源蘋果也維護了一個Swift版本的跨平臺CoreFoundation版本术吗,除了mac平臺它還是適配了Linux和Windows平臺尉辑。但是鑒于目前很多關(guān)于Runloop的討論都是以O(shè)C版展開的,所以這里也主要分析OC版本较屿。
下圖描述了Runloop運行流程(基本描述了上面Runloop的核心流程隧魄,當(dāng)然可以查看官方The Run Loop Sequence of Events描述):
整個流程并不復(fù)雜(需要注意的就是黃色區(qū)域的消息處理中并不包含source0,因為它在循環(huán)開始之初就會處理)隘蝎,整個流程其實就是一種Event Loop的實現(xiàn)购啄,其他平臺均有類似的實現(xiàn),只是這里叫做Runloop嘱么。但是既然RunLoop是一個消息循環(huán)狮含,誰來管理和運行Runloop?那么它接收什么類型的消息曼振?休眠過程是怎么樣的几迄?如何保證休眠時不占用系統(tǒng)資源?如何處理這些消息以及何時退出循環(huán)拴测?還有一系列問題需要解開乓旗。
注意的是盡管CFRunLoopPerformBlock在上圖中作為喚醒機制有所體現(xiàn),但事實上執(zhí)行CFRunLoopPerformBlock只是入隊集索,下次RunLoop運行才會執(zhí)行屿愚,而如果需要立即執(zhí)行則必須調(diào)用CFRunLoopWakeUp。
Runloop Mode
從源碼很容易看出务荆,Runloop總是運行在某種特定的CFRunLoopModeRef下(每次運行__CFRunLoopRun()函數(shù)時必須指定Mode)妆距。而通過CFRunloopRef對應(yīng)結(jié)構(gòu)體的定義可以很容易知道每種Runloop都可以包含若干個Mode,每個Mode又包含Source/Timer/Observer函匕。每次調(diào)用Runloop的主函數(shù)__CFRunLoopRun()時必須指定一種Mode娱据,這個Mode稱為** _currentMode**,當(dāng)切換Mode時必須退出當(dāng)前Mode盅惜,然后重新進入Runloop以保證不同Mode的Source/Timer/Observer互不影響中剩。
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};
系統(tǒng)默認(rèn)提供的Run Loop Modes有kCFRunLoopDefaultMode(NSDefaultRunLoopMode)和UITrackingRunLoopMode,需要切換到對應(yīng)的Mode時只需要傳入對應(yīng)的名稱即可抒寂。前者是系統(tǒng)默認(rèn)的Runloop Mode结啼,例如進入iOS程序默認(rèn)不做任何操作就處于這種Mode中,此時滑動UIScrollView屈芜,主線程就切換Runloop到到UITrackingRunLoopMode郊愧,不再接受其他事件操作(除非你將其他Source/Timer設(shè)置到UITrackingRunLoopMode下)朴译。
但是對于開發(fā)者而言經(jīng)常用到的Mode還有一個kCFRunLoopCommonModes(NSRunLoopCommonModes),其實這個并不是某種具體的Mode,而是一種模式組合属铁,在iOS系統(tǒng)中默認(rèn)包含了
** NSDefaultRunLoopMode和 UITrackingRunLoopMode(注意:并不是說Runloop會運行在kCFRunLoopCommonModes這種模式下眠寿,而是相當(dāng)于分別注冊了 NSDefaultRunLoopMode和 UITrackingRunLoopMode。當(dāng)然你也可以通過調(diào)用CFRunLoopAddCommonMode()方法將自定義Mode放到 kCFRunLoopCommonModes**組合)焦蘑。
注意:我們常常還會碰到一些系統(tǒng)框架自定義Mode盯拱,例如Foundation中NSConnectionReplyMode。還有一些系統(tǒng)私有Mode例嘱,例如:GSEventReceiveRunLoopMode接受系統(tǒng)事件坟乾,UIInitializationRunLoopMode App啟動過程中初始化Mode。更多系統(tǒng)或框架Mode查看這里
CFRunLoopRef和CFRunloopMode蝶防、CFRunLoopSourceRef/CFRunloopTimerRef/CFRunLoopObserverRef關(guān)系如下圖:
那么CFRunLoopSourceRef、CFRunLoopTimerRef和CFRunLoopObserverRef究竟是什么明吩?它們在Runloop運行流程中起到什么作用呢间学?
Source
首先看一下官方Runloop結(jié)構(gòu)圖(注意下圖的Input Source Port和前面流程圖中的Source0并不對應(yīng),而是對應(yīng)Source1印荔。Source1和Timer都屬于端口事件源低葫,不同的是所有的Timer都共用一個端口“Mode Timer Port”,而每個Source1都有不同的對應(yīng)端口):
再結(jié)合前面RunLoop核心運行流程可以看出Source0(負(fù)責(zé)App內(nèi)部事件仍律,由App負(fù)責(zé)管理觸發(fā)僵腺,例如UITouch事件)和Timer(又叫Timer Source巩螃,基于時間的觸發(fā)器,上層對應(yīng)NSTimer)是兩個不同的Runloop事件源(當(dāng)然Source0是Input Source中的一類,Input Source還包括Custom Input Source廊散,由其他線程手動發(fā)出),RunLoop被這些事件喚醒之后就會處理并調(diào)用事件處理方法(CFRunLoopTimerRef的回調(diào)指針和CFRunLoopSourceRef均包含對應(yīng)的回調(diào)指針)扯旷。
但是對于CFRunLoopSourceRef除了Source0之外還有另一個版本就是Source1雄妥,Source1除了包含回調(diào)指針外包含一個mach port,和Source0需要手動觸發(fā)不同炕横,Source1可以監(jiān)聽系統(tǒng)端口和其他線程相互發(fā)送消息源内,它能夠主動喚醒RunLoop(由操作系統(tǒng)內(nèi)核進行管理,例如CFMessagePort消息)份殿。官方也指出可以自定義Source膜钓,因此對于CFRunLoopSourceRef來說它更像一種協(xié)議,框架已經(jīng)默認(rèn)定義了兩種實現(xiàn)卿嘲,如果有必要開發(fā)人員也可以自定義颂斜,詳細(xì)情況可以查看官方文檔。
Observer
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
相對來說CFRunloopObserverRef理解起來并不復(fù)雜腔寡,它相當(dāng)于消息循環(huán)中的一個監(jiān)聽器焚鲜,隨時通知外部當(dāng)前RunLoop的運行狀態(tài)(它包含一個函數(shù)指針callout將當(dāng)前狀態(tài)及時告訴觀察者)。具體的Observer狀態(tài)如下:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 進入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將開始Timer處理
kCFRunLoopBeforeSources = (1UL << 2), // 即將開始Source處理
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //從休眠狀態(tài)喚醒
kCFRunLoopExit = (1UL << 7), //退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
Call out
在開發(fā)過程中幾乎所有的操作都是通過Call out進行回調(diào)的(無論是Observer的狀態(tài)通知還是Timer、Source的處理)忿磅,而系統(tǒng)在回調(diào)時通常使用如下幾個函數(shù)進行回調(diào)(換句話說你的代碼其實最終都是通過下面幾個函數(shù)來負(fù)責(zé)調(diào)用的糯彬,即使你自己監(jiān)聽Observer也會先調(diào)用下面的函數(shù)然后間接通知你,所以在調(diào)用堆棧中經(jīng)炒兴看到這些函數(shù)):
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();
例如在控制器的touchBegin中打入斷點查看堆棧(由于UIEvent是Source0撩扒,所以可以看到一個Source0的Call out函數(shù)****CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION****調(diào)用):
RunLoop休眠
其實對于Event Loop而言RunLoop最核心的事情就是保證線程在沒有消息時休眠以避免占用系統(tǒng)資源,有消息時能夠及時喚醒吨些。RunLoop的這個機制完全依靠系統(tǒng)內(nèi)核來完成搓谆,具體來說是蘋果操作系統(tǒng)核心組件Darwin中的Mach來完成的(Darwin是開源的)『朗可以從下圖最底層Kernel中找到Mach:
Mach是Darwin的核心泉手,可以說是內(nèi)核的核心,提供了進程間通信(IPC)偶器、處理器調(diào)度等基礎(chǔ)服務(wù)斩萌。在Mach中,進程屏轰、線程間的通信是以消息的方式來完成的颊郎,消息在兩個Port之間進行傳遞(這也正是Source1之所以稱之為Port-based Source的原因,因為它就是依靠系統(tǒng)發(fā)送消息到指定的Port來觸發(fā)的)霎苗。消息的發(fā)送和接收使用<mach/message.h>中的mach_msg()函數(shù)(事實上蘋果提供的Mach API很少姆吭,并不鼓勵我們直接調(diào)用這些API):
/*
* Routine: mach_msg
* Purpose:
* Send and/or receive a message. If the message operation
* is interrupted, and the user did not request an indication
* of that fact, then restart the appropriate parts of the
* operation silently (trap version does not restart).
*/
__WATCHOS_PROHIBITED __TVOS_PROHIBITED
extern mach_msg_return_t mach_msg(
mach_msg_header_t *msg,
mach_msg_option_t option,
mach_msg_size_t send_size,
mach_msg_size_t rcv_size,
mach_port_name_t rcv_name,
mach_msg_timeout_t timeout,
mach_port_name_t notify);
而mach_msg()的本質(zhì)是一個調(diào)用mach_msg_trap(),這相當(dāng)于一個系統(tǒng)調(diào)用,會觸發(fā)內(nèi)核狀態(tài)切換唁盏。當(dāng)程序靜止時内狸,RunLoop停留在__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy),而這個函數(shù)內(nèi)部就是調(diào)用了mach_msg讓程序處于休眠狀態(tài)。
Runloop和線程的關(guān)系
Runloop是基于pthread進行管理的升敲,pthread是基于c的跨平臺多線程操作底層API答倡。它是mach thread的上層封裝(可以參見Kernel Programming Guide),和NSThread一一對應(yīng)(而NSThread是一套面向?qū)ο蟮腁PI驴党,所以在iOS開發(fā)中我們也幾乎不用直接使用pthread)瘪撇。
蘋果開發(fā)的接口中并沒有直接創(chuàng)建Runloop的接口,如果需要使用Runloop通常CFRunLoopGetMain()和CFRunLoopGetCurrent()兩個方法來獲雀圩(通過上面的源代碼也可以看到倔既,核心邏輯在CFRunLoopGet當(dāng)中),通過代碼并不難發(fā)現(xiàn)其實只有當(dāng)我們使用線程的方法主動get Runloop時才會在第一次創(chuàng)建該線程的Runloop,同時將它保存在全局的Dictionary中(線程和Runloop二者一一對應(yīng))鹏氧,默認(rèn)情況下線程并不會創(chuàng)建Runloop(主線程的Runloop比較特殊渤涌,任何線程創(chuàng)建之前都會保證主線程已經(jīng)存在Runloop),同時在線程結(jié)束的時候也會銷毀對應(yīng)的Runloop把还。
iOS開發(fā)過程中對于開發(fā)者而言更多的使用的是NSRunloop,它默認(rèn)提供了三個常用的run方法:
- (void)run;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
- (void)runUntilDate:(NSDate *)limitDate;
- run方法對應(yīng)上面CFRunloopRef中的CFRunLoopRun并不會退出实蓬,除非調(diào)用CFRunLoopStop();通常如果想要永遠(yuǎn)不會退出RunLoop才會使用此方法茸俭,否則可以使用runUntilDate。
- runMode:beforeDate:則對應(yīng)CFRunLoopRunInMode(mode,limiteDate,true)方法,只執(zhí)行一次安皱,執(zhí)行完就退出调鬓;通常用于手動控制RunLoop(例如在while循環(huán)中)。
- runUntilDate:方法其實是CFRunLoopRunInMode(kCFRunLoopDefaultMode,limiteDate,false)酌伊,執(zhí)行完并不會退出腾窝,繼續(xù)下一次RunLoop直到timeout。
RunLoop應(yīng)用
NSTimer
前面一直提到Timer Source作為事件源居砖,事實上它的上層對應(yīng)就是NSTimer(其實就是CFRunloopTimerRef)這個開發(fā)者經(jīng)常用到的定時器(底層基于使用mk_timer實現(xiàn))虹脯,甚至很多開發(fā)者接觸RunLoop還是從NSTimer開始的。其實NSTimer定時器的觸發(fā)正是基于RunLoop運行的奏候,所以使用NSTimer之前必須注冊到RunLoop循集,但是RunLoop為了節(jié)省資源并不會在非常準(zhǔn)確的時間點調(diào)用定時器,如果一個任務(wù)執(zhí)行時間較長蔗草,那么當(dāng)錯過一個時間點后只能等到下一個時間點執(zhí)行暇榴,并不會延后執(zhí)行(NSTimer提供了一個tolerance屬性用于設(shè)置寬容度,如果確實想要使用NSTimer并且希望盡可能的準(zhǔn)確蕉世,則可以設(shè)置此屬性)。
NSTimer的創(chuàng)建通常有兩種方式婆硬,盡管都是類方法狠轻,一種是timerWithXXX,另一種scheduedTimerWithXXX彬犯。
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
二者最大的區(qū)別就是后者除了創(chuàng)建一個定時器外會自動以NSDefaultRunLoopModeMode添加到當(dāng)前線程RunLoop中向楼,不添加到RunLoop中的NSTimer是無法正常工作的。例如下面的代碼中如果timer2不加入到RunLoop中是無法正常工作的谐区。同時注意如果滾動UIScrollView(UITableView湖蜕、UICollectionview是類似的)二者是無法正常工作的,但是如果將NSDefaultRunLoopMode改為NSRunLoopCommonModes則可以正常工作宋列,這也解釋了前面介紹的Mode內(nèi)容昭抒。
#import "ViewController1.h"
@interface ViewController1 ()
@property (nonatomic,weak) NSTimer *timer1;
@property (nonatomic,weak) NSTimer *timer2;
@end
@implementation ViewController1
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blueColor];
// timer1創(chuàng)建后會自動以NSDefaultRunLoopMode默認(rèn)模式添加到當(dāng)前RunLoop中,所以可以正常工作
self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timeInterval:) userInfo:nil repeats:YES];
NSTimer *tempTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(timeInterval:) userInfo:nil repeats:YES];
// 如果不把timer2添加到RunLoop中是無法正常工作的(注意如果想要在滾動UIScrollView時timer2可以正常工作可以將NSDefaultRunLoopMode改為NSRunLoopCommonModes)
[[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];
self.timer2 = tempTimer;
CGRect rect = [UIScreen mainScreen].bounds;
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectInset(rect, 0, 200)];
[self.view addSubview:scrollView];
UIView *contentView = [[UIView alloc] initWithFrame:CGRectInset(scrollView.bounds, -100, -100)];
contentView.backgroundColor = [UIColor redColor];
[scrollView addSubview:contentView];
scrollView.contentSize = contentView.frame.size;
}
- (void)timeInterval:(NSTimer *)timer {
if (self.timer1 == timer) {
NSLog(@"timer1...");
} else {
NSLog(@"timer2...");
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self dismissViewControllerAnimated:true completion:nil];
}
- (void)dealloc {
NSLog(@"ViewController1 dealloc...");
}
@end
注意上面代碼中UIViewController1對timer1和timer2并沒有強引用炼杖,對于普通的對象而言灭返,執(zhí)行完viewDidLoad方法之后(準(zhǔn)確的說應(yīng)該是執(zhí)行完viewDidLoad方法后的的一個RunLoop運行結(jié)束)二者應(yīng)該會被釋放,但事實上二者并沒有被釋放坤邪。原因是:為了確保定時器正常運轉(zhuǎn)熙含,當(dāng)加入到RunLoop以后系統(tǒng)會對NSTimer執(zhí)行一次retain操作(特別注意:timer2創(chuàng)建時并沒直接賦值給timer2,原因是timer2是weak屬性艇纺,如果直接賦值給timer2會被立即釋放怎静,因為timerWithXXX方法創(chuàng)建的NSTimer默認(rèn)并沒有加入RunLoop邮弹,只有后面加入RunLoop以后才可以將引用指向timer2)。
但是即使使用了弱引用蚓聘,上面的代碼中ViewController1也無法正常釋放腌乡,原因是在創(chuàng)建NSTimer2時指定了target為self,這樣一來造成了timer1和timer2對ViewController1有一個強引用或粮。解決這個問題的方法通常有兩種:一種是將target分離出來獨立成一個對象(在這個對象中創(chuàng)建NSTimer并將對象本身作為NSTimer的target)导饲,控制器通過這個對象間接使用NSTimer;另一種方式的思路仍然是轉(zhuǎn)移target氯材,只是可以直接增加NSTimer擴展(分類)渣锦,讓NSTimer自身做為target,同時可以將操作selector封裝到block中氢哮。后者相對優(yōu)雅袋毙,也是目前使用較多的方案(目前有大量類似的封裝,例如:NSTimer+Block)冗尤。顯然Apple也認(rèn)識到了這個問題听盖,如果你可以確保代碼只在iOS 10下運行就可以使用iOS 10新增的系統(tǒng)級block方案(上面的代碼中已經(jīng)貼出這種方法)。
當(dāng)然使用上面第二種方法可以解決控制器無法釋放的問題裂七,但是會發(fā)現(xiàn)即使控制器被釋放了兩個定時器仍然正常運行皆看,要解決這個問題就需要調(diào)用NSTimer的invalidate方法(注意:無論是重復(fù)執(zhí)行的定時器還是一次性的定時器只要調(diào)用invalidate方法則會變得無效,只是一次性的定時器執(zhí)行完操作后會自動調(diào)用invalidate方法)背零。修改后的代碼如下:
#import "ViewController1.h"
@interface ViewController1 ()
@property (nonatomic,weak) NSTimer *timer1;
@property (nonatomic,weak) NSTimer *timer2;
@end
@implementation ViewController1
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blueColor];
self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer1...");
}];
NSTimer *tempTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer2...");
}];
[[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];
self.timer2 = tempTimer;
CGRect rect = [UIScreen mainScreen].bounds;
UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectInset(rect, 0, 200)];
[self.view addSubview:scrollView];
UIView *contentView = [[UIView alloc] initWithFrame:CGRectInset(scrollView.bounds, -100, -100)];
contentView.backgroundColor = [UIColor redColor];
[scrollView addSubview:contentView];
scrollView.contentSize = contentView.frame.size;
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self dismissViewControllerAnimated:true completion:nil];
}
- (void)dealloc {
[self.timer1 invalidate];
[self.timer2 invalidate];
NSLog(@"ViewController1 dealloc...");
}
@end
其實和定時器相關(guān)的另一個問題大家也經(jīng)常碰到腰吟,那就是NSTimer不是一種實時機制,官方文檔明確說明在一個循環(huán)中如果RunLoop沒有被識別(這個時間大概在50-100ms)或者說當(dāng)前RunLoop在執(zhí)行一個長的call out(例如執(zhí)行某個循環(huán)操作)則NSTimer可能就會存在誤差徙瓶,RunLoop在下一次循環(huán)中繼續(xù)檢查并根據(jù)情況確定是否執(zhí)行(NSTimer的執(zhí)行時間總是固定在一定的時間間隔毛雇,例如1:00:00、1:00:01侦镇、1:00:02灵疮、1:00:05則跳過了第4、5次運行循環(huán))壳繁。
要演示這個問題請看下面的例子(注意:有些示例中可能會讓一個線程中啟動一個定時器震捣,再在主線程啟動一個耗時任務(wù)來演示這個問,如果實際測試可能效果不會太明顯闹炉,因為現(xiàn)在的iPhone都是多核運算的伍派,這樣一來這個問題會變得相對復(fù)雜,因此下面的例子選擇在同一個RunLoop中即加入定時器和執(zhí)行耗時任務(wù))
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic,weak) NSTimer *timer1;
@property (nonatomic,strong) NSThread *thread1;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor redColor];
// 由于下面的方法無法拿到NSThread的引用剩胁,也就無法控制線程的狀態(tài)
//[NSThread detachNewThreadSelector:@selector(performTask) toTarget:self withObject:nil];
self.thread1 = [[NSThread alloc] initWithTarget:self selector:@selector(performTask) object:nil];
[self.thread1 start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self.thread1 cancel];
[self dismissViewControllerAnimated:YES completion:nil];
}
- (void)dealloc {
[self.timer1 invalidate];
NSLog(@"ViewController dealloc.");
}
- (void)performTask {
// 使用下面的方式創(chuàng)建定時器雖然會自動加入到當(dāng)前線程的RunLoop中诉植,但是除了主線程外其他線程的RunLoop默認(rèn)是不會運行的,必須手動調(diào)用
__weak typeof(self) weakSelf = self;
self.timer1 = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
if ([NSThread currentThread].isCancelled) {
//[NSObject cancelPreviousPerformRequestsWithTarget:weakSelf selector:@selector(caculate) object:nil];
//[NSThread exit];
[weakSelf.timer1 invalidate];
}
NSLog(@"timer1...");
}];
NSLog(@"runloop before performSelector:%@",[NSRunLoop currentRunLoop]);
// 區(qū)分直接調(diào)用和「performSelector:withObject:afterDelay:」區(qū)別,下面的直接調(diào)用無論是否運行RunLoop一樣可以執(zhí)行昵观,但是后者則不行晾腔。
//[self caculate];
[self performSelector:@selector(caculate) withObject:nil afterDelay:2.0];
// 取消當(dāng)前RunLoop中注冊測selector(注意:只是當(dāng)前RunLoop舌稀,所以也只能在當(dāng)前RunLoop中取消)
// [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(caculate) object:nil];
NSLog(@"runloop after performSelector:%@",[NSRunLoop currentRunLoop]);
// 非主線程RunLoop必須手動調(diào)用
[[NSRunLoop currentRunLoop] run];
NSLog(@"注意:如果RunLoop不退出(運行中),這里的代碼并不會執(zhí)行灼擂,RunLoop本身就是一個循環(huán).");
}
- (void)caculate {
for (int i = 0;i < 9999;++i) {
NSLog(@"%i,%@",i,[NSThread currentThread]);
if ([NSThread currentThread].isCancelled) {
return;
}
}
}
@end
如果運行并且不退出上面的程序會發(fā)現(xiàn)壁查,前兩秒NSTimer可以正常執(zhí)行,但是兩秒后由于同一個RunLoop中循環(huán)操作的執(zhí)行造成定時器跳過了中間執(zhí)行的機會一直到caculator循環(huán)完畢剔应,這也正說明了NSTimer不是實時系統(tǒng)機制的原因睡腿。
但是以上程序還有幾點需要說明一下:
- NSTimer會對Target進行強引用直到任務(wù)結(jié)束或exit之后才會釋放。如果上面的程序沒有進行線程cancel而終止任務(wù)則及時關(guān)閉控制器也無法正確釋放峻贮。
- 非主線程的RunLoop并不會自動運行(同時注意默認(rèn)情況下非主線程的RunLoop并不會自動創(chuàng)建席怪,直到第一次使用),RunLoop運行必須要在加入NSTimer或Source0纤控、Sourc1挂捻、Observer輸入后運行否則會直接退出。例如上面代碼如果run放到NSTimer創(chuàng)建之前則既不會執(zhí)行定時任務(wù)也不會執(zhí)行循環(huán)運算船万。
- performSelector:withObject:afterDelay:執(zhí)行的本質(zhì)還是通過創(chuàng)建一個NSTimer然后加入到當(dāng)前線程RunLoop(通而過前后兩次打印RunLoop信息可以看到此方法執(zhí)行之后RunLoop的timer會增加1個刻撒。類似的還有performSelector:onThread:withObject:afterDelay:,只是它會在另一個線程的RunLoop中創(chuàng)建一個Timer)耿导,所以此方法事實上在任務(wù)執(zhí)行完之前會對觸發(fā)對象形成引用声怔,任務(wù)執(zhí)行完進行釋放(例如上面會對ViewController形成引用,注意:performSelector: withObject:等方法則等同于直接調(diào)用舱呻,原理與此不同)捧搞。
- 同時上面的代碼也充分說明了RunLoop是一個循環(huán)事實,run方法之后的代碼不會立即執(zhí)行狮荔,直到RunLoop退出。
- 上面程序的運行過程中如果突然dismiss介粘,則程序的實際執(zhí)行過程要分為兩種情況考慮:如果循環(huán)任務(wù)caculate還沒有開始則會在timer1中停止timer1運行(停止了線程中第一個任務(wù))殖氏,然后等待caculate執(zhí)行并break(停止線程中第二個任務(wù))后線程任務(wù)執(zhí)行結(jié)束釋放對控制器的引用;如果循環(huán)任務(wù)caculate執(zhí)行過程中dismiss則caculate任務(wù)執(zhí)行結(jié)束姻采,等待timer1下個周期運行(因為當(dāng)前線程的RunLoop并沒有退出雅采,timer1引用計數(shù)器并不為0)時檢測到線程取消狀態(tài)則執(zhí)行invalidate方法(第二個任務(wù)也結(jié)束了),此時線程釋放對于控制器的引用慨亲。
CADisplayLink是一個執(zhí)行頻率(fps)和屏幕刷新相同(可以修改preferredFramesPerSecond改變刷新頻率)的定時器婚瓜,它也需要加入到RunLoop才能執(zhí)行。與NSTimer類似刑棵,CADisplayLink同樣是基于CFRunloopTimerRef實現(xiàn)巴刻,底層使用mk_timer(可以比較加入到RunLoop前后RunLoop中timer的變化)。和NSTimer相比它精度更高(盡管NSTimer也可以修改精度)蛉签,不過和NStimer類似的是如果遇到大任務(wù)它仍然存在丟幀現(xiàn)象胡陪。通常情況下CADisaplayLink用于構(gòu)建幀動畫沥寥,看起來相對更加流暢,而NSTimer則有更廣泛的用處柠座。
AutoreleasePool
AutoreleasePool是另一個與RunLoop相關(guān)討論較多的話題邑雅。其實從RunLoop源代碼分析,AutoreleasePool與RunLoop并沒有直接的關(guān)系妈经,之所以將兩個話題放到一起討論最主要的原因是因為在iOS應(yīng)用啟動后會注冊兩個Observer管理和維護AutoreleasePool淮野。不妨在應(yīng)用程序剛剛啟動時打印currentRunLoop可以看到系統(tǒng)默認(rèn)注冊了很多個Observer,其中有兩個Observer的callout都是** _ wrapRunLoopWithAutoreleasePoolHandler**吹泡,這兩個是和自動釋放池相關(guān)的兩個監(jiān)聽骤星。
<CFRunLoopObserver 0x6080001246a0 [0x101f81df0]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
'' <CFRunLoopObserver 0x608000124420 [0x101f81df0]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x1020e07ce), context = <CFArray 0x60800004cae0 [0x101f81df0]>{type = mutable-small, count = 0, values = ()}}
第一個Observer會監(jiān)聽RunLoop的進入,它會回調(diào)objc_autoreleasePoolPush()向當(dāng)前的AutoreleasePoolPage增加一個哨兵對象標(biāo)志創(chuàng)建自動釋放池荞胡。這個Observer的order是-2147483647優(yōu)先級最高妈踊,確保發(fā)生在所有回調(diào)操作之前。
第二個Observer會監(jiān)聽RunLoop的進入休眠和即將退出RunLoop兩種狀態(tài)泪漂,在即將進入休眠時會調(diào)用**objc_autoreleasePoolPop() **和 **objc_autoreleasePoolPush() 根據(jù)情況從最新加入的對象一直往前清理直到遇到哨兵對象廊营。而在即將退出RunLoop時會調(diào)用objc_autoreleasePoolPop() **釋放自動自動釋放池內(nèi)對象。這個Observer的order是2147483647萝勤,優(yōu)先級最低露筒,確保發(fā)生在所有回調(diào)操作之后。
主線程的其他操作通常均在這個AutoreleasePool之內(nèi)(main函數(shù)中)敌卓,以盡可能減少內(nèi)存維護操作(當(dāng)然你如果需要顯式釋放【例如循環(huán)】時可以自己創(chuàng)建AutoreleasePool否則一般不需要自己創(chuàng)建)慎式。
其實在應(yīng)用程序啟動后系統(tǒng)還注冊了其他Observer(例如即將進入休眠時執(zhí)行注冊回調(diào)_UIGestureRecognizerUpdateObserver用于手勢處理、回調(diào)為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer用于界面實時繪制更新)和多個Source1(例如context為CFMachPort的Source1用于接收硬件事件響應(yīng)進而分發(fā)到應(yīng)用程序一直到UIEvent)趟径,這里不再一一詳述瘪吏。
UI更新
如果打印App啟動之后的主線程RunLoop可以發(fā)現(xiàn)另外一個callout為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer,這個監(jiān)聽專門負(fù)責(zé)UI變化后的更新蜗巧,比如修改了frame掌眠、調(diào)整了UI層級(UIView/CALayer)或者手動設(shè)置了setNeedsDisplay/setNeedsLayout之后就會將這些操作提交到全局容器。而這個Observer監(jiān)聽了主線程RunLoop的即將進入休眠和退出狀態(tài)幕屹,一旦進入這兩種狀態(tài)則會遍歷所有的UI更新并提交進行實際繪制更新蓝丙。
通常情況下這種方式是完美的,因為除了系統(tǒng)的更新望拖,還可以利用setNeedsDisplay等方法手動觸發(fā)下一次RunLoop運行的更新渺尘。但是如果當(dāng)前正在執(zhí)行大量的邏輯運算可能UI的更新就會比較卡,因此facebook推出了AsyncDisplayKit來解決這個問題说敏。AsyncDisplayKit其實是將UI排版和繪制運算盡可能放到后臺鸥跟,將UI的最終更新操作放到主線程(這一步也必須在主線程完成),同時提供一套類UIView或CALayer的相關(guān)屬性盔沫,盡可能保證開發(fā)者的開發(fā)習(xí)慣锌雀。這個過程中AsyncDisplayKit在主線程RunLoop中增加了一個Observer監(jiān)聽即將進入休眠和退出RunLoop兩種狀態(tài),收到回調(diào)時遍歷隊列中的待處理任務(wù)一一執(zhí)行蚂夕。
NSURLConnection
在前面的網(wǎng)絡(luò)開發(fā)的文章中已經(jīng)介紹過NSURLConnection的使用,一旦啟動NSURLConnection以后就會不斷調(diào)用delegate方法接收數(shù)據(jù)腋逆,這樣一個連續(xù)的的動作正是基于RunLoop來運行婿牍。
一旦NSURLConnection設(shè)置了delegate會立即創(chuàng)建一個線程com.apple.NSURLConnectionLoader,同時內(nèi)部啟動RunLoop并在NSDefaultMode模式下添加4個Source0惩歉。其中CFHTTPCookieStorage用于處理cookie ;CFMultiplexerSource負(fù)責(zé)各種delegate回調(diào)并在回調(diào)中喚醒delegate內(nèi)部的RunLoop(通常是主線程)來執(zhí)行實際操作等脂。
早期版本的AFNetworking庫也是基于NSURLConnection實現(xiàn),為了能夠在后臺接收delegate回調(diào)AFNetworking內(nèi)部創(chuàng)建了一個空的線程并啟動了RunLoop撑蚌,當(dāng)需要使用這個后臺線程執(zhí)行任務(wù)時AFNetworking通過**performSelector: onThread: **將這個任務(wù)放到后臺線程的RunLoop中上遥。
GCD和RunLoop的關(guān)系
在RunLoop的源代碼中可以看到用到了GCD的相關(guān)內(nèi)容,但是RunLoop本身和GCD并沒有直接的關(guān)系争涌。當(dāng)調(diào)用了dispatch_async(dispatch_get_main_queue(), <#^(void)block#>)時libDispatch會向主線程RunLoop發(fā)送消息喚醒RunLoop粉楚,RunLoop從消息中獲取block,并且在CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE回調(diào)里執(zhí)行這個block亮垫。不過這個操作僅限于主線程模软,其他線程dispatch操作是全部由libDispatch驅(qū)動的。
更多RunLoop使用
前面看了很多RunLoop的系統(tǒng)應(yīng)用和一些知名第三方庫使用饮潦,那么除了這些究竟在實際開發(fā)過程中我們自己能不能適當(dāng)?shù)氖褂肦unLoop幫我們做一些事情呢燃异?
思考這個問題其實只要看RunLoopRef的包含關(guān)系就知道了,RunLoop包含多個Mode继蜡,而它的Mode又是可以自定義的回俐,這么推斷下來其實無論是Source1、Timer還是Observer開發(fā)者都可以利用稀并,但是通常情況下不會自定義Timer仅颇,更不會自定義一個完整的Mode,利用更多的其實是Observer和Mode的切換碘举。
例如很多人都熟悉的使用perfromSelector在默認(rèn)模式下設(shè)置圖片忘瓦,防止UITableView滾動卡頓([[UIImageView allocinitWithFrame:CGRectMake(0, 0, 100, 100)] performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode])。還有sunnyxx的UITableView+FDTemplateLayoutCell利用Observer在界面空閑狀態(tài)下計算出UITableViewCell的高度并進行緩存殴俱。再有老譚的PerformanceMonitor關(guān)于iOS實時卡頓監(jiān)控,同樣是利用Observer對RunLoop進行監(jiān)視枚抵。
關(guān)于如何自定義一個Custom Input Source官網(wǎng)給出了詳細(xì)的流程线欲。