什么是 RunLoop
通常在終端中輸入命令,執(zhí)行任務(wù)的線程執(zhí)行完就退出了,等我們?cè)俅屋斎朊?,終端再開(kāi)始執(zhí)行任務(wù)。但在我們的 app 中,要保持一直運(yùn)行(除非app被掛起),不斷接受用戶的輸入,循環(huán)的接受、處理事件,類似于這樣:
while(AppIsRunning){ //只要 app 處于運(yùn)行狀態(tài),就要不斷等待著處理事件
id whoWakesMe = SleepForWakingUp();
id event = GetEvent(whoWakesMe);
HandleEvent(event);
}
RunLoop 來(lái)幫助線程管理一個(gè)或多個(gè)事件或消息,接受用戶輸入等事件源,在事件到達(dá)時(shí),RunLoop 立刻喚醒線程來(lái)處理事件;沒(méi)有事件需要處理時(shí),RunLoop 幫助線程休眠,避免其占用資源,這里是幫助其休眠,而不是直接退出。
RunLoop 還決定了程序在何時(shí)應(yīng)該處理那些事件,并且為被調(diào)用的對(duì)象維護(hù)一個(gè)消息隊(duì)列,被調(diào)用方從這個(gè)消息隊(duì)列中取出需要他處理的事件。
主線程的 RunLoop 默認(rèn)開(kāi)啟,而子線程需要調(diào)用[NSRunLoop currentRunLoop]創(chuàng)建和獲取 RunLoop,RunLoop 的銷毀發(fā)生在線程結(jié)束時(shí)。
RunLoop 與線程的關(guān)系
每個(gè)線程創(chuàng)建的時(shí)候,都有一個(gè) RunLoop 循環(huán),與線程一一對(duì)應(yīng)。
RunLoop 構(gòu)成

如圖可以看到 RunLoop 的大致構(gòu)成,它與線程一一對(duì)應(yīng),而擁有多個(gè)CFRunLoopMode,mode 是一系列輸入事件源、計(jì)時(shí)器、runLoop 觀察者的集合。
RunLoop Mode
RunLoop 只能選擇一個(gè) Mode 啟動(dòng),同時(shí)在“跑”的時(shí)候,總是在特定的唯一的 mode 下,每次運(yùn)行 RunLoop 都要顯式或隱式的指定運(yùn)行 mode。這個(gè) mode 包含了當(dāng)前需要處理的 Source/Timer/Observer,所以 RunLoop 在時(shí)刻內(nèi),僅能處理與當(dāng)前 mode 相關(guān)聯(lián)的事件,只有和模式相關(guān)的源才會(huì)被監(jiān)視,并允許他們傳遞事件消息。
為了保證其中的 Source/Timer/Observer 與其他 mode 的相隔離,切換 mode 時(shí),只能先退出當(dāng)前RunLoop,再以要切換的 mode 重新進(jìn)入RunLoop。
開(kāi)發(fā)中,通常會(huì)遇到這幾種Mode:
- kCFRunLoopDefaultMode:app的默認(rèn) Mode,通常主線程在這個(gè) Mode 下運(yùn)行。
- UITrackingRunLoopMode:界面跟蹤 Mode,ScrollView 的觸摸滑動(dòng) mode (在iOS中,觸摸滑動(dòng)很流暢的原因是在滑動(dòng)時(shí),只處理此 mode 下的事件且不受其他mode影響)。
- UIInitializationRunLoopMode:剛啟動(dòng) app 進(jìn)入的第一個(gè) mode,起到過(guò)渡的作用,啟動(dòng)完成后不再使用。
- GSEventReceiveRunLoopMode: Graphic 相關(guān)事件的 mode,通常用不到。
- kCFRunLoopCommonModes:將 mode 標(biāo)記為"common"屬性,當(dāng) RunLoop 運(yùn)行在標(biāo)記為"common"屬性的任一 mode 下,發(fā)生事件時(shí),里面的 mode 都會(huì)被觸發(fā)。
RunLoop Source
線程的異步事件源,數(shù)據(jù)源。有兩種Source,可以用是否基于Mach Port(進(jìn)程間通訊接口)區(qū)分:
- source0:不基于Mach Port,處理app內(nèi)部事件,用戶自定義的thread發(fā)出。當(dāng)我們使用 NSObject 中的 performSelector 系列方法時(shí),都是source0 事件源。
- source1:基于Mach Port,是由RunLoop和內(nèi)核管理的。
RunLoop Timer
線程的同步事件源,在預(yù)設(shè)的時(shí)間點(diǎn)到了之后同步的發(fā)給線程處理此事件。
RunLoop Observer
Observer 可對(duì) RunLoop 的狀態(tài)變化進(jìn)行觀察,可觀察的變化:
- 剛進(jìn)入此 RunLoop 中
- RunLoop 準(zhǔn)備處理一個(gè) Timer
- RunLoop 準(zhǔn)備處理一個(gè) Input Source
- RunLoop 準(zhǔn)備進(jìn)入睡眠
- RunLoop 將被喚醒處理事件之前
- RunLoop 準(zhǔn)備退出
因?yàn)镺bserver可對(duì)這些事件進(jìn)行觀察追蹤,所以也可被看作是一種事件源。
RunLoop處理的流程

