前言
RunLoop是iOS和OSX開發(fā)中非?;A(chǔ)的一個概念,學(xué)習(xí)Runloop能夠幫助我們更清楚的了解APP為何能夠持續(xù)運行。雖然在平時的工作場景中使用Runloop的機會很少,但是理解RunLoop可以幫助開發(fā)者更好的利用多線程編程。網(wǎng)上關(guān)于Runloop的文章千篇一律,但"一千個讀者,就有一千個哈姆雷特",每個人都有自己不同的理解。
Runloop
通俗概念
- 可以從字面上理解成“運行循環(huán)”、“跑圈”,通俗一點,它就是一個死循環(huán),相當(dāng)于一個do..while;
- 正是因為這個死循環(huán)的存在,才能保持程序的持續(xù)運行,不會像一塊代碼,執(zhí)行完了就完了;
- 有事情的時候做事情,沒事情的時候就休息,充分節(jié)省了CPU的性能;
- 所謂的“事情”,實際上就是App中的各種事件(比如觸摸事件、定時器事件、Selector事件);
官方解釋
- 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.
- Runloop是與多線程相關(guān)的、基礎(chǔ)框架中非常重要的一部分。 Runloop是你用來調(diào)度、協(xié)調(diào)當(dāng)前的到來事件的一個循環(huán)。Runloop的目的就是當(dāng)有任務(wù)到來的時候保持當(dāng)前線程處于繁忙狀態(tài), 當(dāng)沒有任務(wù)需要處理的時候讓當(dāng)前線程處于休眠狀態(tài)。
如果沒有Runloop

image
- 代碼從上到下執(zhí)行,到第三行就結(jié)束了
有了Runloop以后

image
- 由于main函數(shù)里面啟動了RunLoop,所以程序并不會馬上退出,保持持續(xù)運行狀態(tài)
- 在代碼main.m里都默認(rèn)在主線程中啟動了runloop
- 所以UIApplicationMain函數(shù)一直沒有返回,保持了程序的持續(xù)運行
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
RunLoop對象
- iOS中為我們提供了兩套API訪問和操作runloop,一套是面向OC的,一套是基于C語言的
- Foundation下的NSRunLoop
- Core Foundation下的CFRunLoopRef
- NSRunLoop是基于CFRunLoopRef的一層OC包裝,所以要了解RunLoop內(nèi)部結(jié)構(gòu),需要多研究CFRunLoopRef層面的API(Core Foundation層面)
- 更多細(xì)節(jié)我們可以查閱蘋果官方文檔深入學(xué)習(xí)
- 蘋果官方文檔
- RunLoop 官方編程手冊翻譯
Runloop與線程的關(guān)系
- 每條線程都有唯一的一個與之對應(yīng)的RunLoop對象
- 主線程的RunLoop已經(jīng)自動創(chuàng)建好了,子線程的RunLoop需要主動創(chuàng)建(子線程的RunLoop默認(rèn)是關(guān)閉的,因為有時候開了個線程但卻沒有必要開一個RunLoop,不然反而浪費了資源)。
- RunLoop在第一次獲取時創(chuàng)建,在線程結(jié)束時銷毀(內(nèi)部實際上是一個懶加載)
獲得RunLoop對象
- Foundation下的獲取方法
- 獲得當(dāng)前線程的RunLoop對象,在子線程調(diào)用該方法相當(dāng)于新創(chuàng)建一個runloop
- [NSRunLoop currentRunLoop];
- 獲得主線程的RunLoop對象
- [NSRunLoop mainRunLoop];
- 獲得當(dāng)前線程的RunLoop對象,在子線程調(diào)用該方法相當(dāng)于新創(chuàng)建一個runloop
- Core Foundation下的獲取方法
- 獲得當(dāng)前線程的RunLoop對象,在子線程調(diào)用該方法相當(dāng)于新創(chuàng)建一個runloop
- CFRunLoopGetCurrent();
- 獲得主線程的RunLoop對象
- CFRunLoopGetMain();
- 獲得當(dāng)前線程的RunLoop對象,在子線程調(diào)用該方法相當(dāng)于新創(chuàng)建一個runloop
Runloop的相關(guān)類
- 著重了解Core Foundation下的類
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
- 它們的關(guān)系如圖所示
- 一個runloop內(nèi)部包含若干個Mode,而每個Mode下又包含了若干個timer,observer,source.
- 調(diào)用 RunLoop 的主函數(shù)時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode
- 如果需要切換 Mode,只能退出 Loop,再重新指定一個 Mode 進(jìn)入。這樣做主要是為了分隔開不同組的 Source/Timer/Observer,讓其互不影響。

