一丶RunLoop 介紹
官方文檔:
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
看任何文檔,都不如看官方文檔
簡介:
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.
大概意思:
Run loops 是與線程相關(guān)的基礎(chǔ)架構(gòu)的一部分,是一個(gè)事件處理的循環(huán),用于工作調(diào)度并處理接收事件,Run loop 的可以使線程有工作需要做時(shí)可以忙碌起來,而當(dāng)沒有事可做時(shí),又可以使得線程睡眠。
其他:
1.Run Loop 管理不是完全自動(dòng)的;,設(shè)計(jì)線程的時(shí)候,適當(dāng)?shù)臅r(shí)候啟動(dòng)和響應(yīng)傳入事件;
2.每個(gè)線程都有一個(gè)關(guān)聯(lián)的Run Loop 除了主線程默認(rèn)開啟,分線程需要手動(dòng)運(yùn)行;采用懶加載的方式進(jìn)行加載;
1.1 Run Loop 結(jié)構(gòu):
Run Loop 接收2種不同類型的sources事件
1.1.1.輸入源:
異步傳遞事件,通常是來自不同的線程或不同的應(yīng)用的消息。輸入源異步傳遞事件到對應(yīng)的處理程序和在線程關(guān)聯(lián)的NSRunLoop對象調(diào)起runUntilDate:方法來退出事件處理。
1.1.2.timer源:
同步地傳遞事件,發(fā)生在每個(gè)定時(shí)器調(diào)用或周期性地調(diào)用。Timer源傳遞事件到他們的處理程序,但是不會調(diào)用run loop來退出處理。
運(yùn)行循環(huán)及其源的結(jié)構(gòu)圖

1.2 Run Loop Modes
Run Loop模式是一個(gè)監(jiān)視輸入源和定時(shí)器的集合和注冊成為run loop的觀察者的集合。每次要運(yùn)行run loop,都需要顯示或隱式地指定某種運(yùn)行的mode。只有與這種指定的mode關(guān)聯(lián)的源才會被監(jiān)視和允許傳遞他們的事件,同樣地,只有與這種模式關(guān)聯(lián)的觀察者都會收到run loop行為變化的通知。與其它模式想關(guān)聯(lián)的源,直到隨后在合適的模式通過循環(huán)后,都會接收到新的事件
我們通過名稱來唯一標(biāo)識mode。在Cocoa和Core Foundation中都定義了default模式和幾個(gè)常用的模式,都是通過字符串名稱來指定。我們也可以自定義模式,但是我們需要手動(dòng)添加至少一個(gè)input source/timers/observers。
通過使用mode來過濾掉我們不希望接收到來自不想要的通過run loop的源。大部分情況下,我們都是使用系統(tǒng)定義的default模式。對于子線程,我們可以使用自定義模式在關(guān)鍵性操作時(shí)阻止低優(yōu)先級的源傳遞事件。