第7步中,當(dāng)線程進(jìn)入休眠,發(fā)生下列事件,線程將被喚醒:
- 基于 Port 的事件發(fā)生
- 計(jì)時(shí)器到時(shí)
- 被代碼顯式喚醒
第9步中,處理喚醒時(shí)收到的消息,并且:
- 如果是用戶定義的計(jì)時(shí)器到時(shí),處理事件并重啟 RunLoop
- 如果有input 事件源,傳遞這個(gè)消息
- 如果runloop顯式被喚醒,且沒(méi)有超時(shí),重啟RunLoop
之后,跳回第2步
RunLoop應(yīng)用舉例
在漫長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)的理論說(shuō)明后,讓我們看看實(shí)際開(kāi)發(fā)中,有哪些地方會(huì)用到 RunLoop 呢?
解決 NSTimer "不準(zhǔn)"的問(wèn)題
我們有時(shí)候會(huì)發(fā)現(xiàn) NSTimer "不太準(zhǔn)",明明時(shí)間已經(jīng)到了,該執(zhí)行的回調(diào)卻未發(fā)生,這是因?yàn)槲覀兂3?NSTimer 默認(rèn)設(shè)置為default mode,如果這時(shí)屏幕滾動(dòng),mode切換為TrackingMode,時(shí)間到了,但是 TrackingMode 無(wú)法處理 defaultMode下的回調(diào),造成"不準(zhǔn)"。
在 SVProgressHUD 中,我們可以設(shè)置轉(zhuǎn)圈的提示框自動(dòng)消失,可開(kāi)啟一個(gè)定時(shí)器,在到了設(shè)定的時(shí)間點(diǎn)后消失,如下
strongSelf.fadeOutTimer = [NSTimer timerWithTimeInterval:duration target:strongSelf selector:@selector(dismiss) userInfo:nil repeats:NO];
[[NSRunLoop mainRunLoop] addTimer:strongSelf.fadeOutTimer forMode:NSRunLoopCommonModes];
strongSelf 即為提示框,將它消失的定時(shí)器添加在 RunLoop 的common 模式下,不管時(shí)間點(diǎn)到了的那一時(shí)刻 RunLoop 運(yùn)行在哪個(gè)mode下,都會(huì)處理消失的回調(diào),"準(zhǔn)點(diǎn)消失"。
用 dispatch_after 定時(shí),就準(zhǔn)了嗎
我發(fā)現(xiàn)有很多博客寫(xiě),NSTimer 造成定時(shí)不準(zhǔn)的問(wèn)題可以通過(guò) GCD 中的 dispatch_after 來(lái)解決,但是 dispatch_after 并不是說(shuō)在指定時(shí)間后執(zhí)行處理,而只是在指定時(shí)間將操作追加到 Dispatch Queue 中。如果指定時(shí)間到了,需要加入的隊(duì)列正在進(jìn)行耗時(shí)操作,定時(shí)操作并不能立即執(zhí)行,也會(huì)造成不準(zhǔn)。
驗(yàn)證如下:
//獲取主隊(duì)列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
//定時(shí)時(shí)間
int64_t delay = 5 * NSEC_PER_SEC;
//定時(shí)時(shí)間,即從現(xiàn)在到定時(shí)的時(shí)間
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, delay);
NSLog(@"開(kāi)始計(jì)時(shí): %@", [NSDate date]);
dispatch_after(delayTime, mainQueue, ^{
NSLog(@"時(shí)間到: %@", [NSDate date]);
});
//在這里設(shè)置一些復(fù)雜操作,比方來(lái)10000次網(wǎng)絡(luò)請(qǐng)求