image
CFRunLoopRef
- 即代表runloop本身
CFRunLoopModeRef
- 代表RunLoop的運行模式
- 系統(tǒng)默認(rèn)注冊了5個mode,前兩個常用,后三個基本用不到
- kCFRunLoopDefaultMode:App的默認(rèn)Mode,通常主線程是在這個Mode下運行
- UITrackingRunLoopMode:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
- UIInitializationRunLoopMode: 在剛啟動 App 時第進(jìn)入的第一個 Mode,啟動完成后就不再使用
- GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode,通常用不到
- kCFRunLoopCommonModes: 這是一個占位用的Mode,不是一種真正的Mode
CFRunLoopSourceRef
- 代表事件源(輸入源)
- 以port進(jìn)行區(qū)分,而port可以為系統(tǒng)
- 分為系統(tǒng)方法和自定義方法
- sourse0:非基于port的,自定義方法,響應(yīng)
- sourse1:基于port的,系統(tǒng)提供的方法
CFRunLoopTimerRef
- 基于時間的觸發(fā)器
- 基本上就等效于NSTimer
CFRunLoopObserverRef
- 觀察者,用于監(jiān)聽RunLoop的狀態(tài)改變
- 監(jiān)聽以下幾個狀態(tài)
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0),
kCFRunLoopBeforeTimers = (1UL << 1),
kCFRunLoopBeforeSources = (1UL << 2),
kCFRunLoopBeforeWaiting = (1UL << 5),
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopExit = (1UL << 7),
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
圖解

image

