概念
RunLoop是一個相對抽象的概念,在程序運行中循環(huán)做一些事情,主要應用于:定時器(Timer)、PerformSelector、GCD Async To Main Queue、事件詳情、手勢識別、界面刷新、網(wǎng)絡請求、AutoreleasePool
我們想象一個場景:為什么App程序啟動之后能夠持續(xù)運行在前臺呢?
int main(int argc, char * argv[]){
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); } }
UIApplicationMain的大致實現(xiàn)原理就是:(偽代碼)
int retVal = 0;
do {
int message = sleep_and_wait(); //睡眠中等待消息(比如響應點擊各種事件)
retVal = proess_message(message);//處理消息,更改返回值,如果為0,代表程序退出,不為0,程序持續(xù)運行。
}while(retVal == 0);
正因為RunLoop底層在執(zhí)行一個while循環(huán),來維持程序的不退出。
RunLoop的基本作用:
- 保持程序不會馬上退出,而是保持運行狀態(tài)。
- 處理App中的各種事件(比如觸摸,定時器時間等)
- 節(jié)省CPU資源,提高程序性能,有事做做事,沒事做休眠。
RunLoop跟線程的關系
為什么聊Runloop一定要搭上線程,我們知道,程序里的每一句代碼,都會在線程里面執(zhí)行,下面要講到獲取Runloop對象的代碼也不例外,一定是跑在線程里面的.之前我們說道,Runloop是為了讓程序不退出,其實更準確地說,是為了保持某個線程不結(jié)束,只要還有未結(jié)束的線程,那么整個程序就不會退出,因為線程是程序的運行調(diào)度的基本單元.
線程與Runloop的關系是一對一的,一個新創(chuàng)建的線程,是沒有Runloop對象的,當我們在該線程里第一次通過上面的API獲得Runloop時,Runloop對象才會被創(chuàng)建,并且通過一個全局字典將Runloop對象和該線程存儲綁定在一起,形成一對一關系。
Runloop會在線程結(jié)束時銷毀,主線程的Runloop已經(jīng)自動獲取過(創(chuàng)建),子線程默認沒有開啟RunLoop(直到你在該線程獲取它)。RunLoop對象創(chuàng)建后,會被保存在一個全局的Dictionary里,線程作為key,Runloop對象作為value。
- 每條線程都有唯一的一個與之對用的RunLoop對象
- RunLoop保存在一個全局的Dictionary中,線程作為key,RunLoop作為value
- 線程創(chuàng)建時并沒有RunLoop對象,RunLoop會在第一次獲取的時候創(chuàng)建。
- RunLoop會在線程結(jié)束的時候銷毀
- 主線程的RunLoop已經(jīng)自動創(chuàng)建,子線程默認不開啟RunLoop。
RunLoop有兩種獲取方式
在OC中:[NSRunLoop currentRunLoop]
C的:CFRunLoopRef runloop = CFRunLoopGetCurrent();
啟動一個runloop有以下三種方法:
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
這三種方式無論通過哪一種方式啟動runloop,如果沒有一個輸入源或者timer附加于runloop上,runloop就會立刻退出。
第一種方式,runloop會一直運行下去,在此期間會處理來自輸入源的數(shù)據(jù),并且會在NSDefaultRunLoopMode模式下重復調(diào)用runMode:beforeDate:方法;
第二種方式,可以設置超時時間,在超時時間到達之前,runloop會一直運行,在此期間runloop會處理來自輸入源的數(shù)據(jù),并且也會在NSDefaultRunLoopMode模式下重復調(diào)用runMode:beforeDate:方法;
第三種方式,runloop會運行一次,超時時間到達或者第一個input source被處理,則runloop就會退出。
前兩種啟動方式會重復調(diào)用runMode:beforeDate:方法。
我們還可以在CF源碼里面詳細看看,Runloop的信息是寫在CF源碼文件夾的CFRunLoop.c文件里面,我們可以在里面搜索到CFRunLoopGetCurrent()函數(shù)的實現(xiàn)
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
CFRunLoopGetCurrent()中又是通過_CFRunLoopGet0來獲得Runloop對象的
Runloop對象底層結(jié)構
我們可以在源碼CFRunloop.c中找到Runloop的定義
**************????__CFRunLoop????***********
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
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
uint32_t _winthread;
//??????????????核心組成????????????
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
//??????????????核心組成????????????
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
**************????__CFRunLoopMode????***********
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* must have the run loop locked before locking this */
Boolean _stopped;
char _padding[3];
//??????????????核心組成????????????
CFStringRef _name;
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 */
};
退出RunLoop的方式
第一種啟動方式的退出方法
文檔說,如果想退出runloop,不應該使用第一種啟動方式來啟動runloop。
如果runloop沒有input sources或者附加的timer,runloop就會退出。
雖然這樣可以將runloop退出,但是蘋果并不建議我們這么做,因為系統(tǒng)內(nèi)部有可能會在當前線程的runloop中添加一些輸入源,所以通過手動移除input source或者timer這種方式,并不能保證runloop一定會退出。
第二種啟動方式runUntilDate:
可以通過設置超時時間來退出runloop。
第三種啟動方式runMode:beforeDate:
通過這種方式啟動,runloop會運行一次,當超時時間到達或者第一個輸入源被處理,runloop就會退出。
如果我們想控制runloop的退出時機,而不是在處理完一個輸入源事件之后就退出,那么就要重復調(diào)用runMode:beforeDate:,
具體可以參考蘋果文檔給出的方案,如下:
NSRunLoop *myLoop = [NSRunLoop currentRunLoop];
myPort = (NSMachPort *)[NSMachPort port];
[myLoop addPort:_port forMode:NSDefaultRunLoopMode];
BOOL isLoopRunning = YES; // global
while (isLoopRunning && [myLoop runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
//關閉runloop的地方
- (void)quitLoop
{
isLoopRunning = NO;
CFRunLoopStop(CFRunLoopGetCurrent());
}
總之
如果不想退出runloop可以使用第一種方式啟動runloop;
使用第二種方式啟動runloop,可以通過設置超時時間來退出;
使用第三種方式啟動runloop,可以通過設置超時時間或者使用CFRunLoopStop方法來退出。