一. RunLoop簡介
運行循環(huán)禁舷,在程序運行過程中循環(huán)做一些事情权逗,如果沒有Runloop程序執(zhí)行完畢就會立即退出举塔,如果有Runloop程序會一直運行,并且時時刻刻在等待用戶的輸入操作奉狈。RunLoop可以在需要的時候自己跑起來運行,在沒有操作的時候就停下來休息涩惑。充分節(jié)省CPU資源仁期,提高程序性能图焰。
應用場景
- 定時器(Timer)------- timer
- PerformSelector ---------- source0
- GCD Async Main Queue
- 只有 dispatch_async(dispatch_get_main_queue) 情況下是通過 runloop 去調(diào)度的
- 事件響應收捣、手勢識別皮假、界面刷新
- 網(wǎng)絡請求
- AutoreleasePool
二. RunLoop基本作用
- 保持程序持續(xù)運行藻雌,程序一啟動就會開一個主線程销睁,主線程一開起來就會跑一個主線程對應的RunLoop,RunLoop保證主線程不會被銷毀咆槽,也就保證了程序的持續(xù)運行
- 處理App中的各種事件(比如:觸摸事件来吩,定時器事件俐银,Selector事件等)
- 節(jié)省CPU資源岔绸,提高程序性能理逊,程序運行起來時,當什么操作都沒有做的時候盒揉,RunLoop就告訴CPU晋被,現(xiàn)在沒有事情做,我要去休息刚盈,這時CPU就會將其資源釋放出來去做其他的事情羡洛,當有事情做的時候RunLoop就會立馬起來去做事情
我們先通過API內(nèi)一張圖片來簡單看一下RunLoop內(nèi)部運行原理
通過圖片可以看出,RunLoop在跑圈過程中藕漱,當接收到Input sources 或者 Timer sources時就會交給對應的處理方去處理欲侮。當沒有事件消息傳入的時候崭闲,RunLoop就休息了
三. 主線程的 RunLoop
UIApplicationMain函數(shù)內(nèi)啟動了Runloop,程序不會馬上退出锈麸,而是保持運行狀態(tài)镀脂。因此每一個應用必須要有一個runloop,
我們知道主線程一開起來忘伞,就會跑一個和主線程對應的RunLoop薄翅,那么RunLoop一定是在程序的入口main函數(shù)中開啟。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
在UIApplicationMain函數(shù)中氓奈,開啟了一個和主線程相關的RunLoop翘魄,導致UIApplicationMain不會返回,一直在運行中舀奶,也就保證了程序的持續(xù)運行暑竟。
Runloop 源碼:
// 用DefaultMode啟動
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
RunLoop確實是do while通過判斷result的值實現(xiàn)的。因此育勺,我們可以把RunLoop看成一個死循環(huán)但荤。
四. RunLoop對象
- Fundation框架 (基于CFRunLoopRef的封裝) NSRunLoop對象
- CoreFoundation CFRunLoopRef對象
獲取 Runloop 對象
Foundation
[NSRunLoop currentRunLoop]; // 獲得當前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
Core Foundation
CFRunLoopGetCurrent(); // 獲得當前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象
CFRunLoopRef 開源代碼:https://opensource.apple.com/tarballs/CF/
五. RunLoop和線程間的關系
- 每條線程都有唯一的一個與之對應的RunLoop對象
- RunLoop保存在一個全局的Dictionary里,線程作為key,RunLoop作為value
- 主線程的RunLoop已經(jīng)自動創(chuàng)建好了涧至,子線程的RunLoop需要主動創(chuàng)建
- RunLoop在第一次獲取時創(chuàng)建腹躁,在線程結束時銷毀
相關源碼
// 拿到當前Runloop 調(diào)用_CFRunLoopGet0
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
// 查看_CFRunLoopGet0方法內(nèi)部
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);
// 根據(jù)傳入的主線程獲取主線程對應的RunLoop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主線程 將主線程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
// 從字典里面拿,將線程作為key從字典里獲取一個loop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果loop為空南蓬,則創(chuàng)建一個新的loop纺非,所以runloop會在第一次獲取的時候創(chuàng)建
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 創(chuàng)建好之后,以線程為key runloop為value赘方,一對一存儲在字典中烧颖,下次獲取的時候,則直接返回字典內(nèi)的runloop
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 之間是一一對應的炕淮,其關系是保存在一個 Dictionary 里。所以我們創(chuàng)建子線程RunLoop時跳夭,只需在子線程中獲取當前線程的RunLoop對象即可[NSRunLoop currentRunLoop];如果不獲取鳖悠,那子線程就不會創(chuàng)建與之相關聯(lián)的RunLoop,并且只能在一個線程的內(nèi)部獲取其 RunLoop
[NSRunLoop currentRunLoop];方法調(diào)用時优妙,會先看一下字典里有沒有存子線程相對用的RunLoop,如果有則直接返回RunLoop憎账,如果沒有則會創(chuàng)建一個套硼,并將與之對應的子線程存入字典中。當線程結束時胞皱,RunLoop會被銷毀邪意。
從源碼看出子線程只有第一次獲取 runloop 時才會創(chuàng)建
六. RunLoop結構體
__CFRunLoop結構體 源碼
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;
};
主要成員變量
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
CFRunLoopModeRef 其實是指向__CFRunLoopMode結構體的指針九妈,__CFRunLoopMode結構體源碼如下
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 */
};
主要成員變量
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
通過上面分析我們知道,CFRunLoopModeRef代表RunLoop的運行模式雾鬼,一個RunLoop包含若干個Mode萌朱,每個Mode又包含若干個Source0/Source1/Timer/Observer,而RunLoop啟動時只能選擇其中一個Mode作為currentMode策菜。
Source1/Source0/Timers/Observer分別代表什么
- Source1 : 基于Port的線程間通信
- Source0 : 觸摸事件晶疼,PerformSelectors
- Timers : 定時器,NSTimer
- Observer : 監(jiān)聽器又憨,用于監(jiān)聽RunLoop的狀態(tài)
觸摸事件屬于 source1 還是 source0
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
NSLog(@"點擊了屏幕");
}
打斷點之后打印堆棧信息翠霍,當xcode工具區(qū)打印的堆棧信息不全時,可以在控制臺通過“bt”指令打印完整的堆棧信息蠢莺,由堆棧信息中可以發(fā)現(xiàn)寒匙,觸摸事件確實是會觸發(fā)Source0事件。
performSelector 屬于 source1 還是 source0
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
});
可以發(fā)現(xiàn)PerformSelectors同樣是觸發(fā)Source0事件
當我們觸發(fā)了事件(觸摸/鎖屏/搖晃等)后躏将,由IOKit.framework生成一個 IOHIDEvent事件锄弱,而IOKit是蘋果的硬件驅動框架,由它進行底層接口的抽象封裝與系統(tǒng)進行交互傳遞硬件感應的事件祸憋,并專門處理用戶交互設備会宪,由IOHIDServices和IOHIDDisplays兩部分組成,其中IOHIDServices是專門處理用戶交互的夺衍,它會將事件封裝成IOHIDEvents對象狈谊,接著用mach port轉發(fā)給需要的App進程,隨后 Source1就會接收IOHIDEvent沟沙,之后再回調(diào)__IOHIDEventSystemClientQueueCallback()河劝,__IOHIDEventSystemClientQueueCallback()內(nèi)觸發(fā)Source0,Source0 再觸發(fā) _UIApplicationHandleEventQueue()矛紫。所以觸摸事件看到是在 Source0 內(nèi)的赎瞎。
總結:觸摸事件先通過 mach port 發(fā)送,封裝為 source1颊咬,之后又轉換為 source0
Timers 驗證
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"NSTimer ---- timer調(diào)用了");
}];
Observer
七. 詳解RunLoop相關類及作用
相關類
- CFRunLoopRef - 獲得當前RunLoop和主RunLoop
- CFRunLoopModeRef - RunLoop 運行模式务甥,只能選擇一種,在不同模式中做不同的操作
- CFRunLoopSourceRef - 事件源喳篇,輸入源
- CFRunLoopTimerRef - 定時器時間
- CFRunLoopObserverRef - 觀察者
1. CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的運行模式
一個 RunLoop 包含若干個 Mode敞临,每個Mode又包含若干個Source、Timer麸澜、Observer
每次RunLoop啟動時挺尿,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode
如果需要切換Mode,只能退出當前一次 Loop编矾,再重新指定一個Mode進入熟史,這樣做主要是為了分隔開不同組的Source、Timer窄俏、Observer蹂匹,讓其互不影響。如果Mode里沒有任何Source0/Source1/Timer/Observer凹蜈,RunLoop會立馬退出
注意:一種Mode中可以有多個Source(事件源限寞,輸入源,基于端口事件源例鍵盤觸摸等) Observer(觀察者踪区,觀察當前RunLoop運行狀態(tài)) 和Timer(定時器事件源)昆烁。但是必須至少有一個Source或者Timer,因為如果Mode為空缎岗,RunLoop運行到空模式不會進行空轉静尼,就會立刻退出。
系統(tǒng)默認注冊的5個Mode:
RunLoop 有五種運行模式传泊,其中常見的有1.2兩種
1. kCFRunLoopDefaultMode:App的默認Mode鼠渺,通常主線程是在這個Mode下運行
2. UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動眷细,保證界面滑動時不受其他 Mode 影響
3. UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode拦盹,啟動完成后就不再使用,會切換到kCFRunLoopDefaultMode
4. GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode溪椎,通常用不到
5. kCFRunLoopCommonModes: 這是一個占位用的Mode普舆,作為標記kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一種真正的Mode
2. CFRunLoopSourceRef事件源(輸入源)
source 分為兩種
- Source0:非基于Port的 用于用戶主動觸發(fā)的事件(點擊button 或點擊屏幕)
- Source1:基于Port的 通過內(nèi)核和其他線程相互發(fā)送消息(與內(nèi)核相關)
3. CFRunLoopObserverRef
CFRunLoopObserverRef是觀察者校读,能夠監(jiān)聽RunLoop的狀態(tài)改變
我們直接來看代碼沼侣,給RunLoop添加監(jiān)聽者,監(jiān)聽其運行狀態(tài)
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//創(chuàng)建監(jiān)聽者
/*
第一個參數(shù) CFAllocatorRef allocator:分配存儲空間 CFAllocatorGetDefault()默認分配
第二個參數(shù) CFOptionFlags activities:要監(jiān)聽的狀態(tài) kCFRunLoopAllActivities 監(jiān)聽所有狀態(tài)
第三個參數(shù) Boolean repeats:YES:持續(xù)監(jiān)聽 NO:不持續(xù)
第四個參數(shù) CFIndex order:優(yōu)先級歉秫,一般填0即可
第五個參數(shù) :回調(diào) 兩個參數(shù)observer:監(jiān)聽者 activity:監(jiān)聽的事件
*/
/*
所有事件
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6),// 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7),// 即將退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop進入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要處理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要處理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒來了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
// 給RunLoop添加監(jiān)聽者
/*
第一個參數(shù) CFRunLoopRef rl:要監(jiān)聽哪個RunLoop,這里監(jiān)聽的是主線程的RunLoop
第二個參數(shù) CFRunLoopObserverRef observer 監(jiān)聽者
第三個參數(shù) CFStringRef mode 要監(jiān)聽RunLoop在哪種運行模式下的狀態(tài)
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
/*
CF的內(nèi)存管理(Core Foundation)
凡是帶有Create蛾洛、Copy、Retain等字眼的函數(shù)雁芙,創(chuàng)建出來的對象轧膘,都需要在最后做一次release
GCD本來在iOS6.0之前也是需要我們釋放的,6.0之后GCD已經(jīng)納入到了ARC中兔甘,所以我們不需要管了
*/
CFRelease(observer);
}
八. RunLoop處理邏輯
源碼解析
// 共外部調(diào)用的公開的CFRunLoopRun方法谎碍,其內(nèi)部會調(diào)用CFRunLoopRunSpecific
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
// 經(jīng)過精簡的 CFRunLoopRunSpecific 函數(shù)代碼,其內(nèi)部會調(diào)用__CFRunLoopRun函數(shù)
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
// 通知Observers : 進入Loop
// __CFRunLoopDoObservers內(nèi)部會調(diào)用 __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__
函數(shù)
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// 核心的Loop邏輯
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知Observers : 退出Loop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
return result;
}
// 精簡后的 __CFRunLoopRun函數(shù)洞焙,保留了主要代碼
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);
}
// 如果有Sources1蟆淀,就跳轉到handle_msg標記處
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
goto handle_msg;
}
// 通知Observers:即將休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 進入休眠太援,等待其他消息喚醒
__CFRunLoopSetSleeping(rl);
__CFPortSetInsert(dispatchPort, waitSet);
do {
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
} while (1);
// 醒來
__CFPortSetRemove(dispatchPort, waitSet);
__CFRunLoopUnsetSleeping(rl);
// 通知Observers:已經(jīng)喚醒
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg: // 看看是誰喚醒了RunLoop,進行相應的處理
if (被Timer喚醒的) {
// 處理Timer
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
}
else if (被GCD喚醒的) {
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
} else { // 被Sources1喚醒的
__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply);
}
// 執(zhí)行Blocks
__CFRunLoopDoBlocks(rl, rlm);
// 根據(jù)之前的執(zhí)行結果扳碍,來決定怎么做,為retVal賦相應的值
if (sourceHandledThisLoop && stopAfterHandle) {
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
return retVal;
}
runloop 休眠實現(xiàn)原理
runloop 休眠時仙蛉,不占用 CPU笋敞,當有消息時,CPU 從內(nèi)核態(tài)發(fā)送消息荠瘪,喚醒線程夯巷。
九. RunLoop退出
主線程銷毀RunLoop退出
Mode中有一些Timer 、Source哀墓、 Observer趁餐,這些保證Mode不為空時保證RunLoop沒有空轉并且是在運行的,當Mode中為空的時候篮绰,RunLoop會立刻退出
我們在啟動RunLoop的時候可以設置什么時候停止
[NSRunLoop currentRunLoop]runUntilDate:<#(nonnull NSDate *)#>
[NSRunLoop currentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>
十. RunLoop API
1. runloop 啟動
NSRunLoop 有三種啟動方式:
- Unconditionally(run)
- With a set time limit(runUntilDate)
- In a particular mode(runMode:beforeDate:)
runUntilDate:
- 這個方法,會循環(huán)調(diào)用 runMode:beforeDate: 直到達到參數(shù) NSDate 所指定的時間吠各,也就是超時時間臀突。
run
- 這個方法,可以看做是 [runloop runUntilDate:[NSDate distantFuture]];
- 相當于 while 死循環(huán)贾漏,一直調(diào)用 runMode:beforeDate:
runMode:beforeDate:
- 這個方法是啟動一次 RunLoop候学,與前面兩個 API 大不相同,一次 loop 過后就退出 runloop
demo
- (void)setup{
NSLog(@"%@",[[NSRunLoop currentRunLoop] currentMode]);
//創(chuàng)建并啟動一個 thread
self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(threadTest) object:nil];
[self.thread setName:@"Test Thread"];
[self.thread start];
//向 RunLoop 發(fā)送消息的簡便方法纵散,系統(tǒng)會將消息傳遞到指定的 SEL 里面
[self performSelector:@selector(receiveMsg) onThread:self.thread withObject:nil waitUntilDone:NO];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//直接去的子線程 RunLoop 的一個 port梳码,并向其發(fā)送消息,這個是比較底層的 NSPort 方式進行線程通信
[self.port sendBeforeDate:[NSDate date] components:[@[[@"hello" dataUsingEncoding:NSUTF8StringEncoding]] mutableCopy] from:nil reserved:0];
});
}
- (void)threadTest{
NSLog(@"%@",@"child thread start");
//threadTest 這個方法是在 Test Thread 這個線程里面運行的伍掀。
NSLog(@"%@",[NSThread currentThread]);
//獲取這個線程的 RunLoop 并讓他運行起來
NSRunLoop* runloop = [NSRunLoop currentRunLoop];
self.port = [[NSMachPort alloc]init];
self.port.delegate = self;
[runloop addPort:self.port forMode:NSRunLoopCommonModes];
//約等于runUntilDate:[NSDate distantFuture]
[runloop run];
}
- (void)receiveMsg{
NSLog(@"%@",[NSThread currentThread]);
NSLog(@"%@",@"receive msg");
}
#pragma NSMachPortDelegate
- (void)handleMachMessage:(void *)msg{
NSLog(@"handle message thread:%@",[NSThread currentThread]);
}
2.runloop 退出
- 使用 run 啟動 RunLoop掰茶,直到程序結束再退出。主線程的 RunLoop 一定是這種方式啟動的硕盹。
- 使用 runUntileDate 啟動 RunLoop符匾,date 時間到退出。這里的 date 會當做參數(shù)傳到下面這個方法中瘩例,當 date 時間到啊胶,RunLoop 也會因為超時而結束一次循環(huán)。
- 使用 runMode:beforeDate: 這種方式啟動一次消息循環(huán)垛贤,并且自己編寫代碼來控制一次消息循環(huán)結束后是否啟動下一次消息循環(huán)焰坪。也就是類似這樣的代碼:
BOOL shouldKeepRunning = YES; // global
NSRunLoop *theRL = [NSRunLoop currentRunLoop];
while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);
3.線程保活封裝
使用 run 方法開啟 runloop 會使 runloop 一直存活聘惦,直到應用的生命周期結束某饰,這樣的線程無法銷毀。如果希望不使用時銷毀線程,需要調(diào)用 runMode:beforeDate 這個 API 結合 while 去實現(xiàn)黔漂〗刖。或者使用 C 語言的 API
方式一:
@interface MJPermenantThread()
@property (strong, nonatomic) MJThread *innerThread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;
@end
@implementation MJPermenantThread
#pragma mark - public methods
- (instancetype)init
{
if (self = [super init]) {
self.stopped = NO;
__weak typeof(self) weakSelf = self;
self.innerThread = [[MJThread alloc] initWithBlock:^{
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
while (weakSelf && !weakSelf.isStopped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}];
[self.innerThread start];
}
return self;
}
- (void)executeTask:(MJPermenantThreadTask)task
{
if (!self.innerThread || !task) return;
[self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}
- (void)stop
{
if (!self.innerThread) return;
[self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self stop];
}
#pragma mark - private methods
- (void)__stop
{
self.stopped = YES;
CFRunLoopStop(CFRunLoopGetCurrent());
self.innerThread = nil;
}
- (void)__executeTask:(MJPermenantThreadTask)task
{
task();
}
@end
方式二:
基于 CFRunLoop 的 API 不需要使用 while 循環(huán)實現(xiàn)永久 loop
@interface MJPermenantThread()
@property (strong, nonatomic) MJThread *innerThread;
@end
@implementation MJPermenantThread
#pragma mark - public methods
- (instancetype)init
{
if (self = [super init]) {
self.innerThread = [[MJThread alloc] initWithBlock:^{
NSLog(@"begin----");
// 創(chuàng)建上下文(要初始化一下結構體)
CFRunLoopSourceContext context = {0};
// 創(chuàng)建source
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
// 往Runloop中添加source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
// 銷毀source
CFRelease(source);
// 啟動,第三個參數(shù) returnAfterSourceHandled 為 false 表示永久 loop炬守,為 true 表示單次 loop
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
NSLog(@"end----");
}];
[self.innerThread start];
}
return self;
}
- (void)executeTask:(MJPermenantThreadTask)task
{
if (!self.innerThread || !task) return;
[self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}
- (void)stop
{
if (!self.innerThread) return;
[self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self stop];
}
#pragma mark - private methods
- (void)__stop
{
CFRunLoopStop(CFRunLoopGetCurrent());
self.innerThread = nil;
}
- (void)__executeTask:(MJPermenantThreadTask)task
{
task();
}
十一. RunLoop應用
1. 常駐線程
子線程執(zhí)行完操作之后就會立即釋放牧嫉,即使我們使用強引用引用子線程使子線程不被釋放,也不能給子線程再次添加操作减途,或者再次開啟酣藻。我們可以通過在線程中添加 runloop 的方式讓線程保活
demo
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic,strong)NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 創(chuàng)建子線程并開啟
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(show) object:nil];
self.thread = thread;
[thread start];
}
-(void)show
{
// 注意:打印方法一定要在RunLoop創(chuàng)建開始運行之前鳍置,如果在RunLoop跑起來之后打印辽剧,RunLoop先運行起來,已經(jīng)在跑圈了就出不來了税产,進入死循環(huán)也就無法執(zhí)行后面的操作了怕轿。
// 但是此時點擊Button還是有操作的,因為Button是在RunLoop跑起來之后加入到子線程的砖第,當Button加入到子線程RunLoop就會跑起來
NSLog(@"%s",__func__);
// 1.創(chuàng)建子線程相關的RunLoop撤卢,在子線程中創(chuàng)建即可,并且RunLoop中要至少有一個Timer 或 一個Source 保證RunLoop不會因為空轉而退出梧兼,因此在創(chuàng)建的時候直接加入
// 添加Source [NSMachPort port] 添加一個端口
[[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
// 添加一個Timer
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//創(chuàng)建監(jiān)聽者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"RunLoop進入");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"RunLoop要處理Timers了");
break;
case kCFRunLoopBeforeSources:
NSLog(@"RunLoop要處理Sources了");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"RunLoop要休息了");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop醒來了");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop退出了");
break;
default:
break;
}
});
// 給RunLoop添加監(jiān)聽者
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 2.子線程需要開啟RunLoop
[[NSRunLoop currentRunLoop]run];
CFRelease(observer);
}
- (IBAction)btnClick:(id)sender {
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
-(void)test
{
NSLog(@"%@",[NSThread currentThread]);
}
@end
注意:創(chuàng)建子線程相關的RunLoop放吩,在子線程中創(chuàng)建即可,并且RunLoop中要至少有一個Timer 或 一個Source 保證RunLoop不會因為空轉而退出羽杰,因此在創(chuàng)建的時候直接加入渡紫,如果沒有加入Timer或者Source,或者只加入一個監(jiān)聽者考赛,運行程序會崩潰
2. 自動釋放池
RunLoop內(nèi)部有一個自動釋放池惕澎,當RunLoop開啟時,就會自動創(chuàng)建一個自動釋放池颜骤,當RunLoop在休息之前會釋放掉自動釋放池的東西唧喉,然后重新創(chuàng)建一個新的空的自動釋放池,當RunLoop被喚醒重新開始跑圈時忍抽,Timer,Source等新的事件就會放到新的自動釋放池中八孝,當RunLoop退出的時候也會被釋放。
注意:只有主線程的RunLoop會默認啟動鸠项。也就意味著會自動創(chuàng)建自動釋放池干跛,子線程需要在線程調(diào)度方法中手動添加自動釋放池。
@autorelease{
// 執(zhí)行代碼
}
3.解決NSTimer在滑動時停止工作的問題
4.監(jiān)控應用卡頓
5.性能優(yōu)化
十二.面試題
1.runloop 和 GCD 的關系
1.RunLoop 的超時時間
RunLoop 中大量使用到了GCD祟绊,啟動 RunLoop 的超時時間就是使用 GCD 中的 dispatch_source_t來實現(xiàn)的
2.runloop 去執(zhí)行 GCD MainQueue 上的異步任務
dispatch_async(dispatch_get_main_queue(), block)產(chǎn)生的任務需要通過 runloop 去調(diào)度
所以 runloop 和 GCD 是相互調(diào)用依賴的關系
2.runloop 與 autorelease pool 的關系
第一個 Observer 監(jiān)視的事件是 Entry(即將進入Loop)楼入,其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池哥捕。其 order 是-2147483647,優(yōu)先級最高嘉熊,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前遥赚。
第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準備進入睡眠) 和 Exit(即將退出Loop),
BeforeWaiting(準備進入睡眠)時調(diào)用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 釋放舊的池并創(chuàng)建新池阐肤;
Exit(即將退出Loop) 時調(diào)用 _objc_autoreleasePoolPop() 來釋放自動釋放池鸽捻。這個 Observer 的 order 是 2147483647,優(yōu)先級最低泽腮,保證其釋放池子發(fā)生在其他所有回調(diào)之后。