(這也是以前寫的, 主要做個(gè)記錄方便隨時(shí)查閱, 不對的地方請指正!)
RunLoop雖然在平時(shí)開發(fā)過程中使用不多, 但是是非常重要的, 往往能夠解決關(guān)鍵性問題, 比如計(jì)時(shí)器突然不準(zhǔn), 頁面滑動(dòng)有時(shí)會(huì)卡頓等問題, 都可以用RunLoop來解決, 本篇文章是總結(jié)性和實(shí)踐性文章, 主要做個(gè)記錄, 方便自己開發(fā)中查閱.
什么是RunLoop?
RunLoop從字面量意思看就是一個(gè)運(yùn)行循環(huán), 其實(shí)是一個(gè)事件處理的循環(huán), 用來不停的調(diào)度任務(wù)和處理輸入事件. RunLoop 內(nèi)部其實(shí)是一個(gè)do-while大循環(huán), 在這個(gè)循環(huán)里處理輸入事件, 比如:點(diǎn)擊事件, 滑動(dòng)屏幕, 定時(shí)器等, 當(dāng)處理完一個(gè)任務(wù)后RunLoop進(jìn)入休眠, 有任務(wù)時(shí)又喚醒RunLoop處理事件.
簡言之, RunLoop是管理線程各類輸入事件的對象.
基本作用:
-
保持程序的持續(xù)運(yùn)行. 如果線程中沒有RunLoop, 線程執(zhí)行完任務(wù)隊(duì)列中的任務(wù)后, 就會(huì)退出. 所以app的主線程必定有一個(gè)RunLoop, 一直讓主線程處理不退出狀態(tài), 除非系統(tǒng)或者手動(dòng)讓RunLoop停止工作, 此時(shí)主線程退出, app掛掉. -
處理app中的各類輸入事件. 比如:點(diǎn)擊事件,觸摸事件, 方法調(diào)用(seletor)事件, 定時(shí)器等, RunLoop就是一直在等待處理這些事件. -
節(jié)省CPU資源. 雖然RunLoop 是一個(gè)大循環(huán), 但是不同于while(1)死循環(huán), 當(dāng)有任務(wù)的事件會(huì)喚醒RunLoop執(zhí)行任務(wù), 沒有任務(wù)時(shí)休眠RunLoop.
在我們程序的main函數(shù)中, 返回UIApplicationMain方法調(diào)用的返回值, 這個(gè)方法程序運(yùn)行時(shí)是不會(huì)被返回的, 因?yàn)樵谶@個(gè)方法內(nèi)部會(huì)創(chuàng)建一個(gè)RunLoop, 維持主線程的持續(xù)運(yùn)行.
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([CZPAppDelegate class]));
}
}
類似于下面的偽代碼:
int main(int argc, char * argv[]) {
BOOL isRunning = YES;
do {
// 通知觀察者告知RunLoop的狀態(tài)
// ...
// 處理各類事件
// ...
// 休眠, 等待被喚醒
// 喚醒
// 各類條件是否滿足??
BOOL conditions;
if (conditions) {
isRunning = YES;
}else {
isRunning = NO;
}
} while (isRunning);
return 0;
}
RunLoop對象
RunLoop是管理線程輸入事件的對象
-
Core Foundation框架中使用CFRunLoopRef. 是純C寫的代碼, 所以是線程安全的. - 在
Foundation框架中使用NSRunLoop, 是封裝的CFRunLoopRef中, 不是線程安全的. 兩者都代表RunLoop對象, 是可以等價(jià)轉(zhuǎn)換的.
要想了解RunLoop很有必要知道底層的實(shí)現(xiàn), 蘋果公司開源了這個(gè)部分代碼, 其中跟RunLoop相關(guān)的就兩個(gè)文件: CFRunLoop.h, CFRunLoop.c.
CFRunLoopRef開源代碼下載地址
http://opensource.apple.com/source/CF/CF-1151.16/
獲取 RunLoop 對象
-
獲取當(dāng)前 RunLoop 對象:
// NSRunloop NSRunLoop * runloop = [NSRunLoop currentRunLoop]; // CFRunLoopRef CFRunLoopRef runloop = CFRunLoopGetCurrent(); -
獲取主線程的 RunLoop 對象:
// NSRunloop NSRunLoop * runloop = [NSRunLoop mainRunLoop]; // CFRunLoopRef CFRunLoopRef runloop = CFRunLoopGetMain();
注意:
創(chuàng)建 RunLoop 對象不是通過 alloc init 的方式創(chuàng)建的, 是直接獲取, 沒有的話就會(huì)創(chuàng)建. 主線程中也可以通過方式一來獲取 RunLoop 對象.
RunLoop 與線程
線程和RunLoop的關(guān)系:
- 每條線程都有唯一的一個(gè)RunLoop 對象.
- 主線程的RunLoop在程序啟動(dòng)時(shí)自動(dòng)創(chuàng)建好了, 子線程的需要手動(dòng)創(chuàng)建.
- RunLoop 在第一次獲取時(shí)創(chuàng)建, 線程銷毀時(shí)銷毀.
線程和RunLoop的對應(yīng)關(guān)系保存在一個(gè)全局的字典中, 通過key-value保持. 下面是CFRunLoop.m中的源代碼, 無論是主線程還是子線程在獲取RunLoop的時(shí)候會(huì)調(diào)用_CFRunLoopGet0函數(shù), 該函數(shù)判斷該線程RunLoop對象, 有則返回, 沒有則創(chuàng)建并保存.
// 創(chuàng)建字典
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 創(chuàng)建主線程runloop
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主線程runloop
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
// 從字典中獲取子線程的runloop
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
// 如果子線程的runloop不存在,那么就為該線程創(chuàng)建一個(gè)對應(yīng)的runloop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 把當(dāng)前子線程和對應(yīng)的runloop保存到字典中
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
RunLoop 運(yùn)行原理
Runloop運(yùn)行原理圖中可以看到, 一個(gè)線程中的RunLoop接受 source 源的輸入事件和 timer 定時(shí)器事件, 當(dāng)有這些事件發(fā)生時(shí), 就會(huì)喚醒 RunLoop 執(zhí)行相應(yīng)任務(wù).

