你要知道的iOS多線程NSThread、GCD渣刷、NSOperation、RunLoop都在這里
轉(zhuǎn)載請注明出處 http://www.reibang.com/p/cfe5132e975f
本系列文章主要講解iOS中多線程的使用,包括:NSThread防楷、GCD、NSOperation以及RunLoop的使用方法詳解则涯,本系列文章不涉及基礎(chǔ)的線程/進程复局、同步/異步、阻塞/非阻塞粟判、串行/并行亿昏,這些基礎(chǔ)概念,有不明白的讀者還請自行查閱档礁。本系列文章將分以下幾篇文章進行講解角钩,讀者可按需查閱。
- iOS多線程——你要知道的NSThread都在這里
- iOS多線程——你要知道的GCD都在這里
- iOS多線程——你要知道的NSOperation都在這里
- iOS多線程——你要知道的RunLoop都在這里
- iOS多線程——RunLoop與GCD、AutoreleasePool
RunLoop 基本概念
前面幾篇文章詳細講解了創(chuàng)建多線程的方法和多線程編程的相關(guān)知識递礼,當我們使用NSThread
進行多線程編程時惨险,只要任務(wù)結(jié)束,線程也就退出了脊髓,每次執(zhí)行一個任務(wù)都需要創(chuàng)建一個線程非常浪費資源平道,所以需要一種能夠使線程常駐內(nèi)存不退出d,當有任務(wù)來臨時能隨時執(zhí)行的方法供炼,這就是RunLoop
的作用一屋。類似于javascript
的Event Loop
模型,大致類似于如下代碼:
int retVal = Running;
do {
// 執(zhí)行各種任務(wù)袋哼,處理各種事件
// ......
} while (retVal != Stop && retVal != Timeout);
上述循環(huán)只有在特定條件才才會退出冀墨,否則就會一直在循環(huán)中處理各種任務(wù)或事件,諸如觸摸屏幕事件涛贯、手勢事件诽嘉、定時器事件、用戶提交的任務(wù)弟翘、各種方法的執(zhí)行等虫腋。
RunLoop
與線程關(guān)聯(lián)的,是一種事件處理環(huán)稀余,用來安排和協(xié)調(diào)到來的事件悦冀,目的就是讓其關(guān)聯(lián)的線程在有事件到達時時刻保持運行狀態(tài),而當沒有事件需要處理時進入睡眠狀態(tài)從而節(jié)約資源睛琳,每一個線程都可以有一個RunLoop
對象與之對應(yīng)盒蟆,并且是在第一次獲取它是系統(tǒng)自動創(chuàng)建的,比如主線程關(guān)聯(lián)的RunLoop
师骗,我們都知道程序的入口函數(shù)是main
函數(shù)历等,下面是創(chuàng)建工程后Xcode
自動生成的main.m
文件的main
函數(shù)代碼:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
該方法執(zhí)行體被autoreleasepool
包圍,所以程序可以使用ARC
來管理內(nèi)存辟癌,后面會講解RunLoop
與autoreleasepool
的關(guān)系寒屯,main
函數(shù)直接返回了UIApplicationMain
函數(shù),該函數(shù)內(nèi)部就會第一次獲取RunLoop
對象黍少,所以系統(tǒng)就會創(chuàng)建這樣一個RunLoop
對象寡夹,因此在沒有滿足特定條件的時候該主線程不會退出,應(yīng)用就可以持續(xù)運行而不會退出仍侥。
在官方文檔中使用下圖描述RunLoop
模型:
從上圖可以看出一個線程會關(guān)聯(lián)一個RunLoop
對象要出,RunLoop
對象會一直循環(huán),直到超時或收到退出指令农渊。在無限循環(huán)的過程中會一直處理到來的事件,右側(cè)將事件分為了兩類,一類是Input sources
這部分包括基于端口的source1
事件砸紊,開發(fā)者提交的各種source0
事件传于,調(diào)用performSelector:onThread:
方法事件,還有一類Timer sources
這個就是常用的定時器事件醉顽,這些事件在程序運行期間會不斷產(chǎn)生之后會由RunLoop
對象檢測并負責處理相關(guān)事件沼溜。
RunLoop 源碼解析
RunLoop
有兩個對象,NSRunLoop
和CFRunLoopRef
游添,區(qū)別在于由Core Foundation
框架提供的CFRunLoopRef
是純C語言編寫的系草,提供的也是C語言接口,這些接口都是線程安全的唆涝,由Foundation
框架提供的NSRunLoop
是面向?qū)ο蟮恼叶迹腔?code>CFRunLoopRef的封裝,提供的都是面向?qū)ο蟮慕涌诶群ǎ@些接口不是線程安全的能耻,Core Foudation
框架是開源的,可以在這個地址下載:Core Foundation開源代碼亡驰,本文接下來的內(nèi)容主要是針對該開源代碼進行講解晓猛。
首先,看一下在代碼中如何獲取RunLoop
對象,在Foundation
框架中的NSRunLoop
類提供了如下兩個類屬性:
//獲取當前線程關(guān)聯(lián)的RunLoop對象
@property (class, readonly, strong) NSRunLoop *currentRunLoop;
//獲取主線程關(guān)聯(lián)的RunLoop對象
@property (class, readonly, strong) NSRunLoop *mainRunLoop
對應(yīng)的Core Foundation
框架中提供了如下兩個函數(shù)來獲取RunLoop
對象:
//獲得當前線程關(guān)聯(lián)的RunLoop對象
CFRunLoopGetCurrent();
// 獲得主線程關(guān)聯(lián)的RunLoop對象
CFRunLoopGetMain();
前面一直講每一個線程都會關(guān)聯(lián)一個RunLoop
對象,并且不能通過手動創(chuàng)建該對象方援,只能在第一次獲取時系統(tǒng)自動創(chuàng)建送丰,看一下Core Foundation
框架是如何實現(xiàn)的:
//CFRunLoopGetMain函數(shù)用于獲取主線程關(guān)聯(lián)的RunLoop對象
CFRunLoopRef CFRunLoopGetMain(void) {
CHECK_FOR_FORK();
//靜態(tài)變量保存主線程關(guān)聯(lián)的RunLoop對象
static CFRunLoopRef __main = NULL; // no retain needed
//如果主線程關(guān)聯(lián)的RunLoop對象為NULL就調(diào)用_CFRunLoopGet0函數(shù)獲取一個
if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
return __main
}
//獲取當前線程關(guān)聯(lián)的RunLoop對象
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
//這一段沒找到對應(yīng)的函數(shù)...猜測是和上面的函數(shù)用意一樣
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
//如果上面沒找到就調(diào)用_CFRunLoopGet0函數(shù)去獲取一個
return _CFRunLoopGet0(pthread_self());
}
//全局的可變字典數(shù)據(jù)結(jié)構(gòu),key為thread_t即線程珠叔,value為RunLoop對象
static CFMutableDictionaryRef __CFRunLoops = NULL;
//全局的一個鎖
static CFLock_t loopsLock = CFLockInit;
//_CFRunLoopGet0接收一個pthread_t對象,即線程對象,返回一個與之關(guān)聯(lián)的RunLoop對象
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
//判斷是否為主線程
if (pthread_equal(t, kNilPthreadT)) {
//pthread_main_thread_np()函數(shù)用來獲取主線程
t = pthread_main_thread_np();
}
//加鎖蚓曼,防止產(chǎn)生競爭創(chuàng)建多個RunLoop對象
__CFLock(&loopsLock);
//如果全局的保存線程和runloop對象的字典為空
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
//創(chuàng)建一個字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
/*
根據(jù)主線程創(chuàng)建RunLoop對象
所以,當?shù)谝淮潍@取RunLoop對象時就會自動創(chuàng)建主線程關(guān)聯(lián)的RunLoop對象
*/
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
//設(shè)置全局的字典钦扭,key為主線程纫版,value為主線程關(guān)聯(lián)的RunLoop對象
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
//通過線程在字典中獲取RunLoop對象
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
//如果沒有獲取到
if (!loop) {
//沒有獲取到就根據(jù)線程創(chuàng)建一個RunLoop對象
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
//再次獲取
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//字典中仍然沒有線程關(guān)聯(lián)的RunLoop對象就將剛才新創(chuàng)建加入到字典照中
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
//設(shè)置銷毀時的回調(diào)
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
//返回線程關(guān)聯(lián)的RunLoop對象
return loop;
}
/*
真正的用于創(chuàng)建RunLoop對象的靜態(tài)函數(shù),形參為線程對象
該函數(shù)主要用于分配存儲空間客情,并進行RunLoop對象相關(guān)初始化操作
*/
static CFRunLoopRef __CFRunLoopCreate(pthread_t t) {
CFRunLoopRef loop = NULL;
CFRunLoopModeRef rlm;
uint32_t size = sizeof(struct __CFRunLoop) - sizeof(CFRuntimeBase);
loop = (CFRunLoopRef)_CFRuntimeCreateInstance(kCFAllocatorSystemDefault, CFRunLoopGetTypeID(), size, NULL);
if (NULL == loop) {
return NULL;
}
(void)__CFRunLoopPushPerRunData(loop);
__CFRunLoopLockInit(&loop->_lock);
loop->_wakeUpPort = __CFPortAllocate();
if (CFPORT_NULL == loop->_wakeUpPort) HALT;
__CFRunLoopSetIgnoreWakeUps(loop);
loop->_commonModes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
CFSetAddValue(loop->_commonModes, kCFRunLoopDefaultMode);
loop->_commonModeItems = NULL;
loop->_currentMode = NULL;
loop->_modes = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
loop->_blocks_head = NULL;
loop->_blocks_tail = NULL;
loop->_counterpart = NULL;
loop->_pthread = t;
#if DEPLOYMENT_TARGET_WINDOWS
loop->_winthread = GetCurrentThreadId();
#else
loop->_winthread = 0;
#endif
rlm = __CFRunLoopFindMode(loop, kCFRunLoopDefaultMode, true);
if (NULL != rlm) __CFRunLoopModeUnlock(rlm);
return loop;
}
通過上面源碼不難發(fā)現(xiàn)其弊,RunLoop
對象保存在一個全局的字典中,該字典以線程對象pthread_t
為key
膀斋,以RunLoop
對象為value
梭伐,并且,在第一次獲取RunLoop
對象時總會先把主線程關(guān)聯(lián)的RunLoop
對象創(chuàng)建好仰担,在獲取其他線程關(guān)聯(lián)的RunLoop
對象時都從這個全局的字典中獲取糊识,如果沒有獲取到就創(chuàng)建一個并且添加進字典中,所以每一個線程有且僅有一個與之關(guān)聯(lián)的RunLoop
對象,重要的是赂苗,如果不獲取線程關(guān)聯(lián)的RunLoop
對象愉耙,那么這個RunLoop
對象就不會被創(chuàng)建。當線程退出時拌滋,也會將RunLoop
對象銷毀朴沿。
接下來查看一下CFRunLoopRef
具體的數(shù)據(jù)結(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;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
typedef struct __CFRunLoop * CFRunLoopRef;
上述數(shù)據(jù)結(jié)構(gòu)中比較重要的就是_commonModes
、_commonModeItems
败砂、_currentMode
以及_modes
赌渣,具體關(guān)系如下圖所示,該圖取自文章深入理解RunLoop https://blog.ibireme.com/2015/05/18/runloop/下面講的內(nèi)容也有參考該博客中的內(nèi)容昌犹,建議讀者閱讀原文:
上圖很好的描述了struct __CFRunLoop
數(shù)據(jù)結(jié)構(gòu)相關(guān)成員變量的關(guān)系坚芜,每一個__CFRunLoop
對象可以包含數(shù)個不同的Mode
,而每一個Mode
又包含了數(shù)個Source
祭隔、Observer
和Timer
货岭,當一個RunLoop
運行時只能選擇其中的某一個Mode
來執(zhí)行,如果要切換Mode
則需要退出運行后指定一個新的Mode
后重新執(zhí)行運行疾渴。通過這樣的方式千贯,可以在不同Mode
中設(shè)置不同的Source/Observer/Timer
而不同的Mode
中間的這三部分互不影響,也就是說搞坝,有些Source/Observer/Timer
只能在某一個Mode
中運行搔谴,當RunLoop
運行在其他Mode
中,該事件得不到處理桩撮。
Source CFRunLoopSourceRef
Source
即CFRunLoopSourceRef
類的對象敦第,指代事件源,即前文官方結(jié)構(gòu)圖中的Input Source
店量,在官方文檔中該事件源Source
分為三類:
Port-Based Sources 基于端口的芜果,也稱為
source1
事件,通過內(nèi)核和其他線程通信融师,接收到事件后包裝為source0
事件后分發(fā)給其他線程處理右钾。Custom Input Sources 用戶自定義
Cocoa Perform Selector Sources 調(diào)用諸如
perfromSelector:onThread:
這樣的方法產(chǎn)生的事件
按照調(diào)用棧來說其實只分成兩類,Source0
不基于端口的和Source1
基于端口的旱爆,分類方式并不是很重要舀射,了解即可。
Timer CFRunLoopTimerRef
Timer
可以理解為定時器即NSTimer
怀伦,因為CFRunLoopTimerRef
和NSTimer
是toll-free bridged
脆烟,所以可以互相轉(zhuǎn)換,將其理解為NSTimer
即可房待,RunLoop
對象會在注冊的定時器時間到達時喚醒關(guān)聯(lián)的線程對象來執(zhí)行定時器的回調(diào)邢羔。
Observer CFRunLoopObserverRef
Observer
就是監(jiān)聽器驼抹,用來監(jiān)聽RunLoop
的各種狀態(tài),在源碼中有如下監(jiān)聽狀態(tài)的枚舉定義:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
//即將進入RunLoop的執(zhí)行循環(huán)
kCFRunLoopEntry = (1UL << 0),
//即將處理Timer事件
kCFRunLoopBeforeTimers = (1UL << 1),
//即將處理Source事件
kCFRunLoopBeforeSources = (1UL << 2),
//RunLoop即將進入休眠狀態(tài)
kCFRunLoopBeforeWaiting = (1UL << 5),
//RunLoop即將被喚醒
kCFRunLoopAfterWaiting = (1UL << 6),
//RunLoop即將退出
kCFRunLoopExit = (1UL << 7),
//監(jiān)聽RunLoop的全部狀態(tài)
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
Observer
中定義了一系列的監(jiān)聽器张抄,開發(fā)者也可以使用監(jiān)聽器來監(jiān)聽具體的狀態(tài)改變砂蔽,具體栗子后文會介紹洼怔。
Mode CFRunLoopModeRef
Mode
是RunLoop
中比較重要的部分署惯,系統(tǒng)默認為我們提供了五種Mode
:
kCFRunLoopDefaultMode 即 NSDefaultRunLoopMode,默認運行模式
UITrackingRunLoopMode 跟蹤UIScrollView滑動時使用的運行模式镣隶,保證滑動時不受其他事件處理的影響极谊,保證絲滑
UIInitializationRunLoopMode 啟動應(yīng)用時的運行模式,應(yīng)用啟動完成后就不會再使用
GSEventReceiveRunLoopMode 事件接收運行模式
kCFRunLoopCommonModes 即 NSRunLoopCommonModes 是一種標記的模式安岂,還需要上述四種模式的支持
UITrackingRunLoopMode
只有當用戶滑動屏幕時轻猖,即滑動UIScrollView
時才會執(zhí)行的模式,此時域那,不在該模式內(nèi)的Source/Timer/Observer
都不會得到執(zhí)行咙边,它僅僅專注于滑動時產(chǎn)生的各種事件,通過這樣的方式就可以保證用戶在滑動頁面時的流暢性次员,這也是分不同Mode
的優(yōu)點败许。
具體數(shù)據(jù)結(jié)構(gòu)如下:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
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 */
};
從上述數(shù)據(jù)結(jié)構(gòu)中可以看出,Mode
內(nèi)部管理了一個_source0
的事件集合淑蔚,一個_source1
的事件集合市殷,一個_observers
的數(shù)組以及_timers
的數(shù)組,這也印證了前文中關(guān)于Mode
的圖例刹衫,再結(jié)合之前講的__CFRunLoop
中比較重要的幾個成員變量:
struct __CFRunLoop {
...
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
...
}
其中_currentMode
即代表當前RunLoop
對象正在執(zhí)行的Mode
即CFRunLoopModeRef
類的對象醋寝。
_mode
是一個Set
集合保存了所有該RunLoop
對象可以執(zhí)行的Mode
。
_commonModes
保存的是具有Common
屬性的Mode
的名稱带迟,前文__CFRunLoopMode
的結(jié)構(gòu)體定義中可以看到音羞,每個Mode
管理自己的Source/Timer/Observer
,而被標記為Common
屬性的Mode
還有一個特性就是當RunLoop
對象在執(zhí)Common
屬性的Mode
時仓犬,會自動將_commonModeItems
中保存的Source/Observer/Timer
同步添加該Mode
中嗅绰,標識Common
屬性只需要將__CFRunLoopModeRef
的_name
成員變量的值添加進_commonModes
集合中即可。被標記為Common
屬性的Mode
就是前文講的kCFRunLoopCommonModes
模式婶肩,可以看出這種模式不是一種真正的模式办陷,僅僅是標識其他模式是否需要同步添加_commonModeItems
中的Source/Timer/Observer
。
_commonModeItems
中保存的就是那些需要同步添加到具有Common
屬性的Mode
中的Source/Timer/Observer
集合律歼。
系統(tǒng)默認將kCFRunLoopDefaultMode
和UITrackingRunLoopMode
添加到了_commonModes
中民镜,即標識為Common
屬性,所以當RunLoop
運行在這兩種模式中會自動同步添加_commonModeItems
中的Source/Timer/Observer
险毁。
舉個常見的栗子:
- (void) viewWillAppear:(BOOL)animate
{
[super viewWilAppear:YES];
//創(chuàng)建一個NSTimer的對象制圈,從當前時間開始每1s輸出一次Hello们童,World
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"Hello, World");
}];
//將timer加入到當前線程關(guān)聯(lián)的RunLoop對象的NSDefaultRunLoopMode中
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//類方法,創(chuàng)建一個timer并添加到當前線程關(guān)聯(lián)的RunLoop的NSDefaultRunLoopMode中
[NSTimer scheduledTimerWithTimeInterval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"Hello, World222");
}];
}
上面的栗子創(chuàng)建了兩個NSTimer
鲸鹦,這兩個定時器執(zhí)行效果相同慧库,但如果頁面中有一個UIScrollView
或其子類的對象在滑動時,NSTimer
就不會再有任何輸出馋嗜,當停下滑動時又會有輸出齐板,因為上述代碼創(chuàng)建的兩個NSTimer
都加入到了RunLoop
對象的NSDefaultRunLoopMode
中,在滑動時RunLoop
會切換到UITrackingRunLoopMode
模式下執(zhí)行葛菇,而UITrackingRunLoopMode
中沒有上述定時器甘磨,所以不會執(zhí)行,當停止滑動時RunLoop
對象又切換到了NSDefaultRunLoopMode
模式眯停,所以可以繼續(xù)執(zhí)行定時器的回調(diào)济舆。
為了解決這個問題,可以將NSTimer
即加入到NSDefaultRunLoopMode
中莺债,又加入到UITrackingRunLoopMode
中滋觉,同一個Source/Timer/Observer
可以添加到不同的Mode
中,但同一個Source/Timer/Observer
不能添加到同一個Mode
中齐邦,這樣不會有任何效果椎侠,但添加到兩個Mode
中并不是最好的解決方案,還有一個方案就是利用前面的Common
屬性侄旬,NSDefaultRunLoopMode
和UITrackingRunLoopMode
都被添加進了_commonModes
集合中被標識了具有Common
屬性肺蔚,所以在運行時就會自動將_commonModeItems
中的Source/Timer/Observer
同步添加到其中,因此儡羔,只需要將創(chuàng)建的NSTimer
加入到_commonModeItems
中即可宣羊,此時只需要使用NSRunLoopCommonModes
即可,代碼如下:
- (void) viewWillAppear:(BOOL)animate
{
[super viewWilAppear:YES];
NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"Hello, World");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
將NSTimer
加入到NSRunLoopCommonModes
中就是把其加入到_commonModeItems
集合中汰蜘,這樣在滑動時就會自動同步添加NSTimer
到UITrackingRunLoopMode
模式下仇冯,所以定時器也可會得到執(zhí)行。如果需要注意使用類方法scheduledTimerWithTimeInterval:repeats:block
時要注意該方法默認是加入到NSDefaultRunLoopMode
模式中的族操。
通過上述講解苛坚,可以發(fā)現(xiàn),NSTimer
其實是不那么精確的色难,首先泼舱,在使用時需要加入到RunLoop
中,如果加在CommonMode
在普通情況或滑動時都可以執(zhí)行回調(diào)方法枷莉,這個時候的誤差就來自于RunLoop
一次循環(huán)的執(zhí)行延遲娇昙,最壞情況下,RunLoop
一次循環(huán)需要執(zhí)行的任務(wù)較多笤妙,NSTimer
回調(diào)執(zhí)行的延遲就會加大冒掌。如果加在其他模式下噪裕,當模式切換時就不會再執(zhí)行NSTimer
的回調(diào)方法了,所以股毫,在使用時需要根據(jù)情況選擇不同的定時器以滿足項目需求膳音。
在查看RunLoop
運行機制前,做一個小實驗铃诬,創(chuàng)建一個視圖控制器祭陷,并添加一個按鈕,在按鈕點擊事件的回調(diào)函數(shù)中打一個斷點氧急,然后運行程序點擊按鈕颗胡,之后查看調(diào)用棧如下圖所示:
從上圖中可以看到程序在18處執(zhí)行main
函數(shù)毫深,17執(zhí)行UIApplicationMain
函數(shù)吩坝,這就是程序啟動過程,16是系統(tǒng)內(nèi)部事件哑蔫,15調(diào)用CFRunLoopRunSpecific
后文會詳細講解該函數(shù)钉寝,14開始執(zhí)行RunLoop
進入循環(huán),13開始處理source0
這個source0
就是點擊按鈕的事件闸迷,11是真正執(zhí)行source0
的函數(shù)嵌纲,10-0就是點擊事件的整個轉(zhuǎn)發(fā)處理過程,最終交由我們自定義的回調(diào)方法進行處理腥沽。
RunLoop 執(zhí)行邏輯
在官方文檔中描述的RunLoop
循環(huán)中的執(zhí)行邏輯如下:
通知監(jiān)聽器RunLoop進入循環(huán)
通知監(jiān)聽器即將處理Timer事件
通知監(jiān)聽器即將處理source0(不是基于端口的)事件
執(zhí)行source0事件
如果有source1(基于端口的)事件則立即執(zhí)行跳轉(zhuǎn)到第九步
通知監(jiān)聽器RunLoop即將進入休眠狀態(tài)
-
將線程休眠逮走,直到以下事件發(fā)生才會被喚醒:
- 有source1事件到達
- 定時器觸發(fā)時間到達
- RunLoop對象的超時時間過期
- 被外部顯示喚醒
通知監(jiān)聽器RunLoop對象即將被喚醒
-
處理添加進來的事件,包括:
- 如果用戶定義的定時器時間到達今阳,執(zhí)行定時器時間并重啟循環(huán)师溅,跳轉(zhuǎn)到第二步
- 如果有source1事件,傳遞這個事件
- 如果RunLoop被顯示喚醒并且沒有超時則重啟RunLoop盾舌,跳轉(zhuǎn)到第二步
通知監(jiān)聽器RunLoop退出循環(huán)
為了驗證上述執(zhí)行順序墓臭,可以自行編寫監(jiān)聽器來監(jiān)聽RunLoop
對象狀態(tài)的改變,具體栗子如下:
- (void)viewWillAppear:(BOOL)animated
{
// 創(chuàng)建觀察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"Status has changed into: %zd", activity);
});
/*
將監(jiān)聽器添加到當前RunLoop對象中妖谴,在RunLoop循環(huán)中就會執(zhí)行上述回調(diào)塊
監(jiān)聽的是kCFRunLoopDefaultMode即默認狀態(tài)
也可以使用kCFRunLoopCommonModes窿锉,同時監(jiān)聽默認狀態(tài)以及滑動視圖的狀態(tài)
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
//CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopCommonModes);
//Core Foundation需要手動釋放observer
CFRelease(observer);
//添加一個textView,它是UIScrollView的子類
UITextView *textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 20, ScreenWidth, 300)];
textView.text = @"Hello, World";
[textView setBackgroundColor:[UIColor redColor]];
[self.view addSubview:textView];
}
為了減少輸出選擇監(jiān)聽kCFRunLoopDefaultMode
模式膝舅,啟動程序后不做任何操作發(fā)現(xiàn)其輸出如下:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
//即將進入RunLoop的執(zhí)行循環(huán) 1
kCFRunLoopEntry = (1UL << 0),
//即將處理Timer事件 2
kCFRunLoopBeforeTimers = (1UL << 1),
//即將處理Source事件 4
kCFRunLoopBeforeSources = (1UL << 2),
//RunLoop即將進入休眠狀態(tài) 32
kCFRunLoopBeforeWaiting = (1UL << 5),
//RunLoop即將被喚醒 64
kCFRunLoopAfterWaiting = (1UL << 6),
//RunLoop即將退出 128
kCFRunLoopExit = (1UL << 7),
//監(jiān)聽RunLoop的全部狀態(tài)
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 2
Status has changed into: 4
Status has changed into: 32
Status has changed into: 64
Status has changed into: 2
Status has changed into: 4
Status has changed into: 32
Status has changed into: 64
從輸出中不難發(fā)現(xiàn)嗡载,不做任何操作時程序處于NSDefaultRunLoopMode
模式下,一直在2-9步間循環(huán)仍稀,當沒有事件要處理時就轉(zhuǎn)入了休眠狀態(tài)洼滚,之后又被喚醒繼續(xù)處理,可能有讀者疑惑為什么連續(xù)那么多次都是2 4的輸出琳轿,狀態(tài)2表示即將處理Timer
判沟,狀態(tài)4表示即將處理Source
耿芹,接著就會處理Source
,但如果有source1
的存在(基于端口的事件)就不會休眠直接跳轉(zhuǎn)到第九步處理相關(guān)事件挪哄,處理完成之后又回到第二步吧秕,所以產(chǎn)生上述輸出。
此時當我們將UITextView
中添加多個換行符直到滾動條出現(xiàn)后迹炼,滑動UItextView
會發(fā)現(xiàn)有如下輸出:
Status has changed into: 2
Status has changed into: 4
Status has changed into: 32
Status has changed into: 64
Status has changed into: 2
Status has changed into: 4
Status has changed into: 32
Status has changed into: 64
Status has changed into: 2
Status has changed into: 4
Status has changed into: 128
128代表RunLoop
對象退出循環(huán)了砸彬,因為當我們滑動UItextView
時,RunLoop
對象切換到了UITrackingRunLoopMode
斯入,前文講過砂碉,RunLoop
對象每次執(zhí)行時只能執(zhí)行在一個模式下,如果要切換模式只能退出后重新進入循環(huán)刻两,從上述輸出就證明了這一點增蹭。
接下來看一下RunLoop
的執(zhí)行源碼。
RunLoop執(zhí)行的入口函數(shù)
RunLoop
對外只提供了兩個入口函數(shù)
/*
RunLoop對外提供的入口函數(shù)
用戶可以顯示調(diào)用后使當前線程關(guān)聯(lián)的RunLoop對象以默認模式運行
*/
void CFRunLoopRun(void) { /* DOES CALLOUT */
//返回值
int32_t result;
//循環(huán)體磅摹,直到RunLoop停止或者結(jié)束時才會終止循環(huán)
do {
/*
調(diào)用CFRunLoopRunSpecific啟動RunLoop
執(zhí)行的RunLoop就是當前線程關(guān)聯(lián)的RunLoop對象
超時時間100億秒滋迈,317.098年,永不超時
*/
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
/*
RunLoop對外提供的入口函數(shù)
用戶可以顯示調(diào)用后使當前線程關(guān)聯(lián)的RunLoop對象以指定模式户誓、超時時間運行
*/
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
CFRunLoopRunSpecific
/*
按照指定的模式饼灿、超時時間以及條件運行RunLoop
rl: 要運行的RunLoop對象
modeName: RunLoop對象要執(zhí)行的模式名稱
seconds: RunLoop循環(huán)的超時時間
returnAfterSourceHandled: 是否在處理完source后就退出RunLoop循環(huán)
*/
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
//通過Mode的名稱查找CFRunLoopModeRef對象
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
/*
如果沒有獲取到Mode
或Mode的內(nèi)容為空,內(nèi)容為空即Mode的Source/Timer/Observer集合都沒有數(shù)據(jù)
為空就直接返回帝美,并不真正執(zhí)行RunLoop的循環(huán)
*/
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;
/*
調(diào)用__CFRunLoopDoObservers觸發(fā)監(jiān)聽器碍彭,RunLoop即將進入循環(huán)
*/
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
//調(diào)用真正執(zhí)行RunLoop循環(huán)的函數(shù)
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
/*
調(diào)用__CFRunLoopDoObservers觸發(fā)監(jiān)聽器,RunLoop退出循環(huán)
*/
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
上述代碼做了一系列的處理悼潭,比如通過名稱查找Mode
庇忌,判斷Mode
是否為空,即判斷Mode
中是否還有Source/Timer/Observer
女责,其中比較重要的函數(shù)有__CFRunLoopFindMode
函數(shù)漆枚,該函數(shù)在查找根據(jù)Mode
名稱查找時,如果沒有找到會嘗試創(chuàng)建一個新的Mode
抵知,如果創(chuàng)建失敗才會返回NULL
墙基。__CFRunLoopModeIsEmpty
函數(shù)用來判斷Mode
中的Source/Timer/Observer
是否為空,如果集合中沒有對象就返回true
刷喜。__CFRunLoopDoObservers
用來觸發(fā)監(jiān)聽器的回調(diào)函數(shù)或回調(diào)塊残制,前文舉的栗子在創(chuàng)建監(jiān)聽器并加入到RunLoop
對象后,其實是將這個監(jiān)聽器加入到了Mode
的_observers
數(shù)組中掖疮,所以該函數(shù)內(nèi)部會遍歷對應(yīng)數(shù)組并調(diào)用回調(diào)函數(shù)或回調(diào)塊來進行通知初茶。
接下來就要查看__CFRunLoopRun
函數(shù)的實現(xiàn),但該函數(shù)源碼太長有三百多行浊闪,而且包含了不少跨平臺的預(yù)編譯指令恼布,由于篇幅的問題螺戳,這里不直接分析了,有興趣的讀者可以參考本系列文章第五篇iOS多線程——RunLoop與GCD折汞、AutoreleasePool倔幼,在這篇文章中會詳細講解該函數(shù)的源碼,那這里直接使用深入理解RunLoop https://blog.ibireme.com/2015/05/18/runloop/中作者簡化整理版本爽待,代碼如下:
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
/// 2. 通知 Observers: RunLoop 即將觸發(fā) Timer 回調(diào)损同。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: RunLoop 即將觸發(fā) Source0 (非port) 回調(diào)。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
/// 執(zhí)行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 4. RunLoop 觸發(fā) Source0 (非port) 回調(diào)鸟款。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
/// 執(zhí)行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
/// 5. 如果有 Source1 (基于port) 處于 ready 狀態(tài)膏燃,直接處理這個 Source1 然后跳轉(zhuǎn)去處理消息。
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
if (hasMsg) goto handle_msg;
}
/// 通知 Observers: RunLoop 的線程即將進入休眠(sleep)何什。
if (!sourceHandledThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}
/// 7. 調(diào)用 mach_msg 等待接受 mach_port 的消息组哩。線程將進入休眠, 直到被下面某一個事件喚醒。
/// ? 一個基于 port 的Source 的事件富俄。
/// ? 一個 Timer 到時間了
/// ? RunLoop 自身的超時時間到了
/// ? 被其他什么調(diào)用者手動喚醒
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
}
/// 8. 通知 Observers: RunLoop 的線程剛剛被喚醒了禁炒。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);
/// 收到消息,處理消息霍比。
handle_msg:
/// 9.1 如果一個 Timer 到時間了,觸發(fā)這個Timer的回調(diào)暴备。
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
}
/// 9.2 如果有dispatch到main_queue的block悠瞬,執(zhí)行block。
else if (msg_is_dispatch) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
}
/// 9.3 如果一個 Source1 (基于port) 發(fā)出事件了涯捻,處理這個事件
else {
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandledThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
/// 執(zhí)行加入到Loop的block
__CFRunLoopDoBlocks(runloop, currentMode);
if (sourceHandledThisLoop && stopAfterHandle) {
/// 進入loop時參數(shù)說處理完事件就返回浅妆。
retVal = kCFRunLoopRunHandledSource;
} else if (timeout) {
/// 超出傳入?yún)?shù)標記的超時時間了
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(runloop)) {
/// 被外部調(diào)用者強制停止了
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
/// source/timer/observer一個都沒有了
retVal = kCFRunLoopRunFinished;
}
/// 如果沒超時,mode里沒空障癌,loop也沒被停止凌外,那繼續(xù)loop。
} while (retVal == 0);
}
上面執(zhí)行代碼就和前面官方文檔中講解的順序一致涛浙,不再贅述康辑。
在前文給了一個點擊按鈕的調(diào)用棧運行圖,可以發(fā)現(xiàn)執(zhí)行source0
事件時是調(diào)用了一個非常長的函數(shù)來處理轿亮,為了方便查看調(diào)用棧執(zhí)行的順序疮薇,深入理解RunLoop https://blog.ibireme.com/2015/05/18/runloop/一文中,作者將整個RunLoop
響應(yīng)函數(shù)按執(zhí)行順序列了下來我注,如下:
{
/// 1. 通知Observers按咒,即將進入RunLoop
/// 此處有Observer會創(chuàng)建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即將觸發(fā) Timer 回調(diào)。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即將觸發(fā) Source (非基于port的,Source0) 回調(diào)但骨。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 觸發(fā) Source0 (非基于port的) 回調(diào)励七。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers智袭,即將進入休眠
/// 此處有Observer釋放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers,線程被喚醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer喚醒的掠抬,回調(diào)Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch喚醒的补履,執(zhí)行所有調(diào)用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件喚醒了,處理這個事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers剿另,即將退出RunLoop
/// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
參考文章
備注
由于作者水平有限箫锤,難免出現(xiàn)紕漏,如有問題還請不吝賜教雨女。