1. CFRunloopRef
CFRunloopRef 是純 C 的函數(shù)士嚎,而 NSRunloop 僅僅是 CFRunloopRef 的 OC 封裝篱竭,沒有增加額外的功能毙籽,因此主要分析 CFRunloopRef。蘋果已經(jīng)開源了 CFRunloop 源代碼虫啥。
從代碼可以看出 CFRunloopRef 其實是 __CFRunloop 這個結(jié)構(gòu)體的指針蔚约。
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;
CFTypeRef _counterpart;
};
rl->_blocks_head
rl->_commonModes
從代碼的執(zhí)行順序 CFRunLoopRun() / CFRunLoopRunInMode() -> CFRunLoopRunSpecific() -> __CFRunloopRun() 可知 RunLoop 的核心方法是 __CFRunloopRun()。
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
為了方便閱讀不再直接貼源代碼涂籽,放一段偽代碼:
int32_t __CFRunLoopRun()
{
// 通知即將進入 runloop
__CFRunLoopDoObservers(KCFRunLoopEntry);
do
{
// 通知將要處理 timer 和 source
__CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
__CFRunLoopDoObservers(kCFRunLoopBeforeSources);
// 處理非延遲的主線程調(diào)用
__CFRunLoopDoBlocks();
// 處理 Source0 事件
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(...);
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);
}
現(xiàn)在只要了解上面的偽代碼知道核心的方法 __CFRunLoopRun() 內(nèi)部其實是一個 do while 循環(huán),這也正是 Runloop 運行的本質(zhì)柳骄。執(zhí)行這個函數(shù)以后就一直處于 “等待-處理” 的循環(huán)之中团赏,直到循環(huán)結(jié)束。只是不同于我們寫的循環(huán)耐薯,它在休眠時幾乎不會占用系統(tǒng)資源舔清,當然這是由于系統(tǒng)內(nèi)核負責實現(xiàn)的,也是 Runloop 精華所在曲初。
隨著 Swift 的開源体谒,蘋果也維護著一個 Swift 版本的跨平臺 CoreFoundation,除了 mac 平臺臼婆,它還適配了 Linux 和 Windows 平臺抒痒。
下圖描述了 Runloop 運行流程(基本描述了上面 Runloop 的核心流程,當然可以查看官方 The Run Loop Sequence of Events 描述):
需要注意的是黃色區(qū)域的消息處理中并不包含 source0颁褂,因為它在循環(huán)開始之初就被處理了故响,之后的循環(huán)中不再處理。
整個流程其實就是一種 Event Loop 的實現(xiàn)颁独,其他平臺均有類似的實現(xiàn)彩届,只是名稱不同。
既然 RunLoop 是一個消息循環(huán)誓酒,誰來管理和運行 Runloop 樟蠕?那么它接收什么類型的消息?休眠過程是怎么樣的 靠柑?如何保證休眠時不占用系統(tǒng)資源 寨辩?如何處理這些消息以及何時退出循環(huán)?還有一系列問題需要解開歼冰。
注意:盡管 CFRunLoopPerformBlock() 在上圖中作為喚醒機制(手動)有所體現(xiàn)靡狞,但事實上執(zhí)行 CFRunLoopPerformBlock() 只是入隊,下次 RunLoop 運行才會執(zhí)行停巷,而如果需要立即執(zhí)行則必須調(diào)用 CFRunLoopWakeUp()耍攘。
2. Runloop Mode
從源碼很容易看出榕栏,每次運行 __CFRunLoopRun() 函數(shù)時必須指定 Mode,Runloop 總是運行在某種特定的 CFRunLoopModeRef 下蕾各。
而通過 CFRunloopRef 對應(yīng)的結(jié)構(gòu)體 __CFRunLoop 的定義可以很容易知道每種 Runloop 都可以包含若干個 Mode扒磁,每個 Mode 又包含 Source/Timer/Observer。
struct __CFRunLoop {
...
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
...
};
struct __CFRunLoopMode {
...
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
...
};
每次調(diào)用 __CFRunLoopRun() 時指定的 Mode 是 _currentMode式曲,當切換 Mode 時必須退出當前 Mode妨托,然后重新進入 Runloop 以保證不同 Mode 的Source/Timer/Observer互不影響。
系統(tǒng)提供的 Mode 有
- kCFRunLoopCommonModes(NSRunLoopCommonModes)
- kCFRunLoopDefaultMode(NSDefaultRunLoopMode)
- UITrackingRunLoopMode吝羞。
進入 iOS 程序默認不做任何操作就處于 NSDefaultRunLoopMode 中兰伤,此時滑動視圖,主線程就切換 Runloop 到 UITrackingRunLoopMode钧排,不再接受其他事件操作敦腔,除非你將其他 Source/Timer 設(shè)置到 UITrackingRunLoopMode 下。
NSRunLoopCommonModes 并不是某種具體的 Mode恨溜,而是一種模式組合符衔,在 iOS 系統(tǒng)中默認包含了 NSDefaultRunLoopMode 和 UITrackingRunLoopMode。
注意:并不是 Runloop 會運行在 kCFRunLoopCommonModes 這種模式下糟袁,而是相當于分別注冊了 NSDefaultRunLoopMode 和 UITrackingRunLoopMode判族。
當然你也可以通過調(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 運行流程中起到什么作用呢槐臀?
3. Source
Run Loop 處理兩大類事件源:Timer Source 和 Input Source(包括 performSelector** 方法簇锄蹂、Port 或者自定義 Input Source)氓仲,每個事件源都會綁定在 Run Loop 的某個特定模式 mode 上,而且只有 RunLoop 在這個模式運行的時候才會觸發(fā)該 Timer 和 Input Source得糜。
首先看一下官方 Runloop 結(jié)構(gòu)圖(注意下圖右側(cè)的 Input Source Port 和前面流程圖中的 Source0 并不對應(yīng)敬扛,而是對應(yīng) Source1。當然 Source0 是 Input Source 中的一類朝抖,Input Source 還包括 Custom Input Source啥箭,由其他線程手動發(fā)出。Source1 和 Timer 都屬于端口事件源治宣,不同的是所有的 Timer 都共用一個端口 "Mode Timer Port"急侥,而每個 Source1 都有不同的對應(yīng)端口):
結(jié)合前面 RunLoop 核心運行流程可以看出 Source0(負責 App 內(nèi)部事件砌滞,由 App 負責管理觸發(fā),例如 UITouch 事件) 和 Timer (又叫 Timer Source坏怪,基于時間的觸發(fā)器贝润,上層對應(yīng)NSTimer) 是兩個不同的 Runloop 事件源,RunLoop 被這些事件喚醒之后就會處理并調(diào)用事件處理方法(CFRunLoopTimerRef 和 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)默認定義了兩種實現(xiàn)百拓,如果有必要開發(fā)人員也可以自定義叶撒,官方文檔。
4. 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ù)雜耐版,它相當于消息循環(huán)中的一個監(jiān)聽器祠够,隨時通知外部當前 RunLoop 的運行狀態(tài)(它包含一個函數(shù)指針 _callout 將當前狀態(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
};
5. Call out
開發(fā)過程中粪牲,無論是 Observer 的狀態(tài)通知還是 Timer古瓤、Source 的處理,幾乎所有的操作都是通過 Call out 進行回調(diào)的腺阳,而系統(tǒng)在回調(diào)時通常使用如下幾個函數(shù)進行回調(diào)落君,換句話說你的代碼其實最終都是通過下面幾個函數(shù)來負責調(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)用):
6. RunLoop 休眠
對于 Event Loop 而言焙蚓,RunLoop 最核心的事情就是保證線程在沒有消息時休眠以避免占用系統(tǒng)資源纹冤,有消息時能夠及時喚醒。
RunLoop 的這個機制完全依靠系統(tǒng)內(nèi)核來完成购公,具體來說是蘋果操作系統(tǒng)核心組件 Darwin 中的 Mach 來完成的萌京。可以從下圖最底層 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ù):
/**
* 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(),這相當于一個系統(tǒng)調(diào)用吧趣,會觸發(fā)內(nèi)核狀態(tài)切換法竞。當程序靜止時,RunLoop 停留在__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy)强挫,而這個函數(shù)內(nèi)部就是調(diào)用了 mach_msg() 讓程序處于休眠狀態(tài)岔霸。
7. Runloop 與線程的關(guān)系
Runloop 是基于 pthread 進行管理的,pthread 是基于 c 的跨平臺多線程操作底層 API俯渤。它是 mach thread 的上層封裝(可以參見 Kernel Programming Guide)呆细,和 NSThread 一一對應(yīng)。
蘋果沒有開放直接創(chuàng)建 Runloop 的接口八匠,如果需要絮爷,通常調(diào)用 CFRunLoopGetMain() 和 CFRunLoopGetCurrent() 兩個方法來獲取。
通過代碼不難發(fā)現(xiàn)梨树,只有當我們使用線程的方法主動 get 時才會在第一次創(chuàng)建該線程的Runloop坑夯,同時將它保存在全局的字典中(線程和 Runloop 一一對應(yīng)),默認情況下線程并不會創(chuàng)建 Runloop(主線程的 Runloop 比較特殊抡四,任何線程創(chuàng)建之前都會保證主線程的已經(jīng)存在)柜蜈,同時在線程結(jié)束的時也會銷毀對應(yīng)的 Runloop。
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
static CFRunLoopRef __main = NULL; // no retain needed
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main;
}
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
NSRunloop 默認提供了三個常用的 run 方法:
- (void)run;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
- (void)runUntilDate:(NSDate *)limitDate;
- run 方法對應(yīng)上面 CFRunloopRef 中的 CFRunLoopRun() 并不會退出指巡,除非調(diào)用 CFRunLoopStop()淑履。通常如果想要永遠不會退出 RunLoop 才會使用此方法,否則可以使用 runUntilDate藻雪。
- runMode:beforeDate: 則對應(yīng) CFRunLoopRunInMode(mode, limiteDate, true) 方法秘噪,只執(zhí)行一次,執(zhí)行完就退出勉耀。通常用于手動控制 RunLoop指煎。
- runUntilDate: 方法其實是 CFRunLoopRunInMode(kCFRunLoopDefaultMode, limiteDate, false),執(zhí)行完并不會退出瑰排,繼續(xù)下一次 RunLoop 直到 timeout贯要。
8. RunLoop應(yīng)用
1暖侨、NSTimer
前面提到的 Timer Source 作為事件源椭住,事實上它的上層對應(yīng)就是 NSTimer(其實就是 CFRunloopTimerRef,底層基于使用 mk_timer 實現(xiàn))字逗,甚至很多開發(fā)者接觸 RunLoop 還是從 NSTimer 開始的京郑。
其實 NSTimer 定時器的觸發(fā)正是基于 RunLoop 運行的宅广,所以使用 NSTimer 之前必須注冊到 RunLoop。但是 RunLoop 為了節(jié)省資源并不會在非常準確的時間點調(diào)用定時器些举,如果一個任務(wù)執(zhí)行時間較長跟狱,那么當錯過一個時間點后只能等到下一個時間點執(zhí)行,并不會延后執(zhí)行(NSTimer 提供了一個 tolerance 屬性用于設(shè)置寬容度户魏,如果確實想要 NSTimer 盡可能的準確驶臊,可以設(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
schedued 方式不僅創(chuàng)建一個定時器纵寝,而且會自動以 NSDefaultRunLoopMode 添加到當前線程 RunLoop 中,不添加到 RunLoop 中的 NSTimer 是無法正常工作的星立。
同時注意爽茴,如果觸發(fā)滾動事件,NSDefaultRunLoopMode 下 NSTimer 是無法正常工作的绰垂,但將 NSDefaultRunLoopMode 改為 NSRunLoopCommonModes 則可以正常工作室奏,這也解釋了前面介紹的 Mode 內(nèi)容。
@interface MyViewController ()
@property (nonatomic, weak) NSTimer * timer1;
@property (nonatomic, weak) NSTimer * timer2;
@end
- (void)viewDidLoad
{
[super viewDidLoad];
self.timer1 = [NSTimer scheduledTimerWithTimeInterval:...];
NSTimer * tempTimer = [NSTimer timerWithTimeInterval:...];
// 如果不把 tempTimer 添加到 RunLoop 中是無法正常工作的
[[NSRunLoop currentRunLoop] addTimer:tempTimer forMode:NSDefaultRunLoopMode];
self.timer2 = tempTimer;
}
注意上面的 timer1 和 timer2 并沒有強引用劲装,對于其他的對象而言窍奋,執(zhí)行完 viewDidLoad 方法后的的一個 RunLoop 運行結(jié)束,二者應(yīng)該會被釋放酱畅,但事實上二者并沒有被釋放琳袄。
為了確保定時器正常運轉(zhuǎn),當加入到 RunLoop 以后系統(tǒng)會對 NSTimer 執(zhí)行一次 retain 操作纺酸。
特別注意:tempTimer 創(chuàng)建時并沒直接賦值給 timer2窖逗,原因是 timer2 是 weak 屬性,timerWithXXX: 方法創(chuàng)建的 NSTimer 默認并沒有加入 RunLoop餐蔬,如果直接賦值給 timer2 會被立即釋放碎紊,只有加入 RunLoop 以后才可以將引用指向 timer2。
但是即使使用了弱引用樊诺,MyViewController 對象也無法正常釋放
創(chuàng)建 NSTimer 時指定了 target : self仗考,導(dǎo)致 NSTimer 對象 對 self 有一個強引用。
解決這個問題的方法通常有兩種:
①词爬、將 target 分離出來獨立成一個對象秃嗜,在對象內(nèi)創(chuàng)建 NSTimer 并將對象本身作為 NSTimer 的 target,Controller 通過這個對象間接使用 NSTimer;
②锅锨、增加 NSTimer 分類叽赊,讓 NSTimer 自身作為 target,同時可以將操作 selector 封裝到 block 中必搞。后者相對優(yōu)雅必指,也是目前使用較多的方案,例如:NSTimer+Block恕洲。
顯然蘋果也認識到了這個問題塔橡,如果你確保工程只支持 iOS 10 運行就可以使用iOS 10 新增的系統(tǒng)級 block方案。
NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
}];
使用上面第 ② 種方法可以解決控制器無法釋放的問題霜第,但是會發(fā)現(xiàn)即使控制器被釋放了兩個定時器仍然正常運行谱邪,要解決這個問題就需要調(diào)用 NSTimer 的 invalidate 方法(注意:一次性的定時器執(zhí)行完操作后會自動調(diào)用 invalidate 方法)。
- (void)dealloc
{
[self.timer1 invalidate];
[self.timer2 invalidate];
}
其實和定時器相關(guān)的另一個問題大家也經(jīng)常碰到庶诡,那就是 NSTimer 不是一種實時機制惦银。官方文檔明確說明:
在一個循環(huán)中,如果 RunLoop 沒有被識別(這個時間大概在 50-100ms)末誓,或者說 currentRunLoop 在執(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 "MyViewController.h"
@interface MyViewController ()
@property (nonatomic, weak) NSTimer * timer;
@property (nonatomic, strong) NSThread * thread;
@end
@implementation MyViewController
- (void)dealloc
{
[self.timer invalidate]; // 取消定時器
}
- (void)viewDidLoad
{
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self
selector:@selector(performTask)
object:nil];
[self.thread start];
}
- (void)performTask
{
// 使用下面的方式創(chuàng)建定時器雖然會自動加入到當前線程的 RunLoop 中,但是除了主線程外其他線程的 RunLoop 默認是不會運行的蹂安,必須手動調(diào)用
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
if ([NSThread currentThread].isCancelled) {
//[NSObject cancelPreviousPerformRequestsWithTarget:weakSelf selector:@selector(longTimeTask) object:nil];
//[NSThread exit];
[weakSelf.timer invalidate];
}
NSLog(@"111111111");
}];
NSLog(@"runloop before performSelector:%@",[NSRunLoop currentRunLoop]);
// 區(qū)分直接調(diào)用和「performSelector:withObject:afterDelay:」區(qū)別,下面的直接調(diào)用無論是否運行RunLoop一樣可以執(zhí)行椭迎,但是后者則不行。
//[self longTimeTask];
[self performSelector:@selector(longTimeTask) withObject:nil afterDelay:2.0];
// 取消當前 RunLoop 中注冊的 @selector(注意:只是當前 RunLoop田盈,所以也只能在當前 RunLoop 中取消)
// [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(longTimeTask) object:nil];
NSLog(@"runloop after performSelector:%@",[NSRunLoop currentRunLoop]);
// 非主線程RunLoop必須手動調(diào)用
[[NSRunLoop currentRunLoop] run];
NSLog(@"注意:如果RunLoop 還在運行中畜号,這里的代碼并不會執(zhí)行,RunLoop 本身就是一個循環(huán).");
}
// 長時間任務(wù):打印 9999 次
- (void)longTimeTask
{
for (int i = 0;i < 9999;++i) {
NSLog(@"%i, %@", i, [NSThread currentThread]);
if ([NSThread currentThread].isCancelled) {
return;
}
}
}
// 取消線程
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.thread cancel];
}
@end
如果運行并且不退出上面的程序會發(fā)現(xiàn)允瞧,前兩秒 NSTimer 可以正常執(zhí)行简软,但是兩秒后由于同一個 RunLoop 中 longTimeTask 循環(huán)操作的執(zhí)行造成定時器跳過了中間執(zhí)行的機會一直到 longTimeTask 循環(huán)完畢蛮拔,這也正說明了 NSTimer 不是實時系統(tǒng)機制的原因。
以上程序還有幾點需要說明一下:
①替饿、NSTimer 會對 target 進行強引用直到任務(wù)結(jié)束或 exit 之后才會釋放语泽。如果上面的程序沒有進行線程 cancel 而終止任務(wù)贸典,則即使關(guān)閉控制器也無法正確釋放视卢。
②、非主線程的 RunLoop 并不會自動運行廊驼。同時注意据过,默認情況下非主線程的 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 然后加入到當前線程 RunLoop(通而過前后兩次打印RunLoop信息可以看到此方法執(zhí)行之后 RunLoop 的 timer 會增加 1 個期虾。類似的還有performSelector:onThread:withObject:afterDelay:原朝,只是它會在另一個線程的 RunLoop 中創(chuàng)建一個 Timer),所以此方法事實上在任務(wù)執(zhí)行完之前會對觸發(fā)對象形成引用镶苞,任務(wù)執(zhí)行完進行釋放(例如上面會對 MyViewController 形成引用喳坠,注意:performSelector: withObject: 等方法則等同于直接調(diào)用,原理與此不同)茂蚓。
④壕鹉、同時上面的代碼也充分說明了 RunLoop 是一個循環(huán)事實,run 方法之后的代碼不會立即執(zhí)行聋涨,直到 RunLoop 退出晾浴。
⑤、上面程序的運行過程中如果突然 dismiss/pop牍白,則程序的實際執(zhí)行過程要分為兩種情況考慮:如果循環(huán)任務(wù) longTimeTask 還沒有開始則會停止 timer 運行(停止了線程中第一個任務(wù))怠肋,然后等待 longTimeTask 執(zhí)行并 break(停止線程中第二個任務(wù))后線程任務(wù)執(zhí)行結(jié)束釋放對控制器的引用;如果循環(huán)任務(wù) longTimeTask 執(zhí)行過程中 dismiss/pop 則 longTimeTask 任務(wù)執(zhí)行結(jié)束淹朋,等待timer 下個周期運行(因為當前線程的 RunLoop 并沒有退出笙各,timer 引用計數(shù)器并不為 0)時檢測到線程取消狀態(tài)則執(zhí)行 invalidate 方法(第二個任務(wù)也結(jié)束了),此時線程釋放對于控制器的引用础芍。
CADisplayLink 默認時是一個執(zhí)行頻率 fps 和屏幕刷新相同的定時器杈抢,它也需要加入到 RunLoop 才能執(zhí)行。
CADisplayLink 同樣是基于 CFRunloopTimerRef 實現(xiàn)仑性,底層使用 mk_timer惶楼。它比 NSTimer 精度更高(盡管 NSTimer 可以修改精度)。不過遇到大任務(wù)它和 NStimer 一樣存在丟幀現(xiàn)象。
通常情況下 CADisaplayLink 用于構(gòu)建幀動畫歼捐,看起來相對更加流暢何陆,而 NSTimer 則有更廣泛的用處。
2豹储、AutoreleasePool
AutoreleasePool是另一個與RunLoop相關(guān)討論較多的話題贷盲。其實從RunLoop源代碼分析,AutoreleasePool與RunLoop并沒有直接的關(guān)系剥扣,之所以將兩個話題放到一起討論最主要的原因是因為在iOS應(yīng)用啟動后會注冊兩個Observer管理和維護AutoreleasePool巩剖。不妨在應(yīng)用程序剛剛啟動時打印currentRunLoop可以看到系統(tǒng)默認注冊了很多個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()向當前的AutoreleasePoolPage增加一個哨兵對象標志創(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)存維護操作(當然你如果需要顯式釋放【例如循環(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)输涕,這里不再一一詳述音婶。
3、UI更新
如果打印App啟動之后的主線程RunLoop可以發(fā)現(xiàn)另外一個callout為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer莱坎,這個監(jiān)聽專門負責UI變化后的更新衣式,比如修改了frame、調(diào)整了UI層級(UIView/CALayer)或者手動設(shè)置了setNeedsDisplay/setNeedsLayout之后就會將這些操作提交到全局容器檐什。而這個Observer監(jiān)聽了主線程RunLoop的即將進入休眠和退出狀態(tài)碴卧,一旦進入這兩種狀態(tài)則會遍歷所有的UI更新并提交進行實際繪制更新。
通常情況下這種方式是完美的乃正,因為除了系統(tǒng)的更新住册,還可以利用setNeedsDisplay等方法手動觸發(fā)下一次RunLoop運行的更新。但是如果當前正在執(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í)行。
4耳幢、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負責各種delegate回調(diào)并在回調(diào)中喚醒delegate內(nèi)部的RunLoop(通常是主線程)來執(zhí)行實際操作修档。
早期版本的AFNetworking庫也是基于NSURLConnection實現(xiàn),為了能夠在后臺接收delegate回調(diào)AFNetworking內(nèi)部創(chuàng)建了一個空的線程并啟動了RunLoop府框,當需要使用這個后臺線程執(zhí)行任務(wù)時AFNetworking通過**performSelector: onThread: **將這個任務(wù)放到后臺線程的RunLoop中吱窝。
9. GCD 和 RunLoop的關(guān)系
在RunLoop的源代碼中可以看到用到了GCD的相關(guān)內(nèi)容,但是RunLoop本身和GCD并沒有直接的關(guān)系迫靖。當調(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ū)動的。
10. 更多 RunLoop 使用
前面看了很多RunLoop的系統(tǒng)應(yīng)用和一些知名第三方庫使用盹牧,那么除了這些究竟在實際開發(fā)過程中我們自己能不能適當?shù)氖褂肦unLoop幫我們做一些事情呢俩垃?
思考這個問題其實只要看RunLoopRef的包含關(guān)系就知道了,RunLoop包含多個Mode汰寓,而它的Mode又是可以自定義的口柳,這么推斷下來其實無論是Source1、Timer還是Observer開發(fā)者都可以利用有滑,但是通常情況下不會自定義Timer跃闹,更不會自定義一個完整的Mode,利用更多的其實是Observer和Mode的切換毛好。
例如很多人都熟悉的使用perfromSelector在默認模式下設(shè)置圖片望艺,防止UITableView滾動卡頓([[UIImageView alloc initWithFrame: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)給出了詳細的流程场靴。
11. 學(xué)習(xí)文章
? 崔江濤(KenshinCui)# iOS刨根問底-深入理解RunLoop
? 一個低調(diào)的iOS開發(fā) # Runloop 事件源