RunLoop簡介
- RunLoop 是一個運行循環(huán),內部類似do-while循環(huán)饼问,線程進入并使用它來運行響應輸入事件的事件處理程序。
- 作用是保持程序的持續(xù)運行〉樵撸可以處理 App 中的各種事件(如觸摸事件浅悉、定時器事件趟据、selector 事件),節(jié)省了 cpu 資源术健,提高了程序的性能汹碱,該做事時做事該休息時候休息。
- 在程序的入口UIApplicationMain函數內部就啟動了一個RunLoop荞估,所以函數一直沒有返回咳促,保持了程序的持續(xù)運行。
RunLoop與線程的關系
- 每條線程都有唯一一個與之對應的 RunLoop 對象勘伺。
- 主線程的RunLoop已經自動創(chuàng)建好了跪腹,子線程的RunLoop會在第一次獲取它時創(chuàng)建并啟動,在線程結束時銷毀飞醉。
- RunLoop保存在一個全局的Dictionary里冲茸,線程作為key,RunLoop作為value缅帘。
RunLoop相關API
iOS中有2套API來訪問和使用RunLoop轴术,
- Core Foundation: CFRunLoopRef
- Foundation: NSRunLoop (NSRunLoop是基于CFRunLoopRef的一層 OC 包裝)
RunLoop相關類
Core Foundation中提供了關于RunLoop的5個類
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
內存結構和關系如下圖:
image.png
image.png
屏幕快照 2018-07-18 下午3.18.24.png
CFRunLoopModeRef
- CFRunLoopModeRef代表Runloop的運行模式。
- runloop啟動之后會選擇一種運行模式钦无,只能指定其中一個 Mode逗栽,作為 currentMode。
- 一個RunLoop包含若干個 Mode铃诬,每個 Mode 又包含若干個Source0/Source1/Timer/Observer祭陷。
- 如果當前Mode中沒有任何Sources0/Sources1/Timer/Observer苍凛,RunLoop會立馬退出。
- 如果需要切換Mode兵志,只能退出Loop醇蝴,再重新指定一個Mode進入,以保證不同Mode的Source/Timer/Observer分隔開來,互不影響想罕。
- 系統(tǒng)默認提供的Run Loop Modes有:
1悠栓、kCFRunLoopDefaultMode(NSDefaultRunLoopMode)
:系統(tǒng)默認的Runloop Mode,例如進入iOS程序默認不做任何操作就處于這種Mode中按价。
2惭适、UITrackingRunLoopMode
:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動楼镐,保證界面滑動時不受其他 Mode 影響癞志。(除非你將其他Source/Timer設置到UITrackingRunLoopMode下)
3、kCFRunLoopCommonModes(NSRunLoopCommonModes)
: 其實這個并不是某種具體的Mode框产,而是一個占位用的Mode凄杯,是一種模式組合,在iOS系統(tǒng)中默認包含了NSDefaultRunLoopMode和UITrackingRunLoopMode(注意:并不是說Runloop會運行在kCFRunLoopCommonModes這種模式下秉宿,而是相當于分別注冊了 NSDefaultRunLoopMode和 UITrackingRunLoopMode
4戒突、UIInitializationRunLoopMode
: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用描睦。
5膊存、GSEventReceiveRunLoopMode
: 接受系統(tǒng)事件的內部 Mode,通常用不到忱叭。
CFRunLoopSourceRef
事件源(輸入源)
在 iOS 中有兩種分類方法隔崎,按照以前的分類方法可以分為:①基于端口的;②自定義的窑多;③performSelector事件仍稀;
按照函數調用棧來劃分,可以分為source0和soucr1埂息。
- source0:非基于端口的事件源,(負責App內部事件遥巴,由App負責管理觸發(fā)千康,例如UITouch事件)
- Source1:端口事件源,可以監(jiān)聽系統(tǒng)端口和其他線程相互發(fā)送消息铲掐,基于Port的線程間通信
系統(tǒng)事件捕捉拾弃,它能夠主動喚醒RunLoop(由操作系統(tǒng)內核進行管理,例如CFMessagePort消息)摆霉。
CFRunLoopTimerRef
- CFRunLoopTimerRef基于時間的觸發(fā)器豪椿,基本上來說就是NSTimer奔坟,受RunLoop的Model的影響。
- 注意:GCD的定時器不受RunLoop的Model的影響
- NSTimer添加定時器有兩種方法:
1搭盾、timerWithXXX
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;
創(chuàng)建 timer咳秉,需要手動把 timer 添加到 RunLoop 中,可以指定添加到哪種模式下鸯隅。
2澜建、 scheduledTimerWithXXX
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block ;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
這種方法除了創(chuàng)建一個定時器外會自動以NSDefaultRunLoopModeMode添加到當前線程RunLoop中,但是如果滾動UIScrollView(UITableView蝌以、UICollectionview等類似的)是無法正常工作的炕舵,但是如果將NSDefaultRunLoopMode改為NSRunLoopCommonModes則可以正常工作。
注意:
- 非主線程的RunLoop并不會自動創(chuàng)建跟畅,直到第一次使用咽筋,RunLoop運行必須要在加入NSTimer輸入后運行否則會直接退出。
-
performSelector:withObject:afterDelay:
執(zhí)行的本質還是通過創(chuàng)建一個NSTimer然后加入到當前線程徊件。類似的方法還有performSelector:onThread:withObject:afterDelay:
晤硕,它會在另一個線程的RunLoop中創(chuàng)建一個Timer),此方法事實上在任務執(zhí)行完之前會對觸發(fā)對象形成引用庇忌,任務執(zhí)行完進行釋放舞箍。(注意:performSelector: withObject:等方法則等同于直接調用,原理與此不同)皆疹。
CFRunLoopObserverRef
相當于消息循環(huán)中的一個監(jiān)聽器疏橄,隨時通知外部當前RunLoop的運行狀態(tài)(它包含一個函數指針callout將當前狀態(tài)及時告訴觀察者)。具體的Observer狀態(tài)如下
kCFRunLoopEntry = (1UL << 0), 即將進入runloop
kCFRunLoopBeforeTimers = (1UL << 1), 即將處理timer事件
kCFRunLoopBeforeSources = (1UL << 2),即將處理source事件
kCFRunLoopBeforeWaiting = (1UL << 5),即將進入睡眠,一般在這個狀態(tài)進行UI界面的刷新和對自動釋放池進行release操作
kCFRunLoopAfterWaiting = (1UL << 6), 被喚醒
kCFRunLoopExit = (1UL << 7), runloop退出
kCFRunLoopAllActivities = 0x0FFFFFFFU
和定時器類似,runLoop 觀察者可以只用一次或循環(huán)使用略就。若只用一次,那么在 它啟動后,會把它自己從 runLoop 里面移除,而循環(huán)的觀察者則不會捎迫。你在創(chuàng)建 runLoop 觀察者的時候需要指定它是運行一次還是多次。
給runloop添加觀察者代碼:
// 創(chuàng)建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
{
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopEntry - %@", mode);
CFRelease(mode);
}
break;
case kCFRunLoopExit:
{
CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
NSLog(@"kCFRunLoopExit - %@", mode);
CFRelease(mode);
}
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
default:
break;
}
});
// 添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 釋放
CFRelease(observer);
Run Loop 的運行邏輯
屏幕快照 2018-07-18 下午3.33.50.png
RunLoop休眠的實現原理
屏幕快照 2018-07-18 下午3.39.58.png
在開發(fā)中如何使用RunLoop表牢?什么應用場景窄绒?
- 解決NSTimer在滑動時停止工作的問題
- 監(jiān)控應用卡頓
- 性能優(yōu)化
- 控制線程生命周期(線程保活)崔兴,封裝代碼如下
//LZPermenantThread.h
#import <Foundation/Foundation.h>
typedef void (^LZPermenantThreadTask)(void);
@interface LZPermenantThread : NSObject
/**
開啟線程
*/
//- (void)run;
/**
在當前子線程執(zhí)行一個任務
*/
- (void)executeTask:(LZPermenantThreadTask)task;
/**
結束線程
*/
- (void)stop;
@end
//LZPermenantThread.m
#import "LZPermenantThread.h"
@interface LZPermenantThread()
@property (strong, nonatomic) NSThread *innerThread;
@end
@implementation LZPermenantThread
#pragma mark - public methods
- (instancetype)init
{
if (self = [super init]) {
self.innerThread = [[NSThread alloc] initWithBlock:^{
// 創(chuàng)建上下文(要初始化一下結構體)
CFRunLoopSourceContext context = {0};
// 創(chuàng)建source
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
// 往Runloop中添加source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
// 銷毀source
CFRelease(source);
// 啟動
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
// while (weakSelf && !weakSelf.isStopped) {
// // 第3個參數:returnAfterSourceHandled彰导,設置為true,代表執(zhí)行完source后就會退出當前l(fā)oop
// CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
// }
}];
[self.innerThread start];
}
return self;
}
//- (void)run
//{
// if (!self.innerThread) return;
//
// [self.innerThread start];
//}
- (void)executeTask:(LZPermenantThreadTask)task
{
if (!self.innerThread || !task) return;
[self performSelector:@selector(__executeTask:) onThread:self.innerThread withObject:task waitUntilDone:NO];
}
- (void)stop
{
if (!self.innerThread) return;
[self performSelector:@selector(__stop) onThread:self.innerThread withObject:nil waitUntilDone:YES];
}
- (void)dealloc
{
[self stop];
}
#pragma mark - private methods
- (void)__stop
{
CFRunLoopStop(CFRunLoopGetCurrent());
self.innerThread = nil;
}
- (void)__executeTask:(LZPermenantThreadTask)task
{
task();
}
@end
自動釋放池與RunLoop
kCFRunLoopEntry; // 當runloop進入的時候會創(chuàng)建一個自動釋放
kCFRunLoopBeforeWaiting; // 當runloop即將進入休眠的時候會把之前的自動釋放池先銷毀敲茄,然后創(chuàng)建一個新的自動釋放池位谋。
kCFRunLoopExit; // 當runloop退出的時候會把之前的自動釋放池銷毀。