-
Run Loop是什么
RunLoop顧名思義最易,是運行循環(huán)怒坯。它跟線程是一一對應的,每一個線程都有一個RunLoop藻懒,在需要的時候創(chuàng)建剔猿。RunLoop的作用很簡單,就是保持線程不會退出嬉荆,并且處理一些事件归敬。
如果沒有RunLoop,線程只要一執(zhí)行完代碼就會退出鄙早。RunLoop類似一個while
循環(huán)汪茧,但是又不像while
循環(huán)會占用CPU資源,RunLoop在等待的時候處于休眠狀態(tài)限番,只有接收到事件時舱污,才會被喚醒,然后再做相應的處理弥虐。
程序啟動時扩灯,系統(tǒng)會自動為我們開啟主線程的RunLoop,這就保證了我們的程序不會退出躯舔,并且可以一直響應我們的操作驴剔。而子線程的RunLoop并沒有開啟,需要我們手動開啟粥庄。
-
Run Loop使用
說到使用RunLoop丧失,其實我們在使用NSTimer的時候就已經使用過它了,只不過那時候并沒有對RunLoop深入研究惜互,我們來重新體驗一下一個NSTimer的簡單使用:
//創(chuàng)建一個Timer
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"timer");
}];
//把它加到RunLoop里
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
一個NSTimer
必須和RunLoop一起工作布讹,不然它沒辦法運作琳拭。這里再給RunLoop添加timer
的時候,有一個參數叫Mode
描验,這是RunLoop模式白嘁,不同模式處理不同類型輸入源的事件。
-
NSDefaultRunLoopMode
:App的默認Mode膘流,通常主線程是在這個Mode下運行絮缅。 -
UITrackingRunLoopMode
:界面跟蹤 Mode,用于 ScrollView 追蹤觸摸滑動呼股,保證界面滑動時不受其他 Mode 影響耕魄。 -
UIInitializationRunLoopMode
: 在剛啟動 App 時第進入的第一個 Mode,啟動完成后就不再使用彭谁。 -
GSEventReceiveRunLoopMode
: 接受系統(tǒng)事件的內部 Mode吸奴,通常用不到堡妒。 -
NSRunLoopCommonModes
: 這是一個占位用的Mode灯谣,不是一種真正的Mode妖混,它會同時處理默認模式和UI模式中的事件庆聘。
到這里又引發(fā)出來了新的問題燥透,為什么NSTimer
必須添加到RunLoop才能使用呢概荷?
這就涉及到了RunLoop所能處理的事件了:
Run loop接收輸入事件來自兩種不同的來源:輸入源(input source)和定時源(timer source)悍手。輸入源傳遞異步事件渴逻,通常消息來自于其他線程或程序妙啃。定時源則傳遞同步事件档泽,發(fā)生在特定時間或者重復的時間間隔。兩種源都使用程序的某一特定的處理例程來處理到達的事件揖赴。
這張官方的圖簡單的描述了RunLoop所能處理的事件來源:
接下來馆匿,我們來玩一玩RunLoop。在OC中燥滑,有NSRunLoop
和CFRunLoop
兩種方式來獲取并且操作RunLoop渐北。其中NSRunLoop
是CFRunLoop
的封裝。我們以NSRunLoop
為主對RunLoop進行使用铭拧。首先赃蛛,我們創(chuàng)建一個線程,然后開啟它的runloop搀菩,我們如何證明它的runloop已經開啟呢呕臂?結合上圖,我們只需要找一個runloop能夠處理的事件肪跋,然后讓它去處理就可以了歧蒋,我這里挑選了- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait ;
方法:
//首先持有一個線程對象,方便我們之后使用它
@property (nonatomic, strong) NSThread *thread;
- (void)viewDidLoad {
[super viewDidLoad];
//初始化并開啟,在線程內部開啟它的runloop
self.thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"這是一條子線程%@",[NSThread currentThread]);
[[NSRunLoop currentRunLoop] run];
}];
[self.thread start];
}
//點擊屏幕時谜洽,在線程上執(zhí)行下面的打印
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
//打印出線程萝映,以便我們確認是同一條
- (void)test{
NSLog(@"哈哈哈%@",[NSThread currentThread]);
}
我們可以通過[NSRunLoop currentRunLoop]
獲取當前線程的RunLoop,開啟RunLoop只需要調用其run
方法阐虚。
按照我們預想的結果序臂,運行以后每次點擊屏幕都應該有輸出,但是實際上我們點擊屏幕并沒有任何效果实束。這是因為開啟RunLoop之前必須給其指定至少一種輸入源或者定時源奥秆,不然開啟之后會馬上退出。說到這里磕洪,我們得看一下RunLoop在一次循環(huán)的周期內吭练,到底做了什么事情:
每次運行run loop诫龙,你線程的run loop對會自動處理之前未處理的消息析显,并通知相關的觀察者。具體的順序如下:
- 通知觀察者run loop已經啟動
- 通知觀察者任何即將要開始的定時器
- 通知觀察者任何即將啟動的非基于端口的源
- 啟動任何準備好的非基于端口的源
- 如果基于端口的源準備好并處于等待狀態(tài)签赃,立即啟動谷异;并進入步驟9。
- 通知觀察者線程進入休眠
- 將線程置于休眠直到任一下面的事件發(fā)生:
- 某一事件到達基于端口的源
- 定時器啟動
- Run loop設置的時間已經超時
- run loop被顯式喚醒
- 通知觀察者線程將被喚醒锦聊。
- 處理未處理的事件
- 如果用戶定義的定時器啟動歹嘹,處理定時器事件并重啟run loop。進入步驟2
- 如果輸入源啟動孔庭,傳遞相應的消息
- 如果run loop被顯式喚醒而且時間還沒超時尺上,重啟run loop。進入步驟2
- 通知觀察者run loop結束圆到。
從以上我們可以知道怎抛,如果是定時器事件,執(zhí)行之后會直接重啟RunLoop芽淡,如果是其它事件马绝,處理完畢后,不會再次喚醒RunLoop挣菲,要想它繼續(xù)監(jiān)聽事件富稻,我們必須得手動喚醒它。之前在我們點擊的時候白胀,runloop已經退出了椭赋,所以代碼并沒有執(zhí)行。
不過這難不倒我們或杠,我們可以給它一個循環(huán)哪怔,讓它不斷得開啟:
while (true) {
[[NSRunLoop currentRunLoop] run];
}
再次運行,點擊屏幕就可以看到打印出的信息:
開啟RunLoop還有另外幾個方法,我們平時最好不要直接使用run方法蔓涧,可能會造成無限循環(huán):
//同run方法件已,增加超時參數limitDate,避免進入無限循環(huán)元暴。使用在UI線程(亦即主線程)上篷扩,可以達到暫停的效果。
(void)runUntilDate:(NSDate *)limitDate;
//等待消息處理茉盏,好比在PC終端窗口上等待鍵盤輸入鉴未。一旦有合適事件(mode相當于定義了事件的類型)被處理了,則立刻返回鸠姨;類同run方法铜秆,如果沒有事件處理也立刻返回;有否事件處理由返回布爾值判斷讶迁。同樣limitDate為超時參數连茧。
(BOOL)runMode:(NSString )mode beforeDate:(NSDate )limitDate;
以上是一些簡單的操作,我們可以利用RunLoop去監(jiān)測一些事件巍糯,當它發(fā)生的時候再去做處理啸驯。但是用while
循環(huán)會讓CPU一直在工作,所以我們最好設置一種終止RunLoop循環(huán)的條件祟峦。
前面提到罚斗,RunLoop在開啟時,需要給它指定輸入源宅楞,而輸入源是可以自定義的针姿,不過它需要使用CFRunLoop
。接下來我們可以自己自定義一種輸入源:
/* Run Loop Source Context的三個回調方法厌衙,其實是C語言函數 */
// 當把當前的Run Loop Source添加到Run Loop中時距淫,會回調這個方法。
void runLoopSourceScheduleRoutine (void *info, CFRunLoopRef runLoopRef, CFStringRef mode)
{
NSLog(@"Input source被添加%@",[NSThread currentThread]);
}
// 當前Input source被告知需要處理事件的回調方法
void runLoopSourcePerformRoutine (void *info)
{
NSLog(@"回調方法%@",[NSThread currentThread]);
}
// 如果使用CFRunLoopSourceInvalidate函數把輸入源從Run Loop里面移除的話,系統(tǒng)會回調該方法迅箩。
void runLoopSourceCancelRoutine (void *info, CFRunLoopRef runLoopRef, CFStringRef mode)
{
NSLog(@"Input source被移除%@",[NSThread currentThread]);
}
//創(chuàng)建兩個屬性來保存`runLoopSource `和`runLoop `
@implementation ViewController{
CFRunLoopSourceRef runLoopSource;
CFRunLoopRef runLoop;
}
//在之前的線程代碼中為RunLoop添加Source
self.thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"這是一條子線程%@",[NSThread currentThread]);
runLoop = CFRunLoopGetCurrent();
CFRunLoopSourceContext context = {0, (__bridge void *)(self), NULL, NULL, NULL, NULL, NULL,
&runLoopSourceScheduleRoutine,
&runLoopSourceCancelRoutine,
&runLoopSourcePerformRoutine};
//CFAllocatorRef內存分配器溉愁,默認NULL,CFIndex優(yōu)先索引饲趋,默認0拐揭,CFRunLoopSourceContext上下文
runLoopSource = CFRunLoopSourceCreate(NULL, 0, &context);
CFRunLoopAddSource(runLoop, runLoopSource, kCFRunLoopDefaultMode);
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
}];
[self.thread start];
然后我們需要在點擊的時候通知InputSource
,并且喚醒runLoop
:
//通知InputSource
CFRunLoopSourceSignal(InputSource);
//喚醒runLoop
CFRunLoopWakeUp(runLoop);
然后點擊測試一下:
因為我們設置了超時時間奕塑,所以10秒以后堂污,RunLoop就會退出。同時它的InputSource
被自動移除龄砰。
以上盟猖,我們簡單的自己創(chuàng)建并添加了RunLoop的InputSource
讨衣,實際開發(fā)中,我們可以對InputSource
進行封裝式镐,使用起來更方便反镇。這里就不做這一步了,網上可以找到比較完善的例子娘汞。
RunLoop還有一個觀察者歹茶,可以讓我們監(jiān)聽到RunLoop的各種狀態(tài),它也需要用CFRunLoop
來實現你弦,接下來惊豺,我們在上面的基礎上,對RunLoop添加觀察者進行監(jiān)聽:
// RunLoop監(jiān)聽回調
void currentRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
NSString *activityDescription;
switch (activity) {
case kCFRunLoopEntry:
activityDescription = @"kCFRunLoopEntry";
break;
case kCFRunLoopBeforeTimers:
activityDescription = @"kCFRunLoopBeforeTimers";
break;
case kCFRunLoopBeforeSources:
activityDescription = @"kCFRunLoopBeforeSources";
break;
case kCFRunLoopBeforeWaiting:
activityDescription = @"kCFRunLoopBeforeWaiting";
break;
case kCFRunLoopAfterWaiting:
activityDescription = @"kCFRunLoopAfterWaiting";
break;
case kCFRunLoopExit:
activityDescription = @"kCFRunLoopExit";
break;
default:
break;
}
NSLog(@"Run Loop activity: %@", activityDescription);
}
//為runLoop添加觀察者
CFRunLoopObserverContext runLoopObserverContext = {0, NULL, NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL,//內存分配器禽作,默認NULL
kCFRunLoopAllActivities,//監(jiān)聽所有狀態(tài)
YES,//是否循環(huán)
0,//優(yōu)先索引尸昧,一般為0
¤tRunLoopObserver,//回調方法
&runLoopObserverContext//上下文
);
if (observer)
{
CFRunLoopAddObserver(runLoop, observer, kCFRunLoopDefaultMode);
}
運行以后:
上面,我們對Runloop的使用做了簡單的分析旷偿,但是對我們好像還是沒什么卵用烹俗。接下來,我們通過一個實際的案例來運用RunLoop狸捅,讓它變成我們的法寶衷蜓。
-
Run Loop的實際應用
我們在實際開發(fā)中經常會遇到TableView中有大量的圖片顯示,在滑動過程中尘喝,能明顯得感覺到卡頓。我們這里用TableView顯示多張大圖簡單模擬一下:
self.tableView = [[UITableView alloc] initWithFrame:self.view.frame];
self.tableView.delegate = self;
self.tableView.dataSource = self;
self.tableView.tableFooterView = [[UIView alloc] initWithFrame:CGRectZero];
[self.view addSubview:self.tableView];
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 299;
}
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
return 100;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *identifier = @"identifier";
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:identifier];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:identifier];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
}
for (UIView *view in cell.subviews) {
[view removeFromSuperview];
}
UIImageView *imageView1 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
imageView1.frame = CGRectMake(10, 10, 100, 80);
[cell addSubview:imageView1];
UIImageView *imageView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
imageView2.frame = CGRectMake(120, 10, 100, 80);
[cell addSubview:imageView2];
UIImageView *imageView3 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
imageView3.frame = CGRectMake(230, 10, 100, 80);
[cell addSubview:imageView3];
return cell;
}
那么怎么做優(yōu)化呢斋陪?利用我們前面了解的RunLoop可以實現這個優(yōu)化朽褪。我們知道卡頓的主要原因是因為加載大量大圖是比較耗時的,而在主線程上處理耗時操作時无虚,我們滑動或者點擊屏幕就會有卡頓的感覺缔赠,因為在同一條線程上的任務只能串行執(zhí)行。而我們滑動屏幕時友题,一瞬間要顯示很多張圖片嗤堰,這就形成了一個耗時操作。
經過思考度宦,我們可以把這些圖片在每一次runLoop循環(huán)中添加一張踢匣,這樣的話,因為每次只添加一張圖片戈抄,時間大大縮短离唬,就不會有卡頓的感覺了。
我們這里利用runLoop的觀察者來監(jiān)聽每一次runloop循環(huán)划鸽,然后在監(jiān)聽事件里输莺,添加一張圖片戚哎。我們這里把添加圖片當做任務放到一個數組里面,任務就是一個block嫂用,這樣我們在回調里面只需要拿出任務執(zhí)行就OK了:
//定義一個任務
typedef void(^RunLoopTask)(void);
//用來存放任務的數組
@property (nonatomic, strong) NSMutableArray<RunLoopTask> *tasks;
//最大任務數量
@property (nonatomic, assign) NSInteger maxTaskCount;
//初始化數據
self.maxTaskCount = 24;
self.tasks = [NSMutableArray array];
//添加任務到數組
- (void)addTask:(RunLoopTask)task{
[self.tasks addObject:task];
//保證之前沒來得及顯示的圖片不會再繪制
if (self.tasks.count > _maxTaskCount) {
[self.tasks removeObjectAtIndex:0];
}
}
//添加任務
[self addTask:^{
UIImageView *imageView1 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
imageView1.frame = CGRectMake(10, 10, 100, 80);
[cell addSubview:imageView1];
}];
[self addTask:^{
UIImageView *imageView2 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
imageView2.frame = CGRectMake(120, 10, 100, 80);
[cell addSubview:imageView2];
}];
[self addTask:^{
UIImageView *imageView3 = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"timg2"]];
imageView3.frame = CGRectMake(230, 10, 100, 80);
[cell addSubview:imageView3];
}];
- (void)addObserverToMainRunLoop{
//為runLoop添加觀察者
CFRunLoopObserverContext runLoopObserverContext = {0, (__bridge void *)(self), NULL, NULL, NULL};
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(NULL,//內存分配器型凳,默認NULL
kCFRunLoopBeforeWaiting,//等待之前
YES,//是否循環(huán)
0,//優(yōu)先索引,一般為0
¤tRunLoopObserver,//回調方法
&runLoopObserverContext//上下文
);
if (observer)
{
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
}
CFRelease(observer);
}
// RunLoop監(jiān)聽回調
static void currentRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
MyViewController *vc = (__bridge MyViewController *)info;
if (vc.tasks.count == 0) {
return;
}
RunLoopTask task = vc.tasks.firstObject;
task();
[vc.tasks removeObjectAtIndex:0];
}
然后再運行嘱函,就很流暢了啰脚。當然代碼并不是完整代碼,篇幅有限实夹,只能把主要代碼貼上來橄浓。
總結一下,我們可以把耗時的大量UI操作利用RunLoop分解亮航,使界面保持流暢荸实。
本文參考iOS多線程編程指南(三)Run Loop。
本文所涉及到所有的代碼點擊前往缴淋。