一、RunLoop簡介
RunLoop是通過內(nèi)部維護(hù)的事件循環(huán)來對事件/消息進(jìn)行管理的一個對象。
事件循環(huán):
沒有消息處理時串结,休眠以避免資源占用。用戶態(tài)切換到內(nèi)核態(tài)舅列,等待消息肌割。
有消息需要處理時,立刻被喚醒帐要。內(nèi)核態(tài)切換到用戶態(tài)把敞,處理消息。維護(hù)事件循環(huán)可以用來不斷處理消息和事件榨惠,并對他們進(jìn)行管理奋早。
runloop通過調(diào)用mach_msg()函數(shù)來轉(zhuǎn)移當(dāng)前線程的控制權(quán)給內(nèi)核態(tài)/用戶態(tài)。
基本作用
1赠橙、 保持程序的持續(xù)運行
如果沒有RunLoop耽装,main()函數(shù)一執(zhí)行完,程序就會立刻退出期揪。
而 iOS 程序能保持持續(xù)運行的原因就是在main()函數(shù)中調(diào)用了UIApplicationMain函數(shù)掉奄,這個函數(shù)內(nèi)部會啟動主線程的RunLoop;
2凤薛、處理 App 中的的各種事件(比如觸摸事件姓建、定時器事件等);
3缤苫、節(jié)省 CPU 資源速兔,提高程序性能:該做事時做事,該休息時休息活玲。
RunLoop對象
iOS 中有 2 套 API 來訪問和使用RunLoop:
Foundation:NSRunLoop(是CFRunLoopRef的封裝涣狗,提供了面向?qū)ο蟮?API)
Core Foundation:CFRunLoopRef
NSRunLoop和CFRunLoopRef都代表著RunLoop對象
NSRunLoop不開源,而CFRunLoopRef是開源的:Core Foundation 源碼
獲取RunLoop對象的方式:
//Foundation
[NSRunLoop mainRunLoop]; // 獲取主線程的 RunLoop 對象
[NSRunLoop currentRunLoop]; // 獲取當(dāng)前線程的 RunLoop 對象
// Core Foundation
CFRunLoopGetMain(); // 獲取主線程的 RunLoop 對象
CFRunLoopGetCurrent(); // 獲取當(dāng)前線程的 RunLoop 對象
應(yīng)用范疇
定時器(Timer)翼虫、PerformSelector
GCD:dispatch_async(dispatch_get_main_queue(), ^{ });
事件響應(yīng)屑柔、手勢識別、界面刷新
網(wǎng)絡(luò)請求
AutoreleasePool
二珍剑、RunLoop數(shù)據(jù)結(jié)構(gòu)
CFRunLoopRef
RunLoop對象的底層就是一個CFRunLoopRef結(jié)構(gòu)體掸宛,它里面存儲著:
_pthread:RunLoop與線程是一一對應(yīng)關(guān)系
_commonModes:存儲著 NSString 對象的集合(Mode 的名稱)
_commonModeItems:存儲著被標(biāo)記為通用模式的Source0/Source1/Timer/Observer
_currentMode:RunLoop當(dāng)前的運行模式
_modes:存儲著RunLoop所有的 Mode(CFRunLoopModeRef)模式
// CFRunLoop.h
typedef struct __CFRunLoop * CFRunLoopRef;
// CFRunLoop.c
struct __CFRunLoop {
.......
pthread_t _pthread; //與線程一一對應(yīng)
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
......
};
CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的運行模式;
一個RunLoop包含若干個 Mode招拙,每個 Mode 又包含若干個Source0/Source1/Timer/Observer唧瘾;
RunLoop啟動時只能選擇其中一個 Mode措译,作為 currentMode;
如果需要切換 Mode饰序,只能退出當(dāng)前 Loop领虹,再重新選擇一個 Mode 進(jìn)入,切換模式不會導(dǎo)致程序退出求豫;
不同 Mode 中的Source0/Source1/Timer/Observer能分隔開來塌衰,互不影響;
如果 Mode 里沒有任何Source0/Source1/Timer/Observer蝠嘉,RunLoop會立馬退出最疆。
// CFRunLoop.h
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
// CFRunLoop.c
struct __CFRunLoopMode {
CFStringRef _name; // mode 類型,如:NSDefaultRunLoopMode
CFMutableSetRef _sources0; // CFRunLoopSourceRef
CFMutableSetRef _sources1; // CFRunLoopSourceRef
CFMutableArrayRef _observers; // CFRunLoopObserverRef
CFMutableArrayRef _timers; // CFRunLoopTimerRef
...
};
RunLoop 的常見模式
NSDefaultRunLoopMode / KCFRunLoopDefaultMode:默認(rèn)模式
UITrackingRunLoopMode:界面追蹤模式蚤告,用于 ScrollView 追蹤觸摸滑動努酸,保證界面滑動時不受其他 Mode 影響
NSRunLoopCommonModes / KCFRunLoopCommonModes:通用模式(默認(rèn)包含 KCFRunLoopDefaultMode 和 UITrackingRunLoopMode)
該模式不是實際存在的一種模式,它只是一個特殊的標(biāo)記杜恰,是同步Source0/Source1/Timer/Observer到多個 Mode 中的技術(shù)方案获诈。
被標(biāo)記為通用模式的Source0/Source1/Timer/Observer都會存放到 _commonModeItems 集合中,會同步這些Source0/Source1/Timer/Observer到多個 Mode 中心褐。
注意:
NSDefaultRunLoopMode和NSRunLoopCommonModes屬于Foundation框架舔涎;
KCFRunLoopDefaultMode和KCFRunLoopCommonModes屬于Core Foundation框架;
前者是對后者的封裝檬寂,作用相同终抽。
CFRunLoopModeRef 這樣設(shè)計有什么好處?Runloop為什么會有多個 Mode桶至?
Mode 做到了屏蔽的效果昼伴,當(dāng)RunLoop運行在 Mode1 下面的時候,是處理不了 Mode2 的事件的镣屹,有多個 Mode 使得其在處理不同類型任務(wù)時更加靈活圃郊、高效和可控。
比如NSDefaultRunLoopMode默認(rèn)模式和UITrackingRunLoopMode滾動模式女蜈,滾動屏幕的時候就會切換到滾動模式持舆,就不用去處理默認(rèn)模式下的事件了,保證了 UITableView 等的滾動順暢伪窖。
CFRunLoopSourceRef
在RunLoop中有兩個很重要的概念逸寓,一個是上面提到的模式,還有一個就是事件源覆山。事件源分為輸入源(Input Sources)和定時器源(Timer Sources)兩種竹伸;
輸入源(Input Sources)又分為Source0和Source1兩種,以下__CFRunLoopSource中的共用體union中的version0和version1就分別對應(yīng)Source0和Source1簇宽。
// CFRunLoop.h
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;
// CFRunLoop.m
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; /* immutable, except invalidation */
CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
} _context;
};
Source0 和 Source1的區(qū)別
Source0:需要手動喚醒線程:添加Source0到RunLoop并不會主動喚醒線程勋篓,需要手動喚醒)例如: 觸摸事件處理吧享、performSelector:onThread:
Source1:具備喚醒線程的能力,例如基于 Port 的線程間通信和系統(tǒng)事件捕捉譬嚣。
系統(tǒng)事件捕捉是由Source1來處理钢颂,然后再交給Source0處理
CFRunLoopTimerRef
CFRunloopTimer和NSTimer是 toll-free bridged 的,可以相互轉(zhuǎn)換拜银;
performSelector:withObject:afterDelay:方法會創(chuàng)建timer并添加到RunLoop中殊鞭。
// CFRunLoop.h
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
// CFRunLoop.c
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop; // 添加該 timer 的 RunLoop
CFMutableSetRef _rlModes; // 所有包含該 timer 的 modeName
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable 理想時間間隔 */
CFTimeInterval _tolerance; /* mutable 時間偏差 */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable 回調(diào)入口 */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
CFRunLoopObserverRef
CFRunLoopObserverRef用來監(jiān)聽RunLoop的 6 種活動狀態(tài)。
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入 RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timers
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Sources
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出 RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 表示以上所有狀態(tài)
};
UI 刷新(BeforeWaiting)
Autorelease pool(BeforeWaiting)
// CFRunLoop.h
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
// CFRunLoop.c
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop; // 添加該 observer 的 RunLoop
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable 監(jiān)聽的活動狀態(tài) */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable 回調(diào)入口 */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
CFRunLoopObserverRef中的_activities用來保存RunLoop的活動狀態(tài)盐股。
當(dāng)RunLoop的狀態(tài)發(fā)生改變時钱豁,通過回調(diào)_callout通知所有監(jiān)聽這個狀態(tài)的Observer。
三疯汁、事件循環(huán)機(jī)制
主線程的 RunLoop 的啟動過程
首先我們來看一下主線程的RunLoop的啟動過程。
上面我們說過卵酪,iOS 程序能保持持續(xù)運行的原因就是在main()函數(shù)中調(diào)用了UIApplicationMain函數(shù)幌蚊,這個函數(shù)內(nèi)部會啟動主線程的RunLoop。
打斷點溃卡,通過 LLDB 指令bt查看函數(shù)調(diào)用棧如下
可以看到溢豆,在UIApplicationMain函數(shù)中調(diào)用了 Core Foundation 框架下的CFRunLoopRunSpecific函數(shù)。
CFRunLoopRunSpecific 函數(shù)實現(xiàn):
RunLoop 的入口瘸羡。
查看源碼中該函數(shù)的實現(xiàn)漩仙,如下:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
......
其他邏輯校驗等
......
// 通知 Observers:即將進(jìn)入 RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// RunLoop 具體要做的事情
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知 Observers:即將退出 RunLoop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
從函數(shù)調(diào)用棧,以及CFRunLoopRunSpecific函數(shù)的實現(xiàn)中可以得知犹赖,RunLoop事件循環(huán)的實現(xiàn)機(jī)制體現(xiàn)在__CFRunLoopRun函數(shù)中队他。
__CFRunLoopRun 函數(shù)實現(xiàn):
事件循環(huán)的實現(xiàn)機(jī)制。
由于該函數(shù)實現(xiàn)較復(fù)雜峻村,以下為刪掉細(xì)節(jié)的精簡版本麸折,想探究具體的可以查看Core Foundation 源碼
/**
* __CFRunLoopRun
*
* @param rl 運行的 RunLoop 對象
* @param rlm 運行的 mode
* @param seconds loop 超時時間
* @param stopAfterHandle true: RunLoop 處理完事件就退出 false:一直運行直到超時或者被手動終止
* @param previousMode 上一次運行的 mode
*
* @return 返回 4 種狀態(tài)
*/
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode)
{
......
int32_t retVal = 0;
do {
// 通知 Observers:即將處理 Timers
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 Observers:即將處理 Sources
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 處理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 處理 Sources0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 處理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
// 判斷有無 Source1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
// 如果有 Source1,就跳轉(zhuǎn)到 handle_msg
goto handle_msg;
}
// 通知 Observers:即將進(jìn)入休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
// ??休眠粘昨,等待消息來喚醒線程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
__CFRunLoopUnsetSleeping(rl);
// 通知 Observers:剛從休眠中喚醒
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg:
if (被 Timer 喚醒) {
// 處理 Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
} else if (被 GCD 喚醒) {
// 處理 GCD
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被 Source1 喚醒
// 處理 Source1
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;
}
......
// 處理 Blocks
__CFRunLoopDoBlocks(rl, rlm);
/* 設(shè)置返回值 */
// 進(jìn)入 loop 時參數(shù)為處理完事件就返回
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
// 超出傳入?yún)?shù)標(biāo)記的超時時間
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
// 被外部調(diào)用者強(qiáng)制停止
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
// 自動停止
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
// mode 中沒有任何的 Source0/Source1/Timer/Observer
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
......
return retVal;
}
從該函數(shù)實現(xiàn)中可以得知RunLoop主要就做以下幾件事情:
__CFRunLoopDoObservers:通知Observers接下來要做什么
__CFRunLoopDoBlocks:處理Blocks
__CFRunLoopDoSources0:處理Sources0
__CFRunLoopDoSources1:處理Sources1
__CFRunLoopDoTimers:處理Timers
處理 GCD 相關(guān):dispatch_async(dispatch_get_main_queue(), ^{ });
__CFRunLoopSetSleeping/__CFRunLoopUnsetSleeping:休眠等待/結(jié)束休眠
__CFRunLoopServiceMachPort -> mach-msg():轉(zhuǎn)移當(dāng)前線程的控制權(quán)
__CFRunLoopServiceMachPort 函數(shù)實現(xiàn):
RunLoop 休眠的實現(xiàn)原理垢啼。
在__CFRunLoopRun函數(shù)中,會調(diào)用__CFRunLoopServiceMachPort函數(shù)张肾,該函數(shù)中調(diào)用了mach_msg()函數(shù)來轉(zhuǎn)移當(dāng)前線程的控制權(quán)給內(nèi)核態(tài)/用戶態(tài)芭析。
沒有消息需要處理時,休眠線程以避免資源占用吞瞪。調(diào)用mach_msg()從用戶態(tài)切換到內(nèi)核態(tài)馁启,等待消息;
有消息需要處理時尸饺,立刻喚醒線程进统,調(diào)用mach_msg()回到用戶態(tài)處理消息助币。
這就是RunLoop休眠的實現(xiàn)原理,也是RunLoop與簡單的do...while循環(huán)區(qū)別:
RunLoop:休眠的時候螟碎,當(dāng)前線程不會做任何事眉菱,CPU 不會再分配資源;
簡單的do...while循環(huán):當(dāng)前線程并沒有休息掉分,一直占用 CPU 資源俭缓。
static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout, voucher_mach_msg_state_t *voucherState, voucher_t *voucherCopy) {
Boolean originalBuffer = true;
kern_return_t ret = KERN_SUCCESS;
for (;;) { /* In that sleep of death what nightmares may come ... */
mach_msg_header_t *msg = (mach_msg_header_t *)*buffer;
msg->msgh_bits = 0;
msg->msgh_local_port = port;
msg->msgh_remote_port = MACH_PORT_NULL;
msg->msgh_size = buffer_size;
msg->msgh_id = 0;
if (TIMEOUT_INFINITY == timeout) { CFRUNLOOP_SLEEP(); } else { CFRUNLOOP_POLL(); }
// ??????
ret = mach_msg(msg, MACH_RCV_MSG|(voucherState ? MACH_RCV_VOUCHER : 0)|MACH_RCV_LARGE|((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0)|MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0)|MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV), 0, msg->msgh_size, port, timeout, MACH_PORT_NULL);
// Take care of all voucher-related work right after mach_msg.
// If we don't release the previous voucher we're going to leak it.
voucher_mach_msg_revert(*voucherState);
// Someone will be responsible for calling voucher_mach_msg_revert. This call makes the received voucher the current one.
*voucherState = voucher_mach_msg_adopt(msg);
if (voucherCopy) {
if (*voucherState != VOUCHER_MACH_MSG_STATE_UNCHANGED) {
// Caller requested a copy of the voucher at this point. By doing this right next to mach_msg we make sure that no voucher has been set in between the return of mach_msg and the use of the voucher copy.
// CFMachPortBoost uses the voucher to drop importance explicitly. However, we want to make sure we only drop importance for a new voucher (not unchanged), so we only set the TSD when the voucher is not state_unchanged.
*voucherCopy = voucher_copy();
} else {
*voucherCopy = NULL;
}
}
CFRUNLOOP_WAKEUP(ret);
if (MACH_MSG_SUCCESS == ret) {
*livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL;
return true;
}
if (MACH_RCV_TIMED_OUT == ret) {
if (!originalBuffer) free(msg);
*buffer = NULL;
*livePort = MACH_PORT_NULL;
return false;
}
if (MACH_RCV_TOO_LARGE != ret) break;
buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE);
if (originalBuffer) *buffer = NULL;
originalBuffer = false;
*buffer = realloc(*buffer, buffer_size);
}
HALT;
return false;
}
四、RunLoop與線程
RunLoop官方文檔以及RunLoop的數(shù)據(jù)結(jié)構(gòu)中可知酥郭,Runloop和線程的關(guān)系:
RunLoop對象和線程是一一對應(yīng)關(guān)系华坦;
RunLoop保存在一個全局的Dictionary里,線程作為key不从,RunLoop作為value惜姐;
如果沒有RunLoop,線程執(zhí)行完任務(wù)就會退出椿息;如果沒有RunLoop歹袁,主線程執(zhí)行完main()函數(shù)就會退出,程序就不能處于運行狀態(tài)寝优;
RunLoop創(chuàng)建時機(jī):線程剛創(chuàng)建時并沒有RunLoop對象条舔,RunLoop會在第一次獲取它時創(chuàng)建;
RunLoop銷毀時機(jī):RunLoop會在線程結(jié)束時銷毀乏矾;
主線程的RunLoop自動獲让峡埂(創(chuàng)建),子線程默認(rèn)沒有開啟RunLoop钻心;
主線程的RunLoop對象是在UIApplicationMain中通過[NSRunLoop currentRunLoop]獲取凄硼,一旦發(fā)現(xiàn)它不存在,就會創(chuàng)建RunLoop對象扔役。
未啟動 RunLoop 的子線程
創(chuàng)建一個NSThread的子類HTThread并重寫了dealloc方法來觀察線程的狀態(tài)帆喇。
執(zhí)行以下代碼,發(fā)現(xiàn)子線程執(zhí)行完一次test任務(wù)就退出銷毀了亿胸,沒有再執(zhí)行test任務(wù)坯钦,原因就是沒有啟動該線程的RunLoop。
@implementation HFThread
- (void)dealloc {
NSLog(@"%s ",__func__);
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
HFThread *thread = [[HFThread alloc]initWithTarget:self selector:@selector(threadAction) object:nil];
[thread start];
[self performSelector:@selector(threadAction) onThread:thread withObject:nil waitUntilDone:NO];
}
- (void)threadAction {
NSLog(@"test on %@", [NSThread currentThread]);
}
打印輸出:
test on <HFThread: 0x600001a517c0>{number = 7, name = (null)}
[HFThread dealloc]
開啟子線程的 RunLoop 的過程
獲取 RunLoop 對象
上面提到過可以使用了獲取runloop對象的方法:
//Foundation
[NSRunLoop mainRunLoop]; // 獲取主線程的 RunLoop 對象
[NSRunLoop currentRunLoop]; // 獲取當(dāng)前線程的 RunLoop 對象
// Core Foundation
CFRunLoopGetMain(); // 獲取主線程的 RunLoop 對象
CFRunLoopGetCurrent(); // 獲取當(dāng)前線程的 RunLoop 對象
再來看一下CFRunLoopGetCurrent()函數(shù)是怎么獲取RunLoop對象的:
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self()); // 調(diào)用 _CFRunLoopGet0 函數(shù)并傳入當(dāng)前線程
}
_CFRunLoopGet0內(nèi)部實現(xiàn):
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// ?當(dāng)前線程作為 Key侈玄,從 __CFRunLoops 字典中獲取 RunLoop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) { // ?如果字典中不存在
CFRunLoopRef newLoop = __CFRunLoopCreate(t); // 創(chuàng)建當(dāng)前線程的 RunLoop
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
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)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
啟動子線程的RunLoop
可以通過以下方式來啟動子線程的RunLoop:
// Foundation
[[NSRunLoop currentRunLoop] run];
[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
// Core Foundation
CFRunLoopRun();
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
// 第3個參數(shù):設(shè)置為 true婉刀,代表執(zhí)行完 Source/Port 后就會退出當(dāng)前 loop
- (void)threadAction {
NSLog(@"test on %@", [NSThread currentThread]);
[[NSRunLoop currentRunLoop] run];
}
打印輸出
test on <HFThread: 0x6000006d43c0>{number = 7, name = (null)}
test on <HFThread: 0x6000006d43c0>{number = 7, name = (null)}
CFRunLoopRun()/CFRunLoopRunInMode()函數(shù)是怎么啟動RunLoop的:
void CFRunLoopRun(void) {
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) {
CHECK_FOR_FORK();
return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}
CFRunLoopRunSpecific在上面描述事件循環(huán)機(jī)制的時候詳細(xì)講解過。
實現(xiàn)一個常駐線程
好處:
可以長期執(zhí)行任務(wù)序仙,適用于需要持續(xù)執(zhí)行的后臺務(wù)突颊,例如網(wǎng)絡(luò)請求,定時任務(wù)等。
減少資源消耗律秃,常駐線程不需要頻繁的創(chuàng)建銷毀線程爬橡,可以減少資源消耗和系統(tǒng)開銷。
提高響應(yīng)速度棒动,常駐線程中的光任務(wù)可以減少線程切換的開銷糙申,提高響應(yīng)速度,特別適合需要頻繁執(zhí)行任務(wù)的場景船惨。
設(shè)計條件:該任務(wù)需是串行的柜裸,而非并發(fā);
設(shè)計步驟:
1粱锐、獲取/創(chuàng)建當(dāng)前線程的RunLoop疙挺;
2、向該RunLoop中添加一個Source/Port等來維持RunLoop的事件循環(huán)(如果 Mode 里沒有任何Source0/Source1/Timer/Observer怜浅,RunLoop會立馬退出)铐然;
3、啟動該RunLoop海雪。
#import "HFNextViewViewController.h"
#import "HFThread.h"
@interface HFNextViewViewController ()
@property (nonatomic, assign, getter=isStoped) BOOL stopped;
@property (nonatomic, strong) HFThread *thread;
@end
@implementation HFNextViewViewController
- (void)dealloc
{
NSLog(@"%s", __func__);
if (!self.thread) return;
// 在子線程調(diào)用(waitUntilDone設(shè)置為YES锦爵,代表子線程的代碼執(zhí)行完畢后,當(dāng)前方法才會繼續(xù)往下執(zhí)行)
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
self.view.backgroundColor = [UIColor whiteColor];
[self hf_addButton];
__weak typeof(self) weakSelf = self;
self.stopped = NO;
self.thread = [[HFThread alloc] initWithBlock:^{
NSLog(@"begin-----%@", [NSThread currentThread]);
// ① 獲取/創(chuàng)建當(dāng)前線程的 RunLoop
// ② 向該 RunLoop 中添加一個 Source/Port 等來維持 RunLoop 的事件循環(huán)
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStoped) {
// ③ 啟動該 RunLoop
/*
[[NSRunLoop currentRunLoop] run]
如果調(diào)用 RunLoop 的 run 方法奥裸,則會開啟一個永不銷毀的線程
因為 run 方法會通過反復(fù)調(diào)用 runMode:beforeDate: 方法,以運行在 NSDefaultRunLoopMode 模式下
換句話說沪袭,該方法有效地開啟了一個無限的循環(huán)湾宙,處理來自 RunLoop 的輸入源 Sources 和 Timers 的數(shù)據(jù)
*/
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
NSLog(@"end-----%@", [NSThread currentThread]);
}];
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
if (!self.thread) return;
[self performSelector:@selector(threadAction) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)threadAction {
NSLog(@"%s- - - - %@",__func__, [NSThread currentThread]);
}
// 停止子線程的 RunLoop
- (void)stopThread
{
// 設(shè)置標(biāo)記為 YES
self.stopped = YES;
// 停止 RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s-----%@", __func__, [NSThread currentThread]);
// 清空線程
self.thread = nil;
}
- (void)hf_addButton {
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
button.frame = CGRectMake(50, 140, 200, 200);
button.backgroundColor = [UIColor redColor];
[self.view addSubview:button];
[button addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
}
- (void)buttonAction:(UIButton *)button
{
NSLog(@"back= %@",button);
[self dismissViewControllerAnimated:YES completion:nil];
}
@end
#import "HFThread.h"
@implementation HFThread
- (void)dealloc {
NSLog(@"%s ",__func__);
}
@end
多次點擊HFNextViewViewController的view,然后退出此頁面冈绊,輸出日志
begin-----<HFThread: 0x600000d03340>{number = 8, name = (null)}
-[HFNextViewViewController threadAction]- - - - <HFThread: 0x600000d03340>{number = 8, name = (null)}
-[HFNextViewViewController threadAction]- - - - <HFThread: 0x600000d03340>{number = 8, name = (null)}
-[HFNextViewViewController threadAction]- - - - <HFThread: 0x600000d03340>{number = 8, name = (null)}
-[HFNextViewViewController dealloc]
-[HFNextViewViewController stopThread]-----<HFThread: 0x600000d03340>{number = 8, name = (null)}
end-----<HFThread: 0x600000d03340>{number = 8, name = (null)}
-[HFThread dealloc]
五侠鳄、RunLoop與NSTimer
NSTimer是由RunLoop來管理的,NSTimer其實就是CFRunLoopTimerRef死宣,他們之間是 toll-free bridged 的伟恶,可以相互轉(zhuǎn)換;
如果在子線程上使用NSTimer毅该,就必須開啟子線程的RunLoop博秫,否則定時器無法生效。
解決 tableview 滑動時 NSTimer 失效的問題
問題:
由上面的分析我們知道眶掌,RunLoop同一時間只能運行在一種模式下挡育,當(dāng)我們滑動tableview/scrollview的時候RunLoop會切換到UITrackingRunLoopMode界面追蹤模式下。如果我們的NSTimer是添加到RunLoop的KCFRunLoopDefaultMode/NSDefaultRunLoopMode默認(rèn)模式下的話朴爬,此時是會失效的即寒。
解決方案:
可以將NSTimer添加到RunLoop的KCFRunLoopCommonModes/NSRunLoopCommonModes通用模式下,來保證無論在默認(rèn)模式還是界面追蹤模式下NSTimer都可以執(zhí)行。
NSTimer的創(chuàng)建方式
// 方式一母赵、這種方式創(chuàng)建的NSTimer是自動添加到RunLoop模式下的
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"123");
}];
// 方式二逸爵、通過timerxxx開頭方法創(chuàng)建的NSTimer是不會自動添加到RunLoop中的,
// 所以一定要記得手動添加凹嘲,否則NSTimer不生效师倔。
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"123");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
CFRunLoopAddTimer 函數(shù)實現(xiàn)
CFRunLoopAddTimer()函數(shù)中會判斷傳入的modeName模式名稱是不是kCFRunLoopCommonModes通用模式。
是的話就會將timer添加到RunLoop的 _commonModeItems 集合中施绎,并同步該timer到 _commonModes 里的所有模式中溯革,
這樣無論在默認(rèn)模式還是界面追蹤模式下NSTimer都可以執(zhí)行。
void CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef rlt, CFStringRef modeName) {
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return;
if (!__CFIsValid(rlt) || (NULL != rlt->_runLoop && rlt->_runLoop != rl)) return;
__CFRunLoopLock(rl);
if (modeName == kCFRunLoopCommonModes) { // 判斷 modeName 是不是 kCFRunLoopCommonModes
CFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;
if (NULL == rl->_commonModeItems) { // 懶加載谷醉,判斷 _commonModeItems 是否為空致稀,是的話創(chuàng)建
rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);
}
CFSetAddValue(rl->_commonModeItems, rlt); // 將 timer 添加到 _commonModeItems 中
if (NULL != set) {
CFTypeRef context[2] = {rl, rlt}; // 將 timer 和 RunLoop 封裝到 context 中
/* add new item to all common-modes */
// 遍歷 commonModes,將 timer 添加到 commonModes 的所有模式下
CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);
CFRelease(set);
}
......
}
}
static void __CFRunLoopAddItemToCommonModes(const void *value, void *ctx) {
CFStringRef modeName = (CFStringRef)value;
CFRunLoopRef rl = (CFRunLoopRef)(((CFTypeRef *)ctx)[0]);
CFTypeRef item = (CFTypeRef)(((CFTypeRef *)ctx)[1]);
if (CFGetTypeID(item) == CFRunLoopSourceGetTypeID()) {
CFRunLoopAddSource(rl, (CFRunLoopSourceRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopObserverGetTypeID()) {
CFRunLoopAddObserver(rl, (CFRunLoopObserverRef)item, modeName);
} else if (CFGetTypeID(item) == CFRunLoopTimerGetTypeID()) {
CFRunLoopAddTimer(rl, (CFRunLoopTimerRef)item, modeName);
}
}
NSTimer 和 CADisplayLink 存在的問題
問題:
不準(zhǔn)時:NSTime和CADisplayLink底層都是基于RunLoop的CFRunLoopTimerRef的實現(xiàn)的俱尼,也就是說它們都依賴于RunLoop抖单。如果RunLoop的任務(wù)過于繁重,會導(dǎo)致它們不準(zhǔn)時遇八。
比如NSTimer每1.0秒就會執(zhí)行一次任務(wù)矛绘,Runloop每進(jìn)行一次循環(huán),就會看一下NSTimer的時間是否達(dá)到1.0秒刃永,是的話就執(zhí)行任務(wù)货矮。但是由于Runloop每一次循環(huán)的任務(wù)不一樣,所花費的時間就不固定斯够。假設(shè)第一次循環(huán)所花時間為 0.2s囚玫,第二次 0.3s,第三次 0.3s读规,則再過 0.2s 就會執(zhí)行NSTimer的任務(wù)抓督,這時候可能Runloop的任務(wù)過于繁重,第四次花了0.5s束亏,那加起來時間就是 1.3s铃在,導(dǎo)致NSTimer不準(zhǔn)時。
解決方案:
使用 GCD 的定時器碍遍。GCD 的定時器是直接跟系統(tǒng)內(nèi)核掛鉤的定铜,而且它不依賴于RunLoop,所以它非常的準(zhǔn)時雀久。
轉(zhuǎn)載參考:
深入淺出RoonLoop
相關(guān)鏈接:
Core Foundation 源碼
RunLoop官方文檔