二丶什么時(shí)候使用Run Loop
2.1什么時(shí)候應(yīng)該使用run loop呢?
只有當(dāng)我們需要?jiǎng)?chuàng)建子線程的時(shí)候,才會需要到顯示地運(yùn)行run loop。應(yīng)用程序的主線程的run loop是應(yīng)用啟動(dòng)的基礎(chǔ)任務(wù),在啟動(dòng)時(shí)就會自動(dòng)啟動(dòng)run loop。所以我們不需要手動(dòng)啟動(dòng)主線程的run loop。
對于子線程,我們需要確定線程是否需要run loop,如果需要,則配置它并啟動(dòng)它。我們并不問題需要啟動(dòng)run loop的。比如說,如果我們開一個(gè)子線程去執(zhí)行一些長時(shí)間的和預(yù)先決定的任務(wù),我們可能不需要啟動(dòng)run loop。Run loop是用于那么需要在線程中有更多地交互的場景。比如說,我們會在下面的任何一種場景中需要開啟run loop:
1.使用端口源或者自定義輸入源與其它線程通信
2.在線程中使用定時(shí)器
3.使用Cocoa中的任何performSelector…方法
4.保持線程來執(zhí)行周期性的任務(wù)
2.2 使用Run Loop 對象
2.2.1
Run Loop對象給添加輸入源、定時(shí)器和觀察者到run loop提供了主接口。每個(gè)線程都有一個(gè)單獨(dú)的run loop與之關(guān)聯(lián)(對于子線程,若沒有調(diào)用過任何獲取run loop的方法是不會有run loop的,只有調(diào)用過,才會創(chuàng)建或者直接使用)。
采用懶加載的方式獲取方式:
2.2.2通過以下兩種方式來獲取run loop對象:
1.在Cocoa中,使用[NSRunLoop currentRunLoop]獲取
2.使用CFRunLoopGetCurrent()函數(shù)獲取
2.2.3配置RunLoop
在子線程運(yùn)行run loop之前,你必須至少添加一種輸入源或者定時(shí)器。如果run loop沒有任何的源需要監(jiān)視,它就會立刻退出。
除了添加sources之外,你還可以添加觀察者來檢測runloop不同的執(zhí)行狀態(tài)。要添加觀察者,可以使用CFRunLoopObserverRef指針類型和使用CFRunLoopAddObserver函數(shù)來添加到run loop中。我們只能通過Core Foundation來創(chuàng)建run loop觀察者,即使是Cocoa應(yīng)用。
下面這段代碼展示主線程如何添加觀察者到run loop以及如何創(chuàng)建run loop觀察者:
- (void)threadMain {
// The application uses garbage collection, so no autorelease pool is needed.
NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
// Create a run loop observer and attach it to the run loop.
CFRunLoopObserverContext context = {0, self, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (observer) {
CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
// Create and schedule the timer.
[NSTimer scheduledTimerWithTimeInterval:0.1 target:self
selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
NSInteger loopCount = 10;
do {
// Run the run loop 10 times to let the timer fire.
[myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
loopCount--;
} while (loopCount);
}
2.2.4 啟動(dòng)Run Loop
只有子線程才有可能需要啟動(dòng)run loop。Run loop必須至少有一種輸入源或者timer源來監(jiān)視。如果沒有任何源,則run loop會退出。
下面的幾種方式可以啟動(dòng)run loop:
- 無條件地:無條件進(jìn)入run loop是最簡單的方式,但也是最不希望這么做的,因?yàn)檫@樣會導(dǎo)致run loop會進(jìn)入永久地循環(huán)。可以添加、刪除輸入源和timer源,但是只能通過kill掉run loop才能停止。而且還不能使用自定義mode。
- 限時(shí):與無條件運(yùn)行run loop不同,最好是給run loop添加一個(gè)超時(shí)時(shí)間。
- 在特定的mode:除了添加超時(shí)時(shí)間,還可以指定mode。
下面是運(yùn)行run loop的一段代碼:
- (void)skeletonThreadMain {
// Set up an autorelease pool here if not using garbage collection.
BOOL done = NO;
// Add your sources or timers to the run loop and do any other setup.
do {
// Start the run loop but return after each source is handled.
SInt32 result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
// If a source explicitly stopped the run loop, or if there are no
// sources or timers, go ahead and exit.
if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
done = YES;
// Check for any other exit conditions here and set the
// done variable as needed.
} while (!done);
// Clean up code here. Be sure to release any allocated autorelease pools.
}
2.2.5 退出Run Loop
有兩種方法使run loop在處理事件之前,退出run loop:
1.給run loop設(shè)定超時(shí)時(shí)間
2.告訴run loop要stop
設(shè)定超時(shí)時(shí)間是比較推薦的。我們可以通過CFRunLoopStop函數(shù)來停止run loop。
2.3 線程安全
CFRunLoopRef 是在 CoreFoundation 框架內(nèi)的,它提供了純 C 函數(shù)的 API,所有這些 API 都是線程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封裝,提供了面向?qū)ο蟮?API,但是這些 API 不是線程安全的。
三丶使用示例:
3.1 檢測卡頓監(jiān)控
通過CFRunLoop源代碼
https://opensource.apple.com/source/CF/CF-1151.16/CFRunLoop.c
NSRunLoop調(diào)用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間,還有kCFRunLoopAfterWaiting之后,也就是如果我們發(fā)現(xiàn)這兩個(gè)時(shí)間內(nèi)耗時(shí)太長,那么就可以判定出此時(shí)主線程卡頓.
技術(shù)要點(diǎn):
1.利用CFRunLoopObserverRef 實(shí)時(shí)獲得這些狀態(tài)值的變化
2.知道RunLoop的Activity
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};
- dispatch_semaphore_t 利用保證計(jì)算精確
參考:https://github.com/suifengqjn/PerformanceMonitor
#import "PerformanceMonitor.h"
#import <CrashReporter/CrashReporter.h>
@interface PerformanceMonitor ()
{
int timeoutCount;
//實(shí)時(shí)獲得這些狀態(tài)值的變化
CFRunLoopObserverRef observer;
@public
dispatch_semaphore_t semaphore;
CFRunLoopActivity activity;
}
@end
@implementation PerformanceMonitor
+ (instancetype)sharedInstance
{
static id instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
PerformanceMonitor *moniotr = (__bridge PerformanceMonitor*)info;
moniotr->activity = activity;
dispatch_semaphore_t semaphore = moniotr->semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)stop
{
if (!observer)return;
CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
CFRelease(observer);
observer = NULL;
}
- (void)start
{
if (observer)return;
// 信號
semaphore = dispatch_semaphore_create(0);
// 注冊RunLoop狀態(tài)觀察
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 在子線程監(jiān)控時(shí)長
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (st != 0)
{
if (!observer)
{
timeoutCount = 0;
semaphore = 0;
activity = 0;
return;
}
if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
{
NSLog(@"---卡頓計(jì)時(shí)器:%ld",(long)timeoutCount);
if (++timeoutCount < 5)
continue;
NSLog(@"...我被卡住了");
}
}
timeoutCount = 0;
}
});
}
@end

3.2 讓線程常在
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//添加MachPort源,保證線程不銷毀
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
3.3 NStimer使用
一丶主線程情況:
NSTimer *timer =[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(run) userInfo:nil repeats:YES];
//滑動(dòng)Scroll,或者tabview等,由于主線程只能執(zhí)行一個(gè)程序,正在運(yùn)行的timer會被停止,所以,加入以下字段,能提升計(jì)時(shí)器的優(yōu)先級別
[[NSRunLoop currentRunLoop] addTimer:_myTimer forMode:UITrackingRunLoopMode];
二丶分線程情況:
//創(chuàng)建分線程
dispatch_queue_t globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQ, ^{
NSTimer *timer =[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(run) userInfo:nil repeats:YES];
//通過[NSRunLoop currentRunLoop]創(chuàng)建RunLoop
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//分線程必須運(yùn)行
[[NSRunLoop currentRunLoop] run];
});