可以看到雖然我們只設(shè)置延遲5秒進(jìn)行,但事實(shí)上,在10秒才進(jìn)行了延遲操作。但是日常的開(kāi)發(fā)中,碰到這么這么復(fù)雜的情況應(yīng)該是比較少的,所以 dispatch_after 也可以一用~~~
GCD 中除了主要的 Dispatch Queue 之外,還對(duì) BSD 系內(nèi)核慣有功能 kqueue 進(jìn)行包裝,可處理內(nèi)核中發(fā)生的各種事件及方法。
其中的 DISPATCH_SOURCE_TYPE_TIMER 可作為定時(shí)器,幫助我們延遲調(diào)用:
//獲取主隊(duì)列
dispatch_queue_t mainQueue = dispatch_get_main_queue();
//新生成一個(gè)定時(shí)器,且此定時(shí)器不能為局部變量,否則方法執(zhí)行完就被銷毀了,還怎么做定時(shí)后的回調(diào)呢?
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, mainQueue);
//定時(shí)時(shí)間
int64_t delay = 5 * NSEC_PER_SEC;
//一定容差范圍時(shí)間
int64_t leeway = 0.1 * NSEC_PER_SEC;
//定時(shí)時(shí)間,即從現(xiàn)在到定時(shí)的時(shí)間
dispatch_time_t delayTime = dispatch_time(DISPATCH_TIME_NOW, delay);
//設(shè)置定時(shí)器
//下一次回調(diào)為DISPATCH_TIMER_FOREVER,表示不需要重復(fù)
dispatch_source_set_timer(self.timer, delayTime,DISPATCH_TIMER_FOREVER, leeway);
//設(shè)置時(shí)間到了后的回調(diào)
__weak typeof(self) weakSelf = self;
dispatch_source_set_event_handler(self.timer, ^{
typeof(self) strongSelf = weakSelf;
NSLog(@"計(jì)時(shí)結(jié)束: %@", [NSDate date]);
dispatch_source_cancel(strongSelf.timer);
});
//啟動(dòng)定時(shí)器
dispatch_resume(self.timer);
保證線程的持續(xù)運(yùn)行
在 AFNetworking 2.3 中,需要一個(gè)自定義線程接受 connection 回調(diào),一開(kāi)始初始化線程時(shí),沒(méi)有需要執(zhí)行的操作,線程會(huì)退出(RunLoop中沒(méi)有source/timer/observer 會(huì)立即退出)。為其添加一個(gè)MachPort,為了保證線程的存活。
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
//初始化線程時(shí),調(diào)用networkRequestThreadEntryPoint方法
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"AFNetworking"];
//為線程創(chuàng)建RunLoop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
//為RunLoop添加事件,保證其持續(xù)運(yùn)行
[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
}
}
解決TableView加載圖片時(shí),滑動(dòng)很卡
TableView 需要加載大量圖片時(shí),滑動(dòng)后,界面會(huì)卡,這是因?yàn)榇藭r(shí)RunLoop 運(yùn)行在 UITrackingRunLoopMode 下,圖片加載在當(dāng)前mode下,cpu 又要處理加載圖片事件,又要處理滑動(dòng)事件,造成卡頓。
可以顯式地將圖片的加載設(shè)置在 NSDefaultRunLoopMode 下,滑動(dòng)時(shí)的 UITrackingRunLoopMode 并不會(huì)去加載圖片,解決卡頓問(wèn)題。
[self.imageView performSelector:@selector(setImage:) withObject:downloadImage afterDelay:0 inModes:@[NSDefaultRunLoopMode]];
自動(dòng)釋放池到底在何時(shí)釋放?
我們知道,手動(dòng)指定 autoreleasepool 中的對(duì)象,會(huì)在作用域結(jié)束時(shí)釋放掉。而設(shè)置為 autorelease 的對(duì)象是在出了作用域之后,被自動(dòng)添加到最近創(chuàng)建的自動(dòng)釋放池中。那么這個(gè)自動(dòng)釋放池遲早有被撐滿需要釋放的時(shí)刻,這個(gè)自動(dòng)釋放池具體是什么時(shí)候被釋放呢?
在Cocoa框架中,相當(dāng)于程序主循環(huán)的NSRunLoop或者在其他程序可運(yùn)行的地方,對(duì) NSAutoreleasePool 對(duì)象進(jìn)行生成、持有和廢棄處理。
---引自《Objective-C 高級(jí)編程》
而它能夠釋放的原因是系統(tǒng)在每個(gè) runloop 迭代中都加入了自動(dòng)釋放池 Push 和 Pop
下面我們舉例討論下:
@property (nonatomic,weak)NSString * weakStr;
- (void)viewDidLoad {
[super viewDidLoad];
NSString *string = [NSString stringWithFormat:@"這個(gè)string要設(shè)置的很長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)長(zhǎng)"];
//因?yàn)樘O(píng)果引用Tagged Pointer專門存儲(chǔ)小的對(duì)象,直接存儲(chǔ)其值,而不是存儲(chǔ)地址
//如果string很短,用Tagged Pointer存儲(chǔ),無(wú)法驗(yàn)證其自動(dòng)釋放,地址被收回的過(guò)程
weakStr = string;
NSLog(@"viewDidLoad:%@",weakStr);
NSLog(@"currentRunLoop:%@",[[NSRunLoop currentRunLoop] currentMode]);
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
NSLog(@"viewWillAppear:%@",weakStr);
NSLog(@"currentRunLoop:%@",[[NSRunLoop currentRunLoop] currentMode]);
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
NSLog(@"viewDidAppear:%@",weakStr);
NSLog(@"currentRunLoop:%@",[[NSRunLoop currentRunLoop] currentMode]);
}
輸出如圖:



