一. RunLoop相關(guān)
什么是Runloop绿满?
顧名思義,Runloop就是運行循環(huán),就是在程序運行過程中循環(huán)做一些事情。
RunLoop的基本作用:
保持程序的持續(xù)運行
處理App中的各種事件(比如觸摸事件戚篙、定時器事件等)
節(jié)省CPU資源,提高程序性能:該做事時做事挽封,該休息時休息
......
1. UIApplicationMain里面的RunLoop
如果沒有RunLoop
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSLog(@"Hello, World!");
}
return 0;
}
執(zhí)行完打印已球,就會退出程序臣镣。
如果有了RunLoop
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
UIApplicationMain函數(shù)里面就是創(chuàng)建一個RunLoop對象辅愿,然后RunLoop對象一直循環(huán)等待和處理消息,偽代碼如下:
int main(int argc, char * argv[]) {
@autoreleasepool {
int retVal = 0;
do {
//睡眠中等待消息
int message = sleep_and_wait();
//處理消息
retVal = process_message(message);
} while (0 == retVal);
return 0;
}
}
這樣就保證了程序并不會馬上退出忆某,一直保持運行狀態(tài)点待。
2. RunLoop對象
iOS中有2套API來訪問和使用RunLoop
Foundation:NSRunLoop
Core Foundation:CFRunLoopRef
獲取RunLoop對象:
Foundation
[NSRunLoop currentRunLoop]; // 獲得當(dāng)前線程的RunLoop對象
[NSRunLoop mainRunLoop]; // 獲得主線程的RunLoop對象
Core Foundation
CFRunLoopGetCurrent(); // 獲得當(dāng)前線程的RunLoop對象
CFRunLoopGetMain(); // 獲得主線程的RunLoop對象
- NSRunLoop和CFRunLoopRef都代表著RunLoop對象
- NSRunLoop是基于CFRunLoopRef的一層OC包裝
- CFRunLoopRef是開源的:Core Foundation源碼
他們兩者之間的關(guān)系如下圖:
NSRunLoop就是對CFRunLoopRef的一層包裝,就好像NSArray是對CFArrayRef的封裝弃舒,NSString是對CFStringRef的封裝一樣癞埠。
獲取當(dāng)前線程、主線程RunLoop方法如下:
NSLog(@"%p %p", [NSRunLoop currentRunLoop], [NSRunLoop mainRunLoop]);
NSLog(@"%p %p", CFRunLoopGetCurrent(), CFRunLoopGetMain());
NSLog(@"%@", [NSRunLoop mainRunLoop]);
打恿亍:
0x600001141260 0x600001141260
0x600000940a00 0x600000940a00
<CFRunLoop 0x600000940a00 [0x1089e6ae8]>{wakeup port = 0x1d07, stopped = false, ignoreWakeUps = false,
current mode = kCFRunLoopDefaultMode,
......
打印的地址不一樣苗踪,可以發(fā)現(xiàn)使用%@打印mainRunLoop,里面裝的的確是CFRunLoop削锰,通過CFRunLoop的地址也可以驗證:NSRunLoop的確是堆CFRunLoopRef的一層封裝通铲,所以地址不一樣。
3. RunLoop與線程
- 每條線程都有唯一的一個與之對應(yīng)的RunLoop對象器贩,主線程的RunLoop已經(jīng)自動獲取(創(chuàng)建)颅夺,子線程默認(rèn)沒有開啟RunLoop,RunLoop會在線程結(jié)束時銷毀蛹稍。
- RunLoop是懶加載的吧黄,線程剛創(chuàng)建時并沒有RunLoop對象,RunLoop會在第一次獲取它時創(chuàng)建唆姐。
- 主線程幾乎所有的事情都是交給了runloop去做拗慨,比如UI界面的刷新、點擊時間的處理奉芦、performSelector等等赵抢。
- RunLoop保存在一個全局的Dictionary里,線程作為key仗阅,RunLoop作為value昌讲。
下載Core Foundation源碼,拖到新創(chuàng)建的命令行項目中减噪,查看CFRunLoopGetCurrent源碼實現(xiàn):
CFRunLoopRef CFRunLoopGetCurrent(void) {
CHECK_FOR_FORK();
CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
if (rl) return rl;
return _CFRunLoopGet0(pthread_self());
}
進入_CFRunLoopGet0:
static CFMutableDictionaryRef __CFRunLoops = NULL; //是個字典短绸,線程作為key车吹,取出對應(yīng)的RunLoop
static CFLock_t loopsLock = CFLockInit;
// 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);
}
//將字典和key(線程)傳入CFDictionaryGetValue函數(shù),獲取RunLoop對象
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) { //如果RunLoop不存在
CFRunLoopRef newLoop = __CFRunLoopCreate(t); //就創(chuàng)建RunLoop
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);//并且將RunLoop保存到字典里面
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;
}
其中__CFRunLoops是個字典,線程作為key醋闭,取出對應(yīng)的RunLoop窄驹,如果沒有,再創(chuàng)建证逻,然后保存到字典中乐埠,驗證了RunLoop對象的確是懶加載的。
4. RunLoop相關(guān)的類
Core Foundation中關(guān)于RunLoop的5個類
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
在源碼中查看囚企,CFRunLoopRef:
typedef struct CF_BRIDGED_MUTABLE_TYPE(id) __CFRunLoop * CFRunLoopRef;
可以發(fā)現(xiàn)CFRunLoopRef是指向__CFRunLoop結(jié)構(gòu)體的指針丈咐,找到__CFRunLoop結(jié)構(gòu)體源碼:
struct __CFRunLoop {
......
pthread_t _pthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;//當(dāng)前模式
CFMutableSetRef _modes; //是個集合,無序的龙宏,里面裝的是CFRunLoopModeRef類型的對象
......
};
__CFRunLoop里面有個_modes棵逊,它是個集合,里面裝的是一堆CFRunLoopModeRef類型的對象银酗,當(dāng)前的模式是_currentMode辆影。
集合是無序的,數(shù)組是有序的黍特,集合只能通過anyObject取值蛙讥,數(shù)組可以通過索引取值,如下:
// 有序的
NSMutableArray *array;
[array addObject:@"123"];
array[0];
// 無序的
NSMutableSet *set;
[set addObject:@"123"];
[set anyObject];
進入CFRunLoopModeRef:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
......
CFStringRef _name;//名稱
CFMutableSetRef _sources0;//里面裝的是CFRunLoopSourceRef類型的對象
CFMutableSetRef _sources1;//里面裝的是CFRunLoopSourceRef類型的對象
CFMutableArrayRef _observers;//里面裝的是CFRunLoopObserverRef類型的對象
CFMutableArrayRef _timers;//里面裝的是CFRunLoopTimerRef類型的對象
......
};
① 5個類之間的關(guān)系:
CFRunLoopRef里面有個_modes集合灭衷,里面裝好多CFRunLoopModeRef類型的模式次慢,_currentMode是當(dāng)前模式。
模式里面有name今布,_sources0经备、_sources1集合(里面裝的是CFRunLoopSourceRef類型的東西),_observers數(shù)組(里面裝的是CFRunLoopObserverRef類型的東西)部默,_timers數(shù)組(里面裝的是CFRunLoopTimerRef類型的東西)侵蒙。
如下圖所示:
- CFRunLoopModeRef代表RunLoop的運行模式
- 一個RunLoop包含若干個Mode,每個Mode又包含若干個Source0/Source1/Timer/Observer
- RunLoop啟動時只能選擇其中一個Mode傅蹂,作為currentMode纷闺,如果需要切換Mode,只能退出當(dāng)前Loop份蝴,再重新選擇一個Mode進入
- 不同組的Source0/Source1/Timer/Observer能分隔開來犁功,互不影響
- 如果Mode里沒有任何Source0/Source1/Timer/Observer,RunLoop會立馬退出
- 其中Timer是定時器婚夫,平時創(chuàng)建的一些定時器都放在這里浸卦,Observer是監(jiān)聽器,source0/Source1是事件案糙,比如點擊事件限嫌、performSelector等等靴庆。
② 為什么多種模式要分開呢?
比如scrollView滾動的時候讓它切換到滾動模式怒医,那么在滾動模式下炉抒,scrollView就專心處理滾動相關(guān)的就可以了,以前模式下的事情就不處理了稚叹。如果不滾動焰薄,在正常模式下,就專心處理正常模式下的事情就好了扒袖,這樣可以做到流暢不卡頓塞茅。
5. 常見的兩種Mode
kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默認(rèn)Mode,通常主線程是在這個Mode下運行僚稿。
UITrackingRunLoopMode:界面跟蹤 Mode凡桥,用于 ScrollView 追蹤觸摸滑動蟀伸,保證界面滑動時不受其他 Mode 影響蚀同。
常用的就是默認(rèn)模式和界面跟蹤模式,其他模式基本用不到啊掏,都懶得說了蠢络。
6. Source0、Source1迟蜜、Timers刹孔、Observers
RunLoop切換到某個模式就開始處理那個模式的Source0、Source1娜睛、Timers髓霞、Observers,那么這些東西分別代表什么呢畦戒?
Source0:觸摸事件處理方库、performSelector:onThread:。
Source1:基于Port(端口)的線程間通信障斋、系統(tǒng)事件捕捉 (比如點擊事件纵潦,通過Source1捕捉,然后包裝成Source0進行處理)垃环。
Timers:NSTimer邀层、performSelector:withObject:afterDelay:(底層就是NSTimer)。
Observers:用于監(jiān)聽RunLoop的狀態(tài)遂庄、UI刷新(BeforeWaiting)寥院、Autorelease pool(BeforeWaiting)。
比如涛目,點擊界面空白就是Source0事件秸谢,驗證如下:
在touchesBegan:withEvent:方法打斷點
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//打斷點
}
查看函數(shù)調(diào)用棧:
可以發(fā)現(xiàn)的確是通過source0處理事件经磅,最后才調(diào)用到touchesBegan:withEvent:方法的。
關(guān)于Observers監(jiān)聽UI刷新(BeforeWaiting):
比如以下代碼就屬于UI刷新:
self.view.backgroundColor = [UIColor redColor];
這句代碼并不是立馬執(zhí)行钮追,Observers會先記下來预厌,當(dāng)Observers監(jiān)聽到RunLoop將要睡覺啦,就在RunLoop將要睡覺之前執(zhí)行(刷新UI)元媚。
同理Autorelease pool也是一樣轧叽,當(dāng)Observers監(jiān)聽到RunLoop將要睡覺啦,就在RunLoop睡覺之前釋放對象刊棕。
7. RunLoop狀態(tài)
上面說了炭晒,Observers可以監(jiān)聽RunLoop的狀態(tài),那么RunLoop有幾種狀態(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), //即將從休眠中喚醒
kCFRunLoopExit = (1UL << 7), //即將退出RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU //所有狀態(tài)
};
8. 如何監(jiān)聽RunLoop的狀態(tài)网严?
Observers數(shù)組里面有系統(tǒng)創(chuàng)建的一些Observer,用于監(jiān)聽RunLoop狀態(tài)進行UI刷新嗤无、Autorelease pool等震束,如果我們自己想監(jiān)聽RunLoop狀態(tài)肯定要自己創(chuàng)建Observer。
// 創(chuàng)建Observer
/*
參數(shù)一:一般傳默認(rèn)的:kCFAllocatorDefault
參數(shù)二:傳入你想監(jiān)聽什么狀態(tài)
參數(shù)三:是否重復(fù)監(jiān)聽
參數(shù)四:順序0当犯,不需要考慮順序
參數(shù)五:監(jiān)聽函數(shù)名
參數(shù)六:context垢村,會傳入監(jiān)聽函數(shù)的info里面,一般傳NULL
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
// 添加Observer到RunLoop中
//kCFRunLoopCommonModes通用模式嚎卫,默認(rèn)包括kCFRunLoopDefaultMode和UITrackingRunLoopMode
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 釋放
CFRelease(observer);
//監(jiān)聽RunLoop的函數(shù)嘉栓,這個函數(shù)要求傳入三個參數(shù)
void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
}
當(dāng)然也可以通過block監(jiān)聽,和上面一樣的:
// 創(chuàng)建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
......
}
});
// 添加Observer到RunLoop中
//kCFRunLoopCommonModes通用模式拓诸,默認(rèn)包括kCFRunLoopDefaultMode和UITrackingRunLoopMode
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 釋放
CFRelease(observer);
① 監(jiān)聽點擊空白事件
點擊空白侵佃,打印如下:
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources 即將處理source事件
-[ViewController touchesBegan:withEvent:]
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
......
可以看出先是kCFRunLoopBeforeSources狀態(tài),之后再執(zhí)行點擊事件方法奠支,這也驗證了點擊事件是source0事件馋辈。
② 監(jiān)聽定時器事件
添加定時器:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"定時器-----------");
}];
// NSLog(@"%s",__func__);
}
點擊空白,打优呋隆:
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting 先睡覺
kCFRunLoopAfterWaiting 3秒后喚醒
定時器-----------
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
kCFRunLoopBeforeWaiting
kCFRunLoopAfterWaiting
kCFRunLoopBeforeTimers
kCFRunLoopBeforeSources
可以發(fā)現(xiàn)首有,先睡覺,睡了3秒后再執(zhí)行定時器事件枢劝。
③ 監(jiān)聽模式切換
主view里面添加一個textView井联,寫如下代碼監(jiān)聽RunLoop模式改變:
// 創(chuàng)建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopEntry - %@", mode);
CFRelease(mode);
break;
}
case kCFRunLoopExit: {
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopExit - %@", mode);
CFRelease(mode);
break;
}
default:
break;
}
});
// 添加Observer到RunLoop中
//kCFRunLoopCommonModes通用模式,默認(rèn)包括kCFRunLoopDefaultMode和UITrackingRunLoopMode
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 釋放
CFRelease(observer);
滑動textView您旁,打永映!:
kCFRunLoopExit - kCFRunLoopDefaultMode
kCFRunLoopEntry - UITrackingRunLoopMode
kCFRunLoopExit - UITrackingRunLoopMode
kCFRunLoopEntry - kCFRunLoopDefaultMode
可以發(fā)現(xiàn),先退出默認(rèn)模式進入滑動模式,再退出滑動模式進入默認(rèn)模式蚕脏。
二. RunLoop的運行流程
1. 流程圖
蘋果官方的流程圖:
官方的圖有點抽象侦副,看MJ老師的圖:
對于第4步的block就是下面的block
CFRunLoopPerformBlock(<#CFRunLoopRef rl#>, <#CFTypeRef mode#>, ^{
//傳入RunLoop和模式,RunLoop就會執(zhí)行這個block里面的代碼
})
2. 源碼驗證
仔細(xì)分析上圖流程之后驼鞭,下面我們查看源碼秦驯,看看到底是不是這樣的,但是RunLoop的入口是哪個呢挣棕?打斷點译隘,查看函數(shù)調(diào)用棧,如下:
可以發(fā)現(xiàn)第一個關(guān)于RunLoop的函數(shù)是CFRunLoopRunSpecific洛心,是Core Foundation框架下的函數(shù)固耘。在Core Foundation源碼中搜索“ CFRunLoopRunSpecific”,由于原函數(shù)比較復(fù)雜词身,精簡后如下:
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
//通知Observers進入Loop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
//具體要做的事情
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
//通知Observers退出Loop
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
//返回事情做完后的結(jié)果
return result;
}
進入__CFRunLoopRun函數(shù)厅目,這個函數(shù)更復(fù)雜,精簡后如下:
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);
// 處理Source0
if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
// 如果返回YES,處理Blocks
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
// 判斷有無Source1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
// 如果有Source1就跳轉(zhuǎn)handle_msg
goto handle_msg;
}
// 通知Observers渐夸,即將休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
// 開始休眠
__CFRunLoopSetSleeping(rl);
CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
// 等待別的消息來喚醒當(dāng)前線程嗤锉,往下走代表有人喚醒它了
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
// 結(jié)束休眠
__CFRunLoopUnsetSleeping(rl);
// 通知Observers,結(jié)束休眠
__CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
handle_msg:
if (被Timer喚醒) {
// 處理Timers
__CFRunLoopDoTimers(rl, rlm, mach_absolute_time());
} else if (被GCD喚醒) {
// 處理GCD相關(guān)
__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è)置返回值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;
}
voucher_mach_msg_revert(voucherState);
os_release(voucherCopy);
// 如果retVal等于0墓塌,繼續(xù)執(zhí)行循環(huán)
} while (0 == retVal);
return retVal;
}
上面源代碼對比流程圖可知,流程的確如圖所示奥额。
3. 小細(xì)節(jié)
①
__CFRunLoopDoObservers函數(shù)中真正做通知Observers相關(guān)的是CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION函數(shù)
__CFRunLoopDoBlocks函數(shù)中真正做Block相關(guān)的是CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK函數(shù)
__CFRunLoopDoSources0函數(shù)中真正做Source0相關(guān)的是CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION函數(shù)
__CFRunLoopDoTimers函數(shù)中真正做定時器相關(guān)的是CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION函數(shù)
__CFRunLoopDoSource函數(shù)中真正做Source1相關(guān)的是CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION函數(shù)
我們也可以通過函數(shù)調(diào)用棧來驗證:
- (void)viewDidLoad {
[super viewDidLoad];
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"123"); // 斷點
}];
}
在斷點處查看函數(shù)調(diào)用棧:
可以發(fā)現(xiàn)的確調(diào)用了CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION函數(shù)苫幢,之后Core Foundation框架的函數(shù)就調(diào)用完了。
②
一般情況下GCD的東西是GCD來處理的垫挨,不會交給RunLoop韩肝。GCD是GCD,RunLoop是RunLoop九榔,他們互不干擾哀峻,但是有一種情況下GCD是交給RunLoop處理的,如下:
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 子線程處理一些邏輯
// 回到主線程去刷新UI界面
dispatch_async(dispatch_get_main_queue(), ^{
NSLog(@"11111111111"); // 斷點
});
});
}
函數(shù)調(diào)用棧:
當(dāng)我們在子線程處理一些邏輯然后回到主線程去刷新UI界面哲泊,這種情況就會交給RunLoop去處理GCD相關(guān)的東西剩蟀,然后再回到GCD。
③ 線程休眠
當(dāng)RunLoop事情處理完發(fā)現(xiàn)沒有事情干切威,就會進入休眠育特,這時候你可以認(rèn)為是“線程阻塞”,這時候代碼就不會往下走了先朦,直到被喚醒缰冤,才會繼續(xù)往下走犬缨。
這種休眠和while(1){};不一樣,這種休眠是當(dāng)前線程休息了棉浸,CPU不再給當(dāng)前線程分配資源怀薛,但是while(1){};這種阻塞是一個死循環(huán),這時候線程沒有休息迷郑,還在一直執(zhí)行while(1){}乾戏。
那么線程休眠是怎么做到的呢?
同樣查看源碼三热,進入__CFRunLoopServiceMachPort函數(shù):
......
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);
......
上面的mach_msg是內(nèi)核層面的API鼓择,內(nèi)核層面的API是不給我們程序員使用的,因為比較危險就漾,給我們使用的都是應(yīng)用層面的API呐能。
RunLoop休眠的實現(xiàn)原理,如下圖:
當(dāng)用戶態(tài)調(diào)用mach_msg()函數(shù)會自動切換到內(nèi)核態(tài)抑堡,會真正調(diào)用內(nèi)核態(tài)的mach_msg()函數(shù)摆出,內(nèi)核態(tài)mach_msg做的事就是:沒有消息就讓線程休眠,有消息就喚醒線程首妖,喚醒線程后就又回到用戶態(tài)偎漫。所以線程休眠就是因為用戶態(tài)和內(nèi)核態(tài)的切換。
三. RunLoop在實際開發(fā)中的應(yīng)用
- 解決NSTimer在滑動時停止工作的問題
- 控制線程生命周期(線程庇欣拢活)
- 監(jiān)控應(yīng)用卡頓
- 性能優(yōu)化
先講前兩種象踊,后面兩種講到性能優(yōu)化再說。
1. 解決NSTimer在滑動時停止工作的問題
定時器失效問題:
在view上添加一個scrollView棚壁,寫如下代碼:
- (void)viewDidLoad {
[super viewDidLoad];
static int count = 0;
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}];
}
不拖動scrollView杯矩,定時器一直在打印,拖動scrollView發(fā)現(xiàn)定時器停止工作了袖外。
這是因為RunLoop只會在一種模式下工作史隆,默認(rèn)情況下定時器被添加到NSDefaultRunLoopMode模式下,但是拖動的時候RunLoop切換成UITrackingRunLoopMode模式曼验,但是定時器沒有添加到這個模式下泌射,所以定時器不會工作。
解決辦法也很簡單鬓照,就是在兩種模式下都添加定時器熔酷,代碼如下:
- (void)viewDidLoad {
[super viewDidLoad];
static int count = 0;
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}];
// NSDefaultRunLoopMode、UITrackingRunLoopMode才是真正存在的模式
// NSRunLoopCommonModes并不是一個真的模式颖杏,它只是一個標(biāo)記
// timer能在_commonModes數(shù)組中存放的模式下工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
{
//scheduledTimerWithTimeInterval默認(rèn)會把定時器添加到默認(rèn)模式下纯陨,所以用timerWithTimeInterval
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}];
}
注意,NSRunLoopCommonModes并不是一個真的模式,它只是一個標(biāo)記翼抠,定時器能在_commonModes數(shù)組中存放的模式下工作咙轩。
如果timer被標(biāo)記為commonModes,那么timer就能在_commonModes數(shù)組中存放的模式下工作阴颖,而_commonModes數(shù)組中存放的恰好是NSDefaultRunLoopMode和UITrackingRunLoopMode模式活喊,當(dāng)然這兩種模式也在_modes數(shù)組里面。
能在commonModes“模式”下工作的東西都會被添加到_commonModeItems數(shù)組中量愧,比如上面的timer钾菊。
再看一下__CFRunLoop結(jié)構(gòu)體源碼:
struct __CFRunLoop {
......
pthread_t _pthread;
CFMutableSetRef _commonModes; //就是這個_commonModes數(shù)組
CFMutableSetRef _commonModeItems; //能在commonModes“模式”下工作的東西都會被添加到這個數(shù)組中
CFRunLoopModeRef _currentMode;//當(dāng)前模式
CFMutableSetRef _modes; //是個集合,無序的偎肃,里面裝的是CFRunLoopModeRef類型的對象
......
};
如果timer是NSDefaultRunLoopMode或者UITrackingRunLoopMode模式煞烫,那么它就會被添加到自己模式下的_timers數(shù)組中。
進入CFRunLoopModeRef:
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
struct __CFRunLoopMode {
......
CFStringRef _name;//名稱
CFMutableSetRef _sources0;//里面裝的是CFRunLoopSourceRef類型的對象
CFMutableSetRef _sources1;//里面裝的是CFRunLoopSourceRef類型的對象
CFMutableArrayRef _observers;//里面裝的是CFRunLoopObserverRef類型的對象
CFMutableArrayRef _timers;//里面裝的是CFRunLoopTimerRef類型的對象
......
};
關(guān)于線程崩鬯蹋活請看下篇文章滞详。
四. 面試題
講講 RunLoop,項目中有用到嗎紊馏?
runloop內(nèi)部實現(xiàn)邏輯料饥?
看流程圖runloop和線程的關(guān)系?
一對一的關(guān)系timer 與 runloop 的關(guān)系朱监?
① RunLoop對象里面有個_modes數(shù)組岸啡,里面放一堆模式,模式里面會放timer赫编,如果timer被標(biāo)記為commonModes巡蘸,那么timer就能在_commonModes數(shù)組中存放的模式下工作,能在commonModes“模式”下工作的東西都會被添加到_commonModeItems數(shù)組里中沛慢。
② 如果線程休眠了赡若,timer也可以喚醒休眠的RunLoop。程序中添加每3秒響應(yīng)一次的NSTimer团甲,當(dāng)拖動tableview時timer可能無法響應(yīng)要怎么解決?
將timer被標(biāo)記為commonModesrunloop 是怎么響應(yīng)用戶操作的黍聂,具體流程是什么樣的躺苦?
當(dāng)用戶有個點擊事件,這個系統(tǒng)事件會先被Source1捕捉产还,Source1捕捉之后會包裝成事件隊列(EventQuene)匹厘,再放到Source0里面進行處理,然后RunLoop循環(huán)再處理Source0里面的事件脐区。說說runLoop的幾種狀態(tài)
runloop的mode作用是什么愈诚?
不同模式的Source0/Source1/Timer/Observer能分隔開來,互不影響
Demo地址:RunLoop1