image
- 從圖上看就是一直在循環(huán),然后響應(yīng)對應(yīng)的事件,有就處理,沒有就休息。
- 在即將進(jìn)入loop的時候會有一個判空操作,如果內(nèi)部沒有任何的source、timer、observer等待著處理,那么runloop會直接退出,所以當(dāng)我們在子線程開啟runloop的時候需要注意兩點:
// 1.要給runloop添加一個事件,讓它先跑起來再說
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
// 2.需要手動開啟它
[[NSRunLoop currentRunLoop] run];
Runloop的應(yīng)用
1.處理NSTimer滑動暫停的問題。
// 通過timerWithTimeInterval創(chuàng)建出來的timer,默認(rèn)不會被添加到runloop,需要手動添加指定mode
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 通過timer的scheduledTimerWithTimeInterval創(chuàng)建出來的timer,默認(rèn)被添加到runloop的NSDefaultRunLoopMode下
// 當(dāng)滑動scrollView時,runloop會切換到UITrackingRunLoopMode
// 也就導(dǎo)致之前NSDefaultRunLoopMode下的timer暫停
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 解決方法,手動將timer添加到NSDefaultRunLoopMode下
// NSDefaultRunLoopMode表示timer既能響應(yīng)UITrackingRunLoopMode,
// 也能響應(yīng)NSDefaultRunLoopMode
// 相當(dāng)于將timer拷貝了一份放在這兩個mode下
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
2.在某個mode下調(diào)用方法。
// 只在NSDefaultRunLoopMode模式下顯示圖片
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
3.更好的理解自動釋放池(@autoreleasepool)
- @autoreleasepool會在runloop進(jìn)入休眠前統(tǒng)一釋放,在下一次即將進(jìn)入runloop時重新創(chuàng)建
- 具體驗證可以通過創(chuàng)建observer來觀察
// 創(chuàng)建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----監(jiān)聽到RunLoop狀態(tài)發(fā)生改變---%zd", activity);
});
// 添加觀察者:監(jiān)聽RunLoop的狀態(tài)
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 釋放Observer
CFRelease(observer);
4. 創(chuàng)建一個常駐線程
- 比如我們需要創(chuàng)建一個子線程,讓這個線程不死,一直循環(huán)做一些事情,比如說后臺不停的監(jiān)控用戶的網(wǎng)絡(luò)狀態(tài),掃描文件等。
- 這時就可以為子線程創(chuàng)建一個runloop,讓它跑起來,有事情的時候做事情,沒事情的時候休息
#import "ViewController.h"
@interface ViewController ()
/** 線程對象 */
@property (nonatomic, strong) NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
- (void)run
{
// 讓線程不死的一種取巧做法,
// 不停的開啟runloop,
// 每次runloop發(fā)現(xiàn)如果沒有任何的port就直接退出了,
// 當(dāng)我們調(diào)用touchesBegan為runloop添加了一個source時,runloop才正在跑起來了
while (1) {
[[NSRunLoop currentRunLoop] run];
}
}
- (void)run1
{
// 推薦開啟常駐線程的辦法
// 手動添加一個port,讓它跑起來
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
- (void)test
{
NSLog(@"----------test----%@", [NSThread currentThread]);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
關(guān)于runloop的面試題
1.什么是Runloop?
- 從字面意思上理解為:運行循環(huán)、跑圈;
- 其實它內(nèi)部是do-while循環(huán),在這個循環(huán)內(nèi)部不斷的處理各種任務(wù)(比如Timer、Sources、Observer)
- 一個線程對應(yīng)一個runloop,主線程的runloop默認(rèn)已經(jīng)啟動,子線程的runloop需要手動開啟(通過調(diào)用run方法)
- runloop只能選擇一個mode啟動,如果當(dāng)前mode中沒有任何Timer、Sources、Observer。那么則直接退出runloop.
2.自動釋放池什么時候釋放?
- 在runloop睡眠之前釋放(kCFRunLoopBeforeWaiting),在下一次跑圈的時候重新創(chuàng)建.
3.在開發(fā)中如何使用runloop?
- 開啟一個常駐線程(即讓一個子線程不進(jìn)入消亡狀態(tài),等待其他線程發(fā)來消息,處理其他事件)
- 在子線程開啟定時器
- 在子線程進(jìn)行一些長期監(jiān)控(比如用戶的網(wǎng)絡(luò)狀態(tài),掃描用戶的文件等)
- 可以控制定時器在特定mode下運行
- 可以讓某些事件(行為、任務(wù))在特定mode下執(zhí)行
- 可以添加observe監(jiān)聽runloop的一些狀態(tài),比如監(jiān)聽點擊事件的處理(在所有點擊事件之前做一些事情)
4.ARC下是否還需要進(jìn)行內(nèi)存管理?
- 需要。即便項目是ARC的情況下,針對Core Foundation下創(chuàng)建的對象也需要進(jìn)行內(nèi)存管理,因為ARC是針對OC而言的,而Core Foundation針對的是C語言.
凡是帶有Create、Copy、Retain等字眼的函數(shù),最后都要記得調(diào)用CFRelease(對象)
5.NSTimer受runloop的影響,精度上存在誤差,如何解決?
- 使用GCD創(chuàng)建timer,創(chuàng)建出來的timer不受runloop的影響,不會被添加到任何mode下,精度更高.
寫在最后
很久很久沒有更新博客,一直忙于日常的工作,有時候?qū)W了新東西想寫一寫,可能發(fā)現(xiàn)網(wǎng)上早已經(jīng)有了很多關(guān)于這方面的文章,于是便放棄了寫下去的念頭。其實,人都是有惰性的,總覺得看現(xiàn)成的比自己去寫一寫要來得快一些,但是整理知識點的過程,我們實際上也在加深一遍理解,而學(xué)習(xí)是一個不斷重復(fù)的過程。對于已經(jīng)掌握了相關(guān)知識的人,這種總結(jié)性文章可能毫無意義,但是對于想入門學(xué)習(xí)的人,文章能夠做到淺顯易懂,它就是有價值的。做了很久的開發(fā),發(fā)現(xiàn)實際編碼中我們真的很渺小,我們總是在搭建UI,創(chuàng)建model,網(wǎng)絡(luò)請求,數(shù)據(jù)填充,搞點炫酷的動畫,和產(chǎn)品經(jīng)理撕逼。即便你會各種黑魔法,各種超能力,能夠用到的機會其實并不多。所以越是基礎(chǔ)的東西越需要打牢,有了基礎(chǔ)才能舉一反三,才能一步一步的去解決更多刁鉆的需求。