一.RunLoop介紹
-
1.概念
RunLoop是一個運(yùn)行循環(huán),正是因?yàn)镽unLoop,IOS才可以保持程序的持續(xù)運(yùn)行匿醒,處理App中的各種事件,并且可以節(jié)省CPU資源缠导,提高性能(因?yàn)镽unLoop可以做到工作休息兩不誤)廉羔。因?yàn)橐话銇碇v,一個線程在處理完一個任務(wù)以后就會退出僻造。
線程與RunLoop
- 每條線程都有唯一的一個與之對應(yīng)的RunLoop對象
- 主線程的RunLoop已經(jīng)自動創(chuàng)建好了憋他,子線程的RunLoop需要通過系統(tǒng)提供的方法進(jìn)行獲取
- 獲取RunLoop以后,如果沒有事件源和Timer事件或者沒有設(shè)置RunLoop運(yùn)行模式髓削,RunLoop會在獲取以后立即銷毀举瑰;如果超時,RunLoop也會被銷毀蔬螟。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
UIApplicationMain內(nèi)部就開啟了一個RunLoop此迅,這個函數(shù)是沒有返回值的,因?yàn)镽unLoop是一個運(yùn)行循環(huán)。如果此處改為:
int main(int argc, char * argv[]) {
@autoreleasepool {
return 0;
}
}
App在啟動以后耸序,就會結(jié)束運(yùn)行忍些。
-
2.如何訪問RunLoop對象
-
Foundation
NSRunLoop
[NSRunLoop currentRunLoop];//獲取當(dāng)前線程RunLoop,如果當(dāng)前線程RunLoop未創(chuàng)建坎怪,則創(chuàng)建罢坝。
[NSRunLoop mainRunLoop];//獲取主線程RunLoop
-
Core Foundation
CFRunLoopRef
CFRunLoopGetCurrent();//獲取當(dāng)前線程RunLoop,如果當(dāng)前線程RunLoop未創(chuàng)建搅窿,則創(chuàng)建嘁酿。
CFRunLoopGetMain();//獲取主線程RunLoop
NSRunLoop是CFRunLoopRef的OC封裝
-
3.RunLoop相關(guān)類介紹
- CFRunLoopRef【RunLoop對象】
- CFRunLoopModeRef【RunLoop的運(yùn)行模式】
- CFRunLoopSourceRef【RunLoop要處理的事件源】
- CFRunLoopTimerRef【Timer事件】
- CFRunLoopObserverRef【RunLoop觀察者】
一個 RunLoop 包含若干個 Mode,每個 Mode 又包含若干個 Source/Timer/Observer男应。每次調(diào)用 RunLoop 的主函數(shù)時闹司,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode沐飘。如果需要切換 Mode游桩,只能退出 Loop,再重新指定一個 Mode 進(jìn)入耐朴。這樣做主要是為了分隔開不同組的 Source/Timer/Observer借卧,讓其互不影響。
-
CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的運(yùn)行模式(下面列舉5種)
NSDefaultRunLoopMode(Cocoa)/kCFRunLoopDefaultMode(Core Foundation):App的默認(rèn)Mode筛峭,通常主線程是在這個Mode下運(yùn)行
UITrackingRunLoopMode:界面跟蹤 Mode铐刘,用于 ScrollView 追蹤觸摸滑動,保證界面滑動時不受其他 Mode 影響
UIInitializationRunLoopMode: 在剛啟動 App 時第進(jìn)入的第一個 Mode影晓,啟動完成后就不再使用
GSEventReceiveRunLoopMode: 接受系統(tǒng)事件的內(nèi)部 Mode镰吵,通常用不到
NSRunLoopCommonModes(Cocoa)/kCFRunLoopCommonModes(Core Foundation): 這是一個占位用的Mode,不是一種真正的Mode
- 一個 RunLoop 包含若干個 Mode俯艰,每個Mode又包含若干個Source/Timer/Observer
- 每次RunLoop啟動時捡遍,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode
- 如果需要切換Mode竹握,只能退出Loop画株,再重新指定一個Mode進(jìn)入
這樣做主要是為了分隔開不同組的Source/Timer/Observer,讓其互不影響- kCFRunLoopCommonModes是一種模式組合啦辐,IOS系統(tǒng)中默認(rèn)包含了kCFRunLoopDefaultMode和UITrackingRunLoopMode,系統(tǒng)會分別注冊這兩種模式谓传,還可以通過CFRunLoopAddCommonMode()將自定義Mode放到kCFRunLoopCommonModes中。
-
CFRunLoopSourceRef
-
按照官方文檔的分類
Port-Based Sources (基于端口,跟其他線程交互,通過內(nèi)核發(fā)布的消息)
Custom Input Sources (自定義)
Cocoa Perform Selector Sources (performSelector…方法) -
按照函數(shù)調(diào)用棧的分類
Source0:非基于Port的
Source1:基于Port的
Source0: event事件芹关,只含有回調(diào)续挟,需要先調(diào)用CFRunLoopSourceSignal(source),將這個 Source 標(biāo)記為待處理侥衬,然后手動調(diào)用 CFRunLoopWakeUp(runloop) 來喚醒 RunLoop诗祸。
Source1: 包含了一個 mach_port 和一個回調(diào)跑芳,被用于通過內(nèi)核和其他線程相互發(fā)送消息,能主動喚醒 RunLoop 的線程。
-
CFRunLoopTimerRef
CFRunLoopTimerRef是基于時間的觸發(fā)器
基本上說的就是NSTimer(CADisplayLink也是加到RunLoop),它受RunLoop的Mode影響
GCD的定時器不受RunLoop的Mode影響
-
CFRunLoopObserverRef
CFRunLoopObserverRef是觀察者直颅,能夠監(jiān)聽RunLoop的狀態(tài)改變
可以監(jiān)聽的RunLoop狀態(tài)
/* Run Loop Observer Activities */
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
kCFRunLoopAllActivities = 0x0FFFFFFFU//以上所有狀態(tài)
};
監(jiān)聽RunLoop狀態(tài)示例:
//新建子線程
- (void)viewDidLoad {
[super viewDidLoad];
//新建子線程
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(subThreadTask) object:nil];
[self.thread start];
}
- (void)subThreadTask {
//創(chuàng)建觀察者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
{
NSLog(@"即將進(jìn)入RunLoop");
}
break;
case kCFRunLoopBeforeTimers:
{
NSLog(@"即將處理Timer");
}
break;
case kCFRunLoopBeforeSources:
{
NSLog(@"即將處理Source");
}
break;
case kCFRunLoopBeforeWaiting:
{
NSLog(@"即將進(jìn)入休眠");
}
break;
case kCFRunLoopAfterWaiting:
{
NSLog(@"即將從休眠中喚醒");
}
break;
case kCFRunLoopExit:
{
NSLog(@"即將退出RunLoop");
}
break;
default:
break;
}
});
//添加觀察者
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
//釋放資源
CFRelease(observer);
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
self.timer = [NSTimer timerWithTimeInterval:2.0f target:self selector:@selector(timerActon) userInfo:nil repeats:YES];
[runLoop addTimer:self.timer forMode:NSDefaultRunLoopMode];
[runLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:4.0f]];
}
- (void)timerActon {
NSLog(@"定時器工作了");
}
運(yùn)行程序控制臺輸出:
2018-05-24 09:32:04.495445+0800 Test[1059:57057] 即將進(jìn)入RunLoop
2018-05-24 09:32:04.499081+0800 Test[1059:57057] 即將處理Timer
2018-05-24 09:32:04.502540+0800 Test[1059:57057] 即將處理Source
2018-05-24 09:32:04.503239+0800 Test[1059:57057] 即將進(jìn)入休眠
2018-05-24 09:32:06.497401+0800 Test[1059:57057] 即將從休眠中喚醒
2018-05-24 09:32:06.497973+0800 Test[1059:57057] 定時器工作了
2018-05-24 09:32:06.498469+0800 Test[1059:57057] 即將處理Timer
2018-05-24 09:32:06.498736+0800 Test[1059:57057] 即將處理Source
2018-05-24 09:32:06.499872+0800 Test[1059:57057] 即將進(jìn)入休眠
2018-05-24 09:32:08.500044+0800 Test[1059:57057] 即將從休眠中喚醒
2018-05-24 09:32:08.500392+0800 Test[1059:57057] 定時器工作了
2018-05-24 09:32:08.500790+0800 Test[1059:57057] 即將退出RunLoop
注:因?yàn)槎〞r器是每兩秒鐘調(diào)用一次博个,子線程的runLoop在4秒鐘以后會銷毀,所以定時器會輸出兩次功偿。4秒鐘以后盆佣,runLoop會銷毀,在銷毀以前觀察者會收到"即將推出RunLoop"的狀態(tài)通知械荷。
二.RunLoop流程
-
1.官方文檔
RunLoop官方流程圖
來自Apple官方文檔翻譯 -
2.網(wǎng)友對官方文檔的整理
RunLoop流程
三.RunLoop應(yīng)用
-
1.NSTimer
創(chuàng)建一個tableView和一個定時器浸踩,定時器用于顯示當(dāng)前時間朗徊。當(dāng)tableView靜止的時候介袜,定時器正常工作蛮粮。當(dāng)拖拽tableView的時候,定時器停止了工作关拒。
定時器的創(chuàng)建代碼:
//創(chuàng)建定時器 并指定RunLoop運(yùn)行模式為NSDefaultRunLoopMode
self.timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(refreshContentLabel) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
如果需要在列表滑動時定時器繼續(xù)工作佃蚜,則需要指定RunLoop運(yùn)行模式為NSRunLoopCommonModes
//創(chuàng)建定時器 并指定RunLoop運(yùn)行模式為NSRunLoopCommonModes
self.timer = [NSTimer timerWithTimeInterval:1.0f target:self selector:@selector(refreshContentLabel) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSRunLoopCommonModes];
-
2.PerformSelector
PerformSelector 可以指定在何種RunLoopMode下進(jìn)行工作
//testAction僅在當(dāng)前RunLoop處于UITrackingRunLoopMode下工作,即ScrollView滾動的時候
[self performSelector:@selector(testAction) withObject:nil afterDelay:2.0f inModes:@[UITrackingRunLoopMode]];
-
3.常駐線程
有時候我們需要讓一個線程庇褂椋活着绊,即一直處于可以執(zhí)行任務(wù)的狀態(tài)。這時候我們就需要用到RunLoop熟尉。
一般情況归露,一個線程在任務(wù)執(zhí)行完畢以后,是不可以去執(zhí)行其他工作的:
- (IBAction)openSubThreadAndexecutingMethodA:(id)sender {
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(methodA) object:nil];
[self.thread start];
}
- (IBAction)executingMethodB:(id)sender {
[self performSelector:@selector(methodAB) onThread:self.thread withObject:nil waitUntilDone:YES];
}
- (void)methodA {
NSLog(@"方法A被執(zhí)行");
}
- (void)methodAB {
NSLog(@"方法B被執(zhí)行");
}
當(dāng)我們點(diǎn)擊"開啟子線程并執(zhí)行MethodA"進(jìn)行子線程創(chuàng)建斤儿,并且讓MethodA在子線程中執(zhí)行剧包。當(dāng)MethodA執(zhí)行完畢以后,我們?nèi)绻c(diǎn)擊
“讓子線程去執(zhí)行MethodB”按鈕往果,讓MethodB在之前創(chuàng)建的子線程中去執(zhí)行的話疆液,程序就會崩潰。因?yàn)楫?dāng)MethodA被執(zhí)行完畢以后陕贮,子線程已經(jīng)處于finished狀態(tài)堕油,系統(tǒng)會將其釋放。
解決上述問題肮之,讓子線程進(jìn)行常駐掉缺,隨時可以執(zhí)行任務(wù):
將上述methodA方法改為:
- (void)methodA {
//在當(dāng)前子線程中開啟一個RunLoop
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[runLoop run];
NSLog(@"方法A被執(zhí)行");
}
此時我們點(diǎn)擊"開啟子線程并執(zhí)行MethodA"創(chuàng)建子線程,并且執(zhí)行methodA戈擒。methodA方法是不會輸出"方法A被執(zhí)行"的眶明。因?yàn)槲覀儎?chuàng)建了一個持續(xù)運(yùn)行的RunLoop。只有RunLoop結(jié)束的時候筐高,此語句才會被輸出搜囱。我們點(diǎn)擊“讓子線程去執(zhí)行MethodB”按鈕去執(zhí)行methodB丑瞧,程序正常運(yùn)行。并且我們?nèi)绻@取此子線程的狀態(tài)蜀肘,它處于正在運(yùn)行的狀態(tài)嗦篱,這就達(dá)到了保活的目的幌缝。在開啟RunLoop的時候灸促,我們需要添加事件源或者Timer,否則RunLoop在獲取以后涵卵,會立馬運(yùn)行結(jié)束浴栽。
- 4.自動釋放池
第一次創(chuàng)建的時機(jī):即將進(jìn)入runloop的時候【kCFRunLoopEntry】。
釋放的時機(jī):runloop進(jìn)入休眠狀態(tài)【kCFRunLoopBeforeWaiting】轿偎,或者退出runLoop時【kCFRunLoopExit】典鸡。
- 注: 當(dāng)runloop即將休眠的時候會把之前的自動釋放池釋放,然后重新創(chuàng)建一個新的釋放池坏晦。