iOS-RunLoop,為手機省電,節省CPU資源,程式離不開的機制
RunLoop是什麼?基本操作是什麼?
1、RunLoop的作用
RunLoop可以:
-
保持程式的持續執行
-
處理App中的各種事件(比如觸控事件、定時器事件、Selector事件)
-
節省CPU資源,提高程式效能:該做事時做事,該休息時休息
學到這裡,你就知道了RUnLoop的作用了吧。看看程式裡的例子:
程式中的main函式裡面:
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
在UIApplicationMain
裡面就開啟了一個RunLoop,這個預設啟動的RunLoop是跟主執行緒相關聯的。它就可以處理我們上面說的那些事情,說白了就是讓CUP有時間休息,沒事的時候幫我們省電。
下面我們看看怎麼訪問它:
2、iOS中有2套API來訪問和使用RunLoop
1.FoundationNSRunLoop
2.Core FoundationCFRunLoopRef
2.1、兩者的關係:
NSRunLoop和CFRunLoopRef都代表著RunLoop物件
NSRunLoop是基於CFRunLoopRef的一層OC包裝,所以要了解RunLoop內部結構,需要多研究CFRunLoopRef層面的API(Core Foundation層面)
2.2、如何獲得RunLoop物件
Foundation
[NSRunLoop currentRunLoop]; // 獲得當前執行緒的RunLoop物件
[NSRunLoop mainRunLoop]; // 獲得主執行緒的RunLoop物件
Core Foundation
CFRunLoopGetCurrent(); // 獲得當前執行緒的RunLoop物件
CFRunLoopGetMain(); // 獲得主執行緒的RunLoop物件
3、RunLoop和執行緒的關係
每條執行緒都有唯一的一個與之對應的RunLoop物件
主執行緒的RunLoop已經自動建立好了,子執行緒的RunLoop需要主動建立
RunLoop在第一次獲取時建立,線上程結束時銷燬
作為一個開發者,有一個學習的氛圍跟一個交流圈子特別重要,這有個iOS交流群:642363427,不管你是小白還是大牛歡迎入駐 ,分享BAT,阿里面試題、面試經驗,討論技術!
4、RunLoop的結構
如圖所示:
image一個RunLoop包含若干個Mode,
而每個Mode又包含若干個Source、Timer、Observer
對應的類是:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
每個RunLoop啟動時,只能指定一種Model,並且切換Mode時,只能先退出RunLoop,這樣是為了分隔開不同組的Source、Timer、Observer。
RunLoop有5種Mode:
系統預設註冊了5個Mode:
NSDefaultRunLoopMode:App的預設Mode,通常主執行緒是在這個Mode下執行,可以把這個理解為一個”過濾器“,我們可以只對自己關心的事件進行監視。
UITrackingRunLoopMode:介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響
UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用
GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到
NSRunLoopCommonModes: 這是一個佔位用的Mode,不是一種真正的Mode
5、RunLoop的內部類
每個Mode又包含若干個Source、Timer、Observer,他們對應的類如下:
5.1、CFRunLoopTimerRef
-
CFRunLoopTimerRef是基於時間的觸發器
-
CFRunLoopTimerRef基本上說的就是NSTimer,它受RunLoop的Mode影響
-
GCD的定時器不受RunLoop的Mode影響
5.2、CFRunLoopSourceRef
-
CFRunLoopSourceRef是事件源(輸入源)
-
按照官方文件,Source的分類
- Port-Based Sources
- Custom Input Sources
- Cocoa Perform Selector Sources
-
按照函式呼叫棧,Source的分類
- Source0:非基於Port的, 用於使用者主動觸發事件
- Source1:基於Port的,通過核心和其他執行緒相互發送訊息
5.3、CFRunLoopObserverRef
CFRunLoopObserverRef是觀察者,能夠監聽RunLoop的狀態改變
可以監聽的時間點有以下幾個
image- 新增Observer
// 建立observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----監聽到RunLoop狀態發生改變---%zd", activity);
});
// 新增觀察者:監聽RunLoop的狀態
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 釋放Observer
CFRelease(observer);
RunLoop的使用
下來是Run Loop的使用場合:
- 使用port或是自定義的input source來和其他執行緒進行通訊
- 線上程(非主執行緒)中使用timer
- 使用 performSelector…系列(如performSelectorOnThread, …)
- 使用執行緒執行週期性工作
-
run loop不需要建立,線上程中只需要呼叫[NSRunLoop currentRunLoop]就可以得到
-
假設我們想要等待某個非同步方法的回撥。比如connection。如果我們的執行緒中沒有啟動run loop,是不會有效果的(因為執行緒已經執行完畢,正常退出了)。
-
你不需要在任何情況下都去啟動一個執行緒的 run loop。比 如,你使用執行緒來處理一個預先定義的長時間執行的任務時,你應該避免啟動 run loop。Run loop 在你要和執行緒有更多的互動時才需要,比如以下情況:
使用埠或自定義輸入源來和其他執行緒通訊
使用執行緒的定時器
Cocoa 中使用任何 performSelector...的方法
使執行緒週期性工作
如果你決定在程式中使用 run loop,那麼它的配置和啟動都很簡單。和所有執行緒 程式設計一樣,你需要計劃好在輔助執行緒退出執行緒的情形。讓執行緒自然退出往往比強制關閉它更好。關於更多介紹如何配置和退出一個 run loop,參閱”使用 Run Loop 物件” 的介紹。
終於學好了關於RunLoop的基本概念,
我們知道了,RunLoop接收到兩種事件就會去呼叫相應的方法處理事件,兩種事件分別是輸入源(input source)和定時源 (timer source),換句話說,RunLoop就是所有要監視的輸入源和定時源以及要通知的 run loop 註冊觀察 者的集合。
所以,我們要知道
Run loop 入口
Run loop 何時處理一個定時器
Run loop 何時處理一個輸入源
Run loop 何時進入睡眠狀態
Run loop 何時被喚醒,但在喚醒之前要處理的事件
Run loop 終止
例子
給子執行緒新增RunLoop
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(show) object:nil];
[thread start];
- (void)show
{
[NSRunLoop currentRunLoop]; // 只要呼叫currentRunLoop方法, 系統就會自動建立一個RunLoop, 新增到當前執行緒中
}
常駐執行緒
有這麼一個需求,我們要在子執行緒中沒接收一個事件就呼叫一次方法。但是子執行緒在完成任務後就銷燬,全域性變數強引用?試試
//
// ViewController.m
// NSThreadTest
//
// Created by 薛銀亮 on 14/8/10.
// Copyright (c) 2014年 薛銀亮. All rights reserved.
//
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong)NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:@"xyl"];
self.thread = thread;
[thread start];
}
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
[self performSelector:@selector(test) onThread:self.thread withObject:@"xyl" waitUntilDone:YES];
}
-(void)run{
NSLog(@"runrunrunrun");
}
-(void)test{
NSLog(@"testtesttest");
}
@end
結果令人感到遺憾:執行緒只能執行一個函式run,然後就死亡了。就算用全域性的變數引用著,這個執行緒也只是存在於記憶體中,同樣是死亡狀態,不能持續的執行。
-
想在子執行緒中不斷執行任務,必須保證子線不處於死亡狀態
-
但是子執行緒執行完一次任務就進入死亡狀態
-
那我們可以把執行緒停留在進入死亡狀態之前,這裡可以用RunLoop
- 我們可以線上程初始化的時候執行的方法中給他建立一個執行時RunLoop,這是他就可以不斷接收source,也就是這樣
-(void)run{
NSLog(@"runrunrunrun");
[[NSRunLoop currentRunLoop]addPort:[[NSPort alloc]init] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop]run];
}
- 注意RunLoop:
啟動前內部必須要有至少一個item,雖然Obsever也是item的一種,但是隻會等待Timer和Source ,Timer是因為有回撥,Source是會接收事件,所以當RunLoop裡面有Timer或者Source的時候,RunLoop會等待裡面的item(除Obsever以外)主動給他發訊息,然後Oberver被動的接收RunLoop傳送過來的訊息,亦即是說,能主動給RunLoop發訊息的item會讓RunLoop跑起來並且不退出。
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
//1.將NSTimer新增在Default模式, 定時器只會執行在Default Mode下, 當拖拽時Mode切換為Tracking模式所以沒反應
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 2.將NSTimer新增在Tracking模式, , 定時器只會執行在Tracking Mode下,當停止時Mode切換為Default模式所以沒反應
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 3.將NSTimer新增為被標記為Common的模式, Default和Tracking都被標記為了Common, 所以都有反應
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
// 4.scheduled建立的定時器預設新增在Default模式, 所以不用手動新增, 但是後期也可以修改
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
// 修改模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
注意:GCD的定時器不受RunLoop的影響,因為RunLoop底層是使用GCD實現timer的
-
GCD定時器
有這麼一個需求,需要這麼一個定時器,誤差幾乎為0的定時器,但是無論是NSTimer還是CGDisplayLink都會有誤差,而且誤差都比較大,這是我們可以用GCD來實現定時器,實際上,上面已經說了,RunLoop底層也是呼叫GCD的source來實現NSTimer的,只是NSTimer還受mode的影響,下面來看看怎麼用GCD實現
// 獲取佇列
dispatch_queue_t queue = dispatch_get_main_queue();
// 建立定時器
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 設定定時器屬性(什麼時候開始,間隔多大)
// 定義開始時間
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC));
// 定義時間間隔
uint64_t interver = (uint64_t)(1.0 * NSEC_PER_SEC);
// 設定開始時間和時間間隔
dispatch_source_set_timer(self.timer, start,interver, 0);
// 設定回撥
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"==================") ;
});
// dispatch_cancel(self.timer);
// self.timer = nil;
// 取消定時器
// 啟動定時器
dispatch_resume(self.timer);
執行緒除了處理輸入源,Run Loops也會生成關於Run Loop行為的通知(notification)。Run Loop觀察者(Run-Loop Observers)可以收到這些通知,並在執行緒上面使用他們來作額為的處理,我們可以像下面這樣新增一個觀察者給RunLoop
新增RunLoop監聽
// 建立Observer
// 第一個引數:用於分配該observer物件的記憶體
// 第二個引數:用以設定該observer所要關注的的事件
// 第三個引數:用於標識該observer是在第一次進入run loop時執行, 還是每次進入run loop處理時均執行
// 第四個引數:用於設定該observer的優先順序
// 第五個引數: observer監聽到事件時的回撥block
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch(activity)
{
case kCFRunLoopEntry:
NSLog(@"即將進入loop");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"即將處理timers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"即將處理sources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"即將進入休眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"剛從休眠中喚醒");
break;
case kCFRunLoopExit:
NSLog(@"即將退出loop");
break;
default:
break;
}
});
將上面的監聽新增到觀察者
/*
第一個引數: 給哪個RunLoop新增監聽
第二個引數: 需要新增的Observer物件
第三個引數: 在哪種模式下監聽
*/
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);
// 釋放observer
CFRelease(observer);
RunLoop面試題
-
什麼是RunLoop?
-
從字面意思看:執行迴圈、跑圈其實它內部就是do-while迴圈,在這個迴圈內部不斷地處理各種任務(比如Source、Timer、Observer)
-
一個執行緒對應一個RunLoop,主執行緒的RunLoop預設已經啟動,子執行緒的RunLoop得手動啟動(呼叫run方法)
-
RunLoop只能選擇一個Mode啟動,如果當前Mode中沒有任何Source(Sources0、Sources1)、Timer,那麼就直接退出RunLoop
-
自動釋放池什麼時候釋放?
-
通過Observer監聽RunLoop的狀態
-
-
在開發中如何使用RunLoop?什麼應用場景?
-
開啟一個常駐執行緒(讓一個子執行緒不進入消亡狀態,等待其他執行緒發來訊息,處理其他事件)
-
在子執行緒中開啟一個定時器
-
在子執行緒中進行一些長期監控
-
可以控制定時器在特定模式下執行
-
可以讓某些事件(行為、任務)在特定模式下執行
-
可以新增Observer監聽RunLoop的狀態,比如監聽點選事件的處理(在所有點選事件之前做一些事情)
-