RunLoop 相關(guān)類
有5個(gè)相關(guān)類:
CFRunloopRef-
CFRunloopModeRef: Runloop的運(yùn)行模式 -
CFRunloopSourceRef: Runloop要處理的事件源 -
CFRunloopTimerRef: Timer事件 -
CFRunloopObserverRef: Runloop的觀察者(監(jiān)聽者)
Runloop和相關(guān)類之間的關(guān)系圖:

CFRunloopModeRef代表RunLoop的運(yùn)行模式, 每次運(yùn)行只能處于一個(gè)模式下, 每個(gè)mode下有很多source, timer, observer, 如果要切換模式, 只能退出當(dāng)前循環(huán), 重新指定一個(gè)新的mode后再次進(jìn)入循環(huán).
RunLoop 這么設(shè)計(jì)其實(shí)是為了避免各個(gè)mode下的source, timer, observer相互影響.
CFRunloopModeRef有五種mode:
kCFRunLoopDefaultMode: App的默認(rèn)Mode,通常主線程是在這個(gè)Mode下運(yùn)行.UITrackingRunLoopMode: 界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動(dòng),保證界面滑動(dòng)時(shí)不受其他 Mode 影響.UIInitializationRunLoopMode: 在剛啟動(dòng) App 時(shí)第進(jìn)入的第一個(gè) Mode,啟動(dòng)完成后就不再使用.GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到kCFRunLoopCommonModes: 這是一個(gè)占位用的Mode,不是一種真正的Mode.
CFRunloopSourceRef分為兩種:
- source0: 非基于Port的, 把事件告訴RunLoop, 需要手動(dòng)激活.
- Source1: 通過系統(tǒng)內(nèi)核來喚醒.
- 可以通過打斷點(diǎn)的方式查看一個(gè)方法的函數(shù)調(diào)用棧
CFRunLoopObserverRef觀察者,監(jiān)聽RunLoop的狀態(tài):
監(jiān)聽的狀態(tài)是枚舉:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即將進(jìn)入Runloop
kCFRunLoopBeforeTimers = (1UL << 1), //即將處理NSTimer
kCFRunLoopBeforeSources = (1UL << 2), //即將處理Sources
kCFRunLoopBeforeWaiting = (1UL << 5), //即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), //即將退出runloop
kCFRunLoopAllActivities = 0x0FFFFFFFU //所有狀態(tài)改變
};
給RunLoop添加監(jiān)聽者:
//創(chuàng)建一個(gè)runloop監(jiān)聽者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"監(jiān)聽runloop狀態(tài)改變---%zd",activity);
});
//為runloop添加一個(gè)監(jiān)聽者
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 必須釋放
CFRelease(observer);
CFRunloopTimerRef是定時(shí)器事件, NSTimer是基于CFRunloopTimerRef的封裝, 是基于時(shí)間的觸發(fā)器, 需要把timer加入到RunLoop里面去, RunLoop會(huì)在相應(yīng)的時(shí)間點(diǎn)注冊事件, 等時(shí)間到了觸發(fā)喚醒RunLoop執(zhí)行事件.
定時(shí)器在開發(fā)中使用廣泛, 但是很容易犯錯(cuò), 有兩種方式設(shè)置定時(shí)事件, NSTimer和GCD, 兩者的定時(shí)器不同, GCD的更加精準(zhǔn), 不受RunLoop的影響. 而且RunLoop里面設(shè)置循環(huán)過期時(shí)間也是用的GCD的定時(shí)器, 由此可知, RunLoop 會(huì)使用 GCD 的部分功能.
注意:
使用timer 的時(shí)候一定要注意添加到哪種mode模式下, 一般標(biāo)記為
NSRunLoopCommonModes下, 也就是說任何模式下都可以正常運(yùn)行定時(shí)器, 否則只能特定模式下運(yùn)行, 當(dāng)RunLoop切換模式后, 定時(shí)器不正常工作.
NSTimer + RunLoop用法:
- (void)timer2 {
//NSTimer 調(diào)用了scheduledTimer方法,那么會(huì)自動(dòng)添加到當(dāng)前的runloop里面去,而且runloop的運(yùn)行模式kCFRunLoopDefaultMode
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
//更改模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)timer1 {
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 定時(shí)器添加到UITrackingRunLoopMode模式,一旦runloop切換模式,那么定時(shí)器就不工作
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 占位模式:common modes標(biāo)記
// 被標(biāo)記為common modes的模式 kCFRunLoopDefaultMode UITrackingRunLoopMode
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
// NSLog(@"%@",[NSRunLoop currentRunLoop]);
}
- (void)run {
NSLog(@"---run---%@", [NSRunLoop currentRunLoop].currentMode);
}
- (IBAction)btnClick {
NSLog(@"---btnClick---");
}
還有一種辦法是添加兩次:
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
GCD 定時(shí)器用法:
//0.創(chuàng)建一個(gè)隊(duì)列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
//1.創(chuàng)建一個(gè)GCD的定時(shí)器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//2.設(shè)置定時(shí)器的開始時(shí)間,間隔時(shí)間以及精準(zhǔn)度
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW,3.0 *NSEC_PER_SEC);
//設(shè)置定時(shí)器工作的間隔時(shí)間
uint64_t intevel = 1.0 * NSEC_PER_SEC;
/*
第四個(gè)參數(shù):定時(shí)器的精準(zhǔn)度,如果傳0則表示采用最精準(zhǔn)的方式計(jì)算,如果傳大于0的數(shù)值,則表示該定時(shí)切換i可以接收該值范圍內(nèi)的誤差,通常傳0
該參數(shù)的意義:可以適當(dāng)?shù)奶岣叱绦虻男阅?注意點(diǎn):GCD定時(shí)器中的時(shí)間以納秒為單位
*/
dispatch_source_set_timer(timer, start, intevel, 0 * NSEC_PER_SEC);
//3.設(shè)置定時(shí)器開啟后回調(diào)的方法
dispatch_source_set_event_handler(timer, ^{
NSLog(@"------%@", [NSThread currentThread]);
});
//4.執(zhí)行定時(shí)器
dispatch_resume(timer);
//注意:dispatch_source_t本質(zhì)上是OC類,在這里是個(gè)局部變量,需要強(qiáng)引用
self.timer = timer;
RunLoop運(yùn)行邏輯
運(yùn)行邏輯就是一個(gè)do-while大循環(huán), 在這個(gè)循環(huán)里面處理事件, 監(jiān)聽RunLoop的狀態(tài), 不停的休眠-喚醒-處理-休眠這個(gè)過程.


