零:前言
聲明:本文非原創(chuàng)邪锌,是我在整理自己iOS知識體系時勉躺,閱讀到這篇文章,感覺作者整理的非常好觅丰,就轉(zhuǎn)載到這里方便自己學習饵溅、備忘。
感謝作者崔江濤妇萄,如有侵權(quán)蜕企,請聯(lián)系刪除咬荷。
原文鏈接:https://www.cnblogs.com/kenshincui/p/6823841.html
原文作者:崔江濤KenshinCui
一:概述
RunLoop
作為 iOS 中一個基礎組件和線程有著千絲萬縷的關(guān)系,同時也是很多常見技術(shù)的幕后功臣糖赔。盡管在平時多數(shù)開發(fā)者很少直接使用RunLoop
萍丐,但是理解RunLoop
可以幫助開發(fā)者更好的利用多線程編程模型轩端,同時也可以幫助開發(fā)者解答日常開發(fā)中的一些疑惑放典。本文將從RunLoop
源碼著手,結(jié)合RunLoop
的實際應用來逐步解開它的神秘面紗基茵。
二: 開源的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( /** 5個參數(shù) */ )
{
// 通知即將進入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)資源,當然這是由于系統(tǒng)內(nèi)核負責實現(xiàn)的剪廉,也是Runloop
精華所在娃循。
隨著Swift的開源蘋果也維護了一個Swift版本的跨平臺
CoreFoundation
版本,除了mac平臺它還是適配了 Linux 和 Windows 平臺斗蒋。但是鑒于目前很多關(guān)于 Runloop 的討論都是以OC版展開的捌斧,所以這里也主要分析OC版本笛质。
https://github.com/apple/swift-corelibs-foundation/
下圖描述了Runloop
運行流程±搪欤基本描述了上面Runloop
的核心流程妇押,當然可以查看官方 The RunLoop Sequence of Events描述。
整個流程并不復雜(需要注意的就是黃色區(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 流程圖:
三: Runloop Mode
從源碼很容易看出,Runloop
總是運行在某種特定的CFRunLoopModeRef
下(每次運行__CFRunLoopRun()
函數(shù)時必須指定Mode
)敞斋。而通過CFRunloopRef
對應結(jié)構(gòu)體的定義可以很容易知道每種Runloop
都可以包含若干個Mode
截汪,每個Mode
又包含Source/Timer/Observer
。每次調(diào)用Runloop
的主函數(shù)__CFRunLoopRun()
時必須指定一種Mode
植捎,這個Mode稱為 _currentMode 衙解,當切換Mode
時必須退出當前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;
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
// ----------------------------------------
struct __CFRunLoopMode { // 部分
CFRuntimeBase _base;
/* must have the run loop locked before locking this */
pthread_mutex_t _lock;
CFStringRef _name;
Boolean _stopped;
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
};
系統(tǒng)默認提供的 Run Loop Modes 有kCFRunLoopDefaultMode
(NSDefaultRunLoopMode
)和UITrackingRunLoopMode
蚓峦,需要切換到對應的Mode 時只需要傳入對應的名稱即可。前者是系統(tǒng)默認的 Runloop Mode济锄,例如進入iOS程序默認不做任何操作就處于這種 Mode 中暑椰,此時滑動UIScrollView
,主線程就切換 Runloop 到UITrackingRunLoopMode
荐绝,不再接受其他事件操作(除非你將其他 Source/Timer 設置到UITrackingRunLoopMode
下)一汽。
但是對于開發(fā)者而言經(jīng)常用到的 Mode 還有一個kCFRunLoopCommonModes
(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查看這里??
https://iphonedev.wiki/index.php/CFRunLoop
CFRunLoopRef
和CFRunloopMode
隶债、CFRunLoopSourceRef
/CFRunloopTimerRef
/CFRunLoopObserverRef
關(guān)系如下圖:
一個RunLoop對象(CFRunLoop
)中包含若干個運行模式(CFRunLoopMode
)腾它。而每一個運行模式下又包含若干個輸入源(CFRunLoopSource
)、定時源(CFRunLoopTimer
)死讹、觀察者(CFRunLoopObserver
)。
那么CFRunLoopSourceRef
赞警、CFRunLoopTimerRef
和CFRunLoopObserverRef
究竟是什么?它們在 Runloop 運行流程中起到什么作用呢虏两?
四: Source
首先看一下官方 Runloop 結(jié)構(gòu)圖(注意下圖的 Input Source Port 和前面流程圖中的Source0
并不對應愧旦,而是對應Source1
。Source1
和Timer
都屬于端口事件源定罢,不同的是所有的Timer
都共用一個端口“Mode Timer Port”笤虫,而每個Source1
都有不同的對應端口):
再結(jié)合前面 RunLoop 核心運行流程可以看出Source0
(負責App內(nèi)部事件,由App負責管理觸發(fā)祖凫,例如UITouch
事件)和Timer
(又叫Timer Source
琼蚯,基于時間的觸發(fā)器,上層對應NSTimer
)是兩個不同的 Runloop 事件源(當然Source0
是Input Source
中的一類惠况,Input Source 還包括 Custom Input Source遭庶,由其他線程手動發(fā)出),RunLoop 被這些事件喚醒之后就會處理并調(diào)用事件處理方法(CFRunLoopTimerRef
的回調(diào)指針和CFRunLoopSourceRef
均包含對應的回調(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ā)人員也可以自定義叔扼,詳細情況可以查看官方文檔事哭。
五: Observer
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
};
相對來說CFRunloopObserverRef
理解起來并不復雜,它相當于消息循環(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
};
六: Call out
RunLoop 幾乎所有的操作都是通過Call out
進行回調(diào)的(無論是 Observer 的狀態(tài)通知還是 Timer、Source 的處理)与柑,而系統(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)用):
七: 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)度等基礎服務植榕。在Mach
中,進程尼夺、線程間的通信是以消息的方式來完成的尊残,消息在兩個Port
之間進行傳遞(這也正是Source1
之所以稱之為 Port-based Source 的原因,因為它就是依靠系統(tǒng)發(fā)送消息到指定的Port來觸發(fā)的)淤堵。消息的發(fā)送和接收使用<mach/message.h>
中的mach_msg()
函數(shù)(事實上蘋果提供的 Mach API 很少寝衫,并不鼓勵我們直接調(diào)用這些API):
而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)。
RunLoop 這種有事做事庙睡,沒事休息的機制其實就是用戶態(tài)
和內(nèi)核態(tài)
的互相轉(zhuǎn)化事富。用戶態(tài)
和內(nèi)核態(tài)
在 Linux 和 Unix 系統(tǒng)中,是基本概念乘陪,是操作系統(tǒng)的兩種運行級別统台,他們的權(quán)限不一樣,由于系統(tǒng)的資源是有限的啡邑,比如網(wǎng)絡贱勃、內(nèi)存等,所以為了優(yōu)化性能,降低電量消耗贵扰,提高資源利用率仇穗,所以內(nèi)核底層就這么設計了。
八: Runloop和線程的關(guān)系
Runloop 是基于pthread
進行管理的戚绕,pthread
是基于 C 的跨平臺多線程操作底層API纹坐。它是 mach thread 的上層封裝(可以參見Kernel Programming Guide),和NSThread
一一對應(而NSThread
是一套面向?qū)ο蟮腁PI舞丛,所以在iOS開發(fā)中我們也幾乎不用直接使用pthread
)耘子。
蘋果開發(fā)的接口中并沒有直接創(chuàng)建 Runloop 的接口,如果需要使用 Runloop 通常CFRunLoopGetMain()
和CFRunLoopGetCurrent()
兩個方法來獲惹蚯小(通過上面的源代碼也可以看到谷誓,核心邏輯在_CFRunLoopGet_
當中),通過代碼并不難發(fā)現(xiàn)其實只有當我們使用線程的方法主動 get Runloop 時才會在第一次創(chuàng)建該線程的Runloop,同時將它保存在全局的Dictionary
中(線程和Runloop是一一對應)吨凑,默認情況下線程并不會創(chuàng)建 Runloop(主線程的Runloop比較特殊捍歪,任何線程創(chuàng)建之前都會保證主線程已經(jīng)存在 Runloop),同時在線程結(jié)束的時候也會銷毀對應的Runloop怀骤。
iOS開發(fā)過程中對于開發(fā)者而言更多的使用的是NSRunloop
,它默認提供了三個常用的run方法:
- (void)run;
- (BOOL)runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate;
- (void)runUntilDate:(NSDate *)limitDate;
-
run:
方法對應上面CFRunloopRef
中的CFRunLoopRun
并不會退出费封,除非調(diào)用CFRunLoopStop()
;通常如果想要永遠不會退出 RunLoop 才會使用此方法,否則可以使用runUntilDate:
蒋伦。 -
runMode:beforeDate:
則對應CFRunLoopRunInMode(mode,limiteDate,true)
方法,只執(zhí)行一次,執(zhí)行完就退出焚鹊;通常用于手動控制 RunLoop(例如在while循環(huán)中)痕届。 -
runUntilDate:
方法其實是CFRunLoopRunInMode(kCFRunLoopDefaultMode,limiteDate,false)
,執(zhí)行完并不會退出末患,繼續(xù)下一次RunLoop直到timeout研叫。
九: RunLoop應用
9.1 NSTimer
前面一直提到 Timer Source 作為事件源,事實上它的上層對應就是NSTimer
(其實就是CFRunloopTimerRef
)這個開發(fā)者經(jīng)常用到的定時器(底層基于使用mk_timer
實現(xiàn))璧针,甚至很多開發(fā)者接觸 RunLoop 還是從NSTimer
開始的嚷炉。其實NSTimer
定時器的觸發(fā)正是基于 RunLoop 運行的,所以使用NSTimer
之前必須注冊到 RunLoop探橱,但是 RunLoop 為了節(jié)省資源并不會在非常準確的時間點調(diào)用定時器申屹,如果一個任務執(zhí)行時間較長,那么當錯過一個時間點后只能等到下一個時間點執(zhí)行隧膏,并不會延后執(zhí)行(NSTimer
提供了一個tolerance
屬性用于設置寬容度哗讥,如果確實想要使用NSTimer
并且希望盡可能的準確,則可以設置此屬性)胞枕。
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
添加到當前線程 RunLoop 中队询,不添加到 RunLoop中的NSTimer
是無法正常工作的。例如下面的代碼中如果timer2
不加入到RunLoop 中是無法正常工作的构诚。同時注意如果滾動UIScrollView
(UITableView蚌斩、UICollectionview是類似的)二者是無法正常工作的,但是如果將NSDefaultRunLoopMode
改為NSRunLoopCommonModes
則可以正常工作唤反,這也解釋了前面介紹的 Mode 內(nèi)容凳寺。
@interface BViewController ()
@property (nonatomic, weak) NSTimer *timer1;
@property (nonatomic, weak) NSTimer *timer2;
@end
@implementation BViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.view.backgroundColor = [UIColor blueColor];
// timer1創(chuàng)建后會自動以NSDefaultRunLoopMode默認模式添加到當前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(@"BViewController dealloc...");
}
@end
注意上面代碼中UIViewController
對timer1
和timer2
并沒有強引用彤侍,對于普通的對象而言肠缨,執(zhí)行完viewDidLoad
方法之后(準確的說應該是執(zhí)行完viewDidLoad
方法后的的一個 RunLoop 運行結(jié)束)二者應該會被釋放,但事實上二者并沒有被釋放盏阶。原因是:為了確保定時器正常運轉(zhuǎn)晒奕,當加入到 RunLoop 以后系統(tǒng)會對NSTimer
執(zhí)行一次retain
操作(特別注意??:timer2
創(chuàng)建時并沒直接賦值給timer2
,原因是timer2
是weak
屬性名斟,如果直接賦值給timer2
會被立即釋放脑慧,因為timerWithXXX
方法創(chuàng)建的NSTimer
默認并沒有加入 RunLoop,只有后面加入 RunLoop 以后才可以將引用指向timer2
)砰盐。
但是即使使用了弱引用闷袒,上面的代碼中BViewController
也無法正常釋放,原因是在創(chuàng)建NSTimer2
時指定了target
為self
岩梳,這樣一來造成了timer1
和timer2
對BViewController
有一個強引用囊骤。
解決這個問題的方法通常有兩種:一種是將target
分離出來獨立成一個對象(在這個對象中創(chuàng)建NSTimer
并將對象本身作為NSTimer
的target
),控制器通過這個對象間接使用NSTimer
冀值;另一種方式的思路仍然是轉(zhuǎn)移target
也物,只是可以直接增加NSTimer
擴展(分類),讓NSTimer
自身做為target
列疗,同時可以將操作selector
封裝到block
中滑蚯。后者相對優(yōu)雅,也是目前使用較多的方案抵栈,如果你可以確保代碼只在 iOS 10 后運行就可以使用 iOS 10 新增的系統(tǒng)級block方案(下面的代碼中已經(jīng)貼出這種方法)告材。
當然使用上面第二種方法可以解決控制器無法釋放的問題,但是會發(fā)現(xiàn)即使控制器被釋放了兩個定時器仍然正常運行竭讳,要解決這個問題就需要調(diào)用NSTimer
的invalidate
方法(注意:無論是重復執(zhí)行的定時器還是一次性的定時器只要調(diào)用invalidate
方法則會變得無效创葡,只是一次性的定時器執(zhí)行完操作后會自動調(diào)用invalidate
方法)。修改后的代碼如下:
@implementation BViewController
- (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(@"BViewController dealloc...");
}
@end
其實和定時器相關(guān)的另一個問題大家也經(jīng)常碰到绢慢,那就是NSTimer
不是一種實時機制灿渴,官方文檔明確說明在一個循環(huán)中如果 RunLoop 沒有被識別(這個時間大概在50-100ms)或者說當前 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))焰扳。
要演示這個問題請看下面的例子(注意:有些示例中可能會讓一個線程中啟動一個定時器,再在主線程啟動一個耗時任務來演示這個問误续,如果實際測試可能效果不會太明顯吨悍,因為現(xiàn)在的iPhone都是多核運算的,這樣一來這個問題會變得相對復雜蹋嵌,因此下面的例子選擇在同一個 RunLoop 中即加入定時器和執(zhí)行耗時任務)
@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)建定時器雖然會自動加入到當前線程的RunLoop中,但是除了主線程外其他線程的RunLoop默認是不會運行的栽烂,必須手動調(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];
// 取消當前RunLoop中注冊selector(注意:只是當前RunLoop腺办,所以也只能在當前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)機制的原因躬拢。
但是以上程序還有幾點需要說明一下:
- 1.
NSTimer
會對Target
進行強引用直到任務結(jié)束或exit
之后才會釋放驻子。如果上面的程序沒有進行線程cancel
而終止任務則及時關(guān)閉控制器也無法正確釋放。 - 2.非主線程的 RunLoop 并不會自動運行(同時注意默認情況下非主線程的RunLoop并不會自動創(chuàng)建估灿,直到第一次使用),RunLoop 運行必須要在加入
NSTimer
或Source0
缤剧、Sourc1
馅袁、Observer
輸入后運行否則會直接退出。例如上面代碼如果run
放到NSTimer
創(chuàng)建之前則既不會執(zhí)行定時任務也不會執(zhí)行循環(huán)運算荒辕。 - 3.
performSelector:withObject:afterDelay:
執(zhí)行的本質(zhì)還是通過創(chuàng)建一個NSTimer
然后加入到當前線程RunLoop(通而過前后兩次打印 RunLoop 信息可以看到此方法執(zhí)行之后RunLoop的 timer 會增加1個汗销。類似的還有performSelector:onThread:withObject:afterDelay:
,只是它會在另一個線程的RunLoop中創(chuàng)建一個Timer)抵窒,所以此方法事實上在任務執(zhí)行完之前會對觸發(fā)對象形成引用弛针,任務執(zhí)行完進行釋放(例如上面會對ViewController
形成引用,注意:performSelector:withObject:
等方法則等同于直接調(diào)用李皇,原理與此不同)削茁。 - 4.同時上面的代碼也充分說明了RunLoop是一個循環(huán)事實宙枷,run方法之后的代碼不會立即執(zhí)行,直到RunLoop退出茧跋。
- 5.上面程序的運行過程中如果突然
dismiss
慰丛,則程序的實際執(zhí)行過程要分為兩種情況考慮:如果循環(huán)任務caculate
還沒有開始則會在timer1
中停止timer1
運行(停止了線程中第一個任務),然后等待caculate
執(zhí)行并break
(停止線程中第二個任務)后線程任務執(zhí)行結(jié)束釋放對控制器的引用瘾杭;如果循環(huán)任務caculate
執(zhí)行過程中dismiss
則caculate
任務執(zhí)行結(jié)束诅病,等待timer1
下個周期運行(因為當前線程的RunLoop并沒有退出,timer1
引用計數(shù)器并不為0)時檢測到線程取消狀態(tài)則執(zhí)行invalidate
方法(第二個任務也結(jié)束了)粥烁,此時線程釋放對于控制器的引用贤笆。
CADisplayLink
是一個執(zhí)行頻率(FPS)和屏幕刷新相同(可以修改preferredFramesPerSecond
改變刷新頻率)的定時器,它也需要加入到RunLoop才能執(zhí)行讨阻。與NSTimer
類似芥永,CADisplayLink
同樣是基于CFRunloopTimerRef
實現(xiàn),底層使用mk_timer
(可以比較加入到 RunLoop 前后 RunLoop 中 timer 的變化)变勇。和NSTimer
相比它精度更高(盡管NSTimer
也可以修改精度)恤左,不過和NStimer
類似的是如果遇到大任務它仍然存在丟幀現(xiàn)象。通常情況下CADisaplayLink
用于構(gòu)建幀動畫搀绣,看起來相對更加流暢飞袋,而NSTimer
則有更廣泛的用處。
9.2 AutoreleasePool
AutoreleasePool
是另一個與 RunLoop 相關(guān)討論較多的話題链患。其實從 RunLoop 源代碼分析巧鸭,AutoreleasePool
與 RunLoop 并沒有直接的關(guān)系,之所以將兩個話題放到一起討論最主要的原因是因為在iOS應用啟動后會注冊兩個 Observer 管理和維護AutoreleasePool
麻捻。不妨在應用程序剛剛啟動時打印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)建)加矛。
其實在應用程序啟動后系統(tǒng)還注冊了其他 Observer(例如即將進入休眠時執(zhí)行注冊回調(diào)_UIGestureRecognizerUpdateObserver
用于手勢處理、回調(diào)為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer用于界面實時繪制更新)和多個Source1
(例如context為CFMachPort
的Source1
用于接收硬件事件響應進而分發(fā)到應用程序一直到UIEvent
),這里不再一一詳述默终。
9.3 UI更新
如果打印App啟動之后的主線程 RunLoop 可以發(fā)現(xiàn)另外一個 callout 為_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv
的 Observer遭商,這個監(jiān)聽專門負責UI變化后的更新泌豆,比如修改了frame毅舆、調(diào)整了UI層級(UIView
/CALayer
)或者手動設置了setNeedsDisplay:
/setNeedsLayout:
之后就會將這些操作提交到全局容器。而這個 Observer 監(jiān)聽了主線程 RunLoop 的即將進入休眠和退出狀態(tài)议谷,一旦進入這兩種狀態(tài)則會遍歷所有的UI更新并提交進行實際繪制更新炉爆。
通常情況下這種方式是完美的,因為除了系統(tǒng)的更新卧晓,還可以利用setNeedsDisplay
等方法手動觸發(fā)下一次 RunLoop 運行的更新芬首。但是如果當前正在執(zhí)行大量的邏輯運算可能UI的更新就會比較卡,因此facebook推出了Texture來解決這個問題逼裆。
Texture
其實是將UI排版和繪制運算盡可能放到后臺郁稍,將UI的最終更新操作放到主線程(這一步也必須在主線程完成),同時提供一套類UIView
或CALayer
的相關(guān)屬性胜宇,盡可能保證開發(fā)者的開發(fā)習慣耀怜。這個過程中Texture
在主線程 RunLoop 中增加了一個Observer 監(jiān)聽即將進入休眠和退出 RunLoop 兩種狀態(tài),收到回調(diào)時遍歷隊列中的待處理任務一一執(zhí)行。
9.4 NSURLConnection
一旦啟動NSURLConnection
以后就會不斷調(diào)用delegate
方法接收數(shù)據(jù)桐愉,這樣一個連續(xù)的的動作正是基于 RunLoop 來運行财破。
一旦NSURLConnection
設置了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í)行任務時AFNetworking
通過performSelector:onThread:
將這個任務放到后臺線程的 RunLoop 中俊性。
十: 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ū)動的。
十一: 更多RunLoop使用
寫到了這里憔购,那么除了這些究竟在實際開發(fā)過程中我們自己能不能適當?shù)氖褂?RunLoop 幫我們做一些事情呢?
思考這個問題其實只要看RunLoopRef
的包含關(guān)系就知道了岔帽,RunLoop包含多個Mode玫鸟,而它的Mode又是可以自定義的,這么推斷下來其實無論是Source1
犀勒、Timer
還是Observer
開發(fā)者都可以利用屎飘,但是通常情況下不會自定義Timer妥曲,更不會自定義一個完整的Mode,利用更多的其實是Observer和Mode的切換钦购。
例如很多人都熟悉的使用perfromSelector
在默認模式下設置圖片檐盟,防止UITableView
滾動卡頓:
[imgView performSelector:@selector(setImage:) withObject:myImage afterDelay:0.0 inModes:@NSDefaultRunLoopMode]
還有sunnyxx的UITableView+FDTemplateLayoutCell利用 Observer 在界面空閑狀態(tài)下計算出UITableViewCell
的高度并進行緩存。
關(guān)于如何自定義一個Custom Input Source官網(wǎng)給出了詳細的流程押桃。
11.1: 自定義常駐線程
// 1.全局的葵萎,或者static的,目的是保持`NSThread`
@property (nonatomic, strong) NSThread *myThread;
// 2.初始化線程并啟動
self.myThread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
self.myThread.name = @"myThread";
[self.myThread start];
// 3.啟動RunLoop唱凯,子線程的RunLoop默認是停止的
- (void)run {
// 只要往RunLoop中添加了 timer羡忘、source或者observer就會繼續(xù)執(zhí)行
// 一個RunLoop通常必須包含一個輸入源或者定時器來監(jiān)聽事件
// 如果一個都沒有,RunLoop啟動后立即退出磕昼。
@autoreleasepool {
// 添加一個input source
NSRunLoop *rl = [NSRunLoop currentRunLoop];
[rl addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[rl run];
// 2卷雕、添加一個定時器
// NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// [[NSRunLoop currentRunLoop] run];
}
}
這樣myThread
這個線程就會一直存在,當需要使用此線程在處理一些事情的時候就這么調(diào)用:
[self performSelector:@selector(act) onThread:self.thread withObject:nil waitUntilDone:NO];
- (void)act {
NSLog(@"111");
NSLog(@"%@", [NSThread currentThread]);
}
END票从。
我是小侯爺漫雕。
在帝都艱苦奮斗,白天是上班族峰鄙,晚上是知識服務工作者浸间。
如果讀完覺得有收獲的話,記得關(guān)注和點贊哦先馆。
非要打賞的話发框,我也是不會拒絕的。