我們?cè)趘iewDidLoad方法中,用stringWithFormat類方法生成一個(gè)字符串,這種方法生成的字符串默認(rèn)被添加進(jìn) autoreleasepool 中。
viewDidLoad 和 viewWillAppear 還在app初始化的 UIInitializationRunLoopMode 下,而 viewDidAppear 已經(jīng)進(jìn)入了默認(rèn)mode下了。期間,autoreleasepool 出現(xiàn)了一次銷毀,其中的對(duì)象也就被銷毀了。
所以說(shuō),在沒(méi)有手加Autorelease Pool的情況下,Autorelease對(duì)象是在當(dāng)前的runloop迭代結(jié)束時(shí)釋放的,而它能夠釋放的原因是系統(tǒng)在每個(gè)runloop迭代中都加入了自動(dòng)釋放池Push和Pop。
關(guān)于RunLoop的一道題
NSRunLoop 的描述正確的是( )
A. RunLoop 決定程序在何時(shí)應(yīng)該處理哪些 Event
B. Cocoa 中的 NSRunLoop 類并不是線程安全的
C. RunLoop 可以使程序一直運(yùn)行接受用戶輸入
D. RunLoop 起到了調(diào)用解耦的作用
我怎么覺(jué)得 ABCD 四個(gè)選項(xiàng)都對(duì)嘞……
參考文章:
RunLoops 官方文檔
深入理解RunLoop
黑幕背后的Autorelease
Objective-C Autorelease Pool 的實(shí)現(xiàn)原理
RunLoop個(gè)人小結(jié)