RunLoop應(yīng)用
下面是開發(fā)中經(jīng)常使用RunLoop的地方:
NSTimer
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSTimer *timer = [NSTimer timerWithTimeInterval:3 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"---timer block---");
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
控制臺輸出:
2017-02-28 11:17:46.102 test[2831:93953] ---timer block---
2017-02-28 11:17:49.102 test[2831:93953] ---timer block---
2017-02-28 11:17:52.102 test[2831:93953] ---timer block---
2017-02-28 11:17:55.102 test[2831:93953] ---timer block---
2017-02-28 11:17:58.102 test[2831:93953] ---timer block---
2017-02-28 11:18:01.175 test[2831:93953] ---timer block---
ImageView顯示
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"abc"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];
PerformSelector
有很多 PerformSelector 方法都是需要制定mode, date的, 正如上面所示.
當(dāng)調(diào)用 NSObject 的 performSelecter:afterDelay: 后,實(shí)際上其內(nèi)部會(huì)創(chuàng)建一個(gè) Timer 并添加到當(dāng)前線程的 RunLoop 中。所以如果當(dāng)前線程沒有 RunLoop,則這個(gè)方法會(huì)失效。
當(dāng)調(diào)用 performSelector:onThread: 時(shí),實(shí)際上其會(huì)創(chuàng)建一個(gè) Timer 加到對應(yīng)的線程去,同樣的,如果對應(yīng)線程沒有 RunLoop 該方法也會(huì)失效。
常駐線程
有時(shí)需要在后臺開啟一個(gè)線程持續(xù)的做任務(wù), 比如搜集數(shù)據(jù), 語音喚起等, 無非就是在一個(gè)線程中開啟一個(gè)RunLoop 讓它維持線程的持續(xù)運(yùn)行, 而不是執(zhí)行完任務(wù)后退出.
AFN框架中也是用了:
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[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;
}
注意:
子線程的RunLoop里面至少要有一個(gè)source或者是timer, 只有observer不行的.
自動(dòng)釋放池
自動(dòng)釋放池的第一次創(chuàng)建:
第一次進(jìn)入RunLoop時(shí)候自動(dòng)創(chuàng)建.
自動(dòng)釋放池的第一次銷毀:
RunLoop即將進(jìn)入休眠的時(shí)候
其它情況下創(chuàng)建和銷毀:
喚醒RunLoop的時(shí)候會(huì)創(chuàng)建新的自動(dòng)釋放池
RunLoop銷毀時(shí)銷毀自動(dòng)釋放池
打印RunLoop信息可以看出:
_wrapRunLoopWithAutoreleasePoolHandler activities = 0x1 1
_wrapRunLoopWithAutoreleasePoolHandler activities = 0xa0 160
160 是 kCFRunLoopBeforeWaiting + kCFRunLoopExit