iOS開發多執行緒-RunLoop
一、什麼是RunLoop
1.從字面意思看
1)執行迴圈
2)跑圈
2.基本作用
1)保持程式的持續執行
2)處理App中的各種事件(比如觸控事件、定時器事件、Selector事件)
3)節省CPU資源,提高程式效能:該做事時做事,該休息時休息
4)......
3.如果沒有RunLoop
說明:沒有RunLoop的情況下,程式執行到第3行後程序就結束了
4.如果有了RunLoop
說明:有RunLoop的情況下,由於main函式裡面啟動了個RunLoop,所以程式並不會馬上退出,保持持續執行狀態
5.main函式中的RunLoop
1)第14行程式碼UIApplicationMain函式內部就啟動了一個RunLoop,
2)所以UIApplicationMain函式一直沒有返回, 保持了程式的持續執行,
3)這個預設啟動的RunLopp是跟主執行緒相關聯的
二、RunLoop物件
1.iOS中有2tAPI來訪問和使用RunLoop
1)Foundation 框架
NSRunLoop
2)Core Foundation
CFRunLoopRef
2.NSRunLoop和CFRunLoopRef都代表著RunLoop物件
3.NSRunLoop是基於CFRunLoopRef
三、RunLoop資料
1.蘋果官方文件
2.CFRunLoopRef是開源的
四、RunLoop與執行緒
1.每條執行緒都有唯一的一個與之對應的RunLoop物件
2.主執行緒的RunLoop已經自動建立好了,子執行緒的RunLoop需要主動建立
3.RunLoop在第一次獲取時建立,線上程結束時銷燬
4.獲取RunLoop物件
1)Foundation
2)Core Foundation[NSRunLoop currentRunLoop]; // 獲得當前執行緒的RunLoop物件 [NSRunLoop mainRunLoop]; // 獲得主執行緒的RunLoop物件
CFRunLoopGetCurrent(); // 獲得當前執行緒的RunLoop物件
CFRunLoopGetMain(); // 獲得主執行緒的RunLoop物件
程式碼示例:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// NSRunLoop 主執行緒對應的RunLoop物件
NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];
NSLog(@"mainRunLoop = %@", mainRunLoop);
// NSRunLoop 獲得當前方法所線上程對應的RunLoop
NSRunLoop *currentRunLoop = [NSRunLoop currentRunLoop];
NSLog(@"currentRunLoop = %@", currentRunLoop);
// CFRunLoopRef 主執行緒對應的RunLoop物件
CFRunLoopRef cfMainRunLoop = CFRunLoopGetMain();
NSLog(@"cfMainRunLoop = %@", cfMainRunLoop);
// CFRunLoopRef 獲得當前方法所線上程對應的RunLoop
CFRunLoopRef cfCurrentRunLoop = CFRunLoopGetCurrent();
NSLog(@"cfCurrentRunLoop = %@", cfCurrentRunLoop);
// 開啟一條子執行緒
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
}
- (void)run
{
// 注意: 如果想給子執行緒新增RunLoop, 不能直接alloc init
// [[NSRunLoop alloc] init]; // 錯誤
// 只要呼叫currentRunLoop方法, 系統就會自動建立一個RunLoop, 新增到當前執行緒中
[NSRunLoop currentRunLoop]; // 這個方法是懶載入
}
@end
5.RunLoop相關類
CoreFoundation中關於RunLoop的5個類 1)CFRunLoopRef 2)CFRunLoopModeRef 3)CFRunLoopSourceRef 4)CFRunLoopTimerRef 5)CFRunLoopObserverRef6.CFRunLoopModeRef
6.1 CFRunLoopModeRef代表RunLoop的執行模式 1)一個 RunLoop包含若干個 Mode,每個Mode又包含若干個Source/Timer/Observer 2)每次RunLoop啟動時,只能指定其中一個 Mode,這個Mode被稱作 CurrentMode 3)如果需要切換Mode,只能退出Loop,再重新指定一個Mode進入,這樣做主要是為了分隔開不同組的Source/Timer/Observer,讓其互不影響6.2系統預設註冊了5個Mode:
1)kCFRunLoopDefaultMode:App的預設Mode,通常主執行緒是在這個Mode下執行 2)UITrackingRunLoopMode:介面跟蹤 Mode,用於 ScrollView追蹤觸控滑動,保證介面滑動時不受其他Mode 影響 3)UIInitializationRunLoopMode: 在剛啟動 App 時第進入的第一個 Mode,啟動完成後就不再使用 4)GSEventReceiveRunLoopMode: 接受系統事件的內部 Mode,通常用不到 5)kCFRunLoopCommonModes: 這是一個佔位用的Mode,不是一種真正的Mode程式碼示例:
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, strong) dispatch_source_t timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
}
// 演示函式呼叫棧:source0(使用者主動觸發)和source1
- (IBAction)btnClick:(id)sender {
NSLog(@"%s", __func__);
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// [self timer2];
// 開啟後臺執行緒執行timer2方法
// [self performSelectorInBackground:@selector(timer2) withObject:nil];
[self gcdTimer];
}
- (void)gcdTimer
{
NSLog(@"%s", __func__);
/*
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"------2.0秒後執行-----");
});
*/
// 建立一個定時器
/**
* 引數1: source的型別 DISPATCH_SOURCE_TYPE_TIMER:定時器
* 引數2: 執行緒等資訊
* 引數3: 對第二個引數的描述資訊
* 引數4: 傳遞一個佇列, 該佇列對應了將來的回撥方法在哪個執行緒中執行
*/
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
// 對timer這個區域性變數新增一個引用,以防止被釋放
self.timer = timer;
// 設定定時器開始的時間和間隔的時間, 以及精準度
// 開始時間
// dispatch_time_t startTime = DISPATCH_TIME_NOW;
dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 3.0 * NSEC_PER_SEC);
// 間隔時間
uint64_t interval = 1.0 * NSEC_PER_SEC;
/**
* 引數1: 需要給哪個定時器設定
* 引數2: 定時器開始的時間 / DISPATCH_TIME_NOW 立即執行
* 引數3: 定時器開始之後的間隔時間
* 引數4: 定時器間隔執行的精準度, 傳入0代表最精準(儘量的讓定時器精準), 傳入一個大於0的值, 代表多少秒的範圍是可以接受的
* 引數4存在的意義: 主要是為了提高程式的效能
* 注意: Dispatch的定時器接收的時間是納秒
*/
dispatch_source_set_timer(timer, startTime, interval, 0 * NSEC_PER_SEC);
// 指定定時器的回撥方法
dispatch_source_set_event_handler(timer, ^{
NSLog(@"test-----%@", [NSThread currentThread]);
});
// 開啟定時器(恢復)
dispatch_resume(timer);
}
- (void)timer1
{
NSLog(@"-----start-----");
// 建立一個定時器
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
// 將NSTime新增到RunLoop中,並告訴系統,當前Timer只有在RunLoop的預設模式下才有效
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// // 將NSTime新增到RunLoop中,並告訴系統,當前Timer只有在RunLoop的預設模式下才有效
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 將NSTimer新增到RunLoop中, 並且告訴系統, 在所有被"標記"common的模式都可以執行
/*
common modes = <CFBasicHash 0x7fc0b8615250 [0x104be7180]>{type = mutable set, count = 2,
entries =>
0 : <CFString 0x1058bae50 [0x104be7180]>{contents = "UITrackingRunLoopMode"}
2 : <CFString 0x104bc3080 [0x104be7180]>{contents = "kCFRunLoopDefaultMode"}
}
UITrackingRunLoopMode和kCFRunLoopDefaultMode都被標記為了common模式, 所以只需要將timer的模式設定為forMode:NSRunLoopCommonModes, 就可以在預設模式和追蹤模式都能夠執行
*/
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}
- (void)timer2
{
// 注意: 如果是通過scheduledTimerWithTimeInterval建立的NSTimer, 預設就會新增到RunLoop得DefaultMode中 , 所以它會自動執行
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
// 雖然預設已經新增到DefaultMode中, 但是我們也可以自己修改它的模式
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
// 開啟子執行緒對應的RunLoop
/**
* 注意:
* 1)如果是子執行緒,那麼需要手動建立子執行緒對應的RunLoop
* 2)子執行緒對應的RunLoop還需要手動開啟
*/
[[NSRunLoop currentRunLoop] run];
}
- (void)show
{
NSLog(@"%s-----%@", __func__, [NSThread currentThread]);
}
@end
7.CFRunLoopSourceRef
7.1 CFRunLoopSourceRef是事件源(輸入源) 7.2 以前的分法 1) Port-BasedSources 2) Custom InputSources 3) Cocoa PerformSelector Sources 7.3 現在的分法 1)Source0:非基於Port的,用於使用者主動觸發的事件 2)Source1:基於Port的,通過核心和其它執行緒相互發送訊息8.CFRunLoopTimerRef
1)CFRunLoopTimerRef是基於時間的觸發器 2)基本上說的就是NSTimer,它會受到runloop的mode的影響 3)GCD的定時器不受Runloop的mode的影響 9.CFRunLoopObserverRef
10. CFRunLoopObserverRef
10.1 新增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);
程式碼示例:
#import "ViewController.h"
@interface ViewController ()
@end
@implementation ViewController
// 當手指觸控控制器View的時候,呼叫該方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 建立Observer
/**
* 引數1: 指定如果給Observer分配儲存空間
* 引數2: 需要監聽的狀態類
* kCFRunLoopEntry = (1UL << 0), 即將啟動(進入)的時候
* kCFRunLoopBeforeTimers = (1UL << 1), 即將處理timer事件
* kCFRunLoopBeforeSources = (1UL << 2), 即將處理source事件
* kCFRunLoopBeforeWaiting = (1UL << 5), 即將進入睡眠
* kCFRunLoopAfterWaiting = (1UL << 6), RunLoop被喚醒
* kCFRunLoopExit = (1UL << 7), RunLoop退出
* kCFRunLoopAllActivities = 0x0FFFFFFFU 監聽所有狀態
*
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"即將進入RunLoop");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"即將處理timer");
break;
case kCFRunLoopBeforeSources:
NSLog(@"即將處理source");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"即將進入睡眠");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"RunLoop剛從睡眠中喚醒");
break;
case kCFRunLoopExit:
NSLog(@"RunLoop即將退出");
break;
default:
break;
}
});
// 給主執行緒的RunLoop新增一個觀察者,要監聽的是RunLoop的哪種執行模式
/**
* 引數1: 需要給哪個RunLoop新增觀察者
* 引數2: 需要新增的Observer物件
* 引數3: 在哪種模式下可以監聽 kCFRunLoopDefaultMode == NSDefaultRunLoopMode
*/
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);
// 釋放物件
CFRelease(observer);
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(show) userInfo:nil repeats:YES];
}
- (void)show{
NSLog(@"%s", __func__);
}
@end
列印檢視:五、CF的記憶體管理(CoreFoundation)
1. 凡是帶有Create、Copy、Retain等字眼的函式,創建出來的物件,都需要在最後做一次release 比如CFRunLoopObserverCreate release函式:CFRelease(物件);六、RunLoop處理邏輯
1.官方版本
2.網友整理版本
七、RunLoop應用
1.NSTimer2.ImageView顯示
3.PerformSelector
4.常駐執行緒
5.自動釋放池
程式碼示例:
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
/** 引用執行緒 */
@property (nonatomic, strong) NSThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//RunLoop的自動釋放池子
/*
自動釋放池什麼時候建立和釋放
1.第一次建立, 是在RunLoop進入的時候建立 對應的狀態 = kCFRunLoopEntry
2.最後一次釋放, 是在RunLoop退出的時候 對應的狀態 = kCFRunLoopExit
3.其它建立和釋放
* 每次睡覺的時候都會釋放前自動釋放池, 然後再建立一個新的
_wrapRunLoopWithAutoreleasePoolHandler activities = 0x1,
1 = kCFRunLoopEntry 進入RunLoop 建立自動釋放池
_wrapRunLoopWithAutoreleasePoolHandler activities = 0xa0,
160 = kCFRunLoopBeforeWaiting 即將進入睡眠 ,先釋放上一次建立的自動釋放池, 然後再建立一個新的釋放池
+
kCFRunLoopExit 即將退出RunLoop 釋放自動釋放池
*/
NSLog(@"%s", __func__);
// 新增到RunLoop的預設執行模式下inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]
// [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20151128_3"] afterDelay:3.0];
// 指定執行模式進行特定的操作
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20151128_3"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];
}
// 執行任務1的按鈕(建立執行緒執行任務)
- (IBAction)oneBtnClick {
// 建立執行緒並且執行任務1
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(operation1) object:nil];
[self.thread start];
// 注意:預設情況下一個執行緒只能使用一次, 也就是說只能執行一個操作, 執行完畢之後就不能使用了
}
// 執行任務2的按鈕(指定按鈕1建立的執行緒來執行)
- (IBAction)twoBtnClick {
// 指定子執行緒執行任務2
[self performSelector:@selector(operation2) onThread:self.thread withObject:nil waitUntilDone:YES];
}
// 任務1
- (void)operation1
{
NSLog(@"---start---");
// 保證子執行緒不被銷燬
// 採用死迴圈的方式是行不通的
// while (1) {
// NSLog(@"%s---%@", __func__, [NSThread currentThread]);
// }
// 子執行緒的NSRunLoop 需要手動建立
// 子執行緒的NSRunLoop 需要手動開啟
// 如果子執行緒的NSRunLoop沒有設定source or timer,那麼子執行緒的NSRunLoop會立即關閉
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// [runloop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(test) userInfo:nil repeats:YES];
[runLoop addTimer:timer forMode:NSRunLoopCommonModes];
// 啟動RunLoop
[runLoop run];
// 注意點: NSRunLoop只會檢查有沒有source和timer, 沒有就關閉, 不會檢查observer
// CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
//
// });
//
// CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
//
// // 釋放物件
// CFRelease(observer);
// 該程式碼永遠不會被執行
NSLog(@"---end---");
}
// 任務2
- (void)operation2
{
NSLog(@"%s-----%@", __func__, [NSThread currentThread]);
}
- (void)test
{
NSLog(@"%s-----%@", __func__, [NSThread currentThread]);
}
@end
執行效果請自行演示八、RunLoop面試題
1. 什麼是RunLoop? 1)從字面意思看:執行迴圈、跑圈 2)其實它內部就是do-while迴圈,在這個迴圈內部不斷地處理各種任務(比如Source、Timer、Observer) 3)一個執行緒對應一個RunLoop,主執行緒的RunLoop預設已經啟動,子執行緒的RunLoop得手動啟動(呼叫run方法) 4)RunLoop只能選擇一個Mode啟動,如果當前Mode中沒有任何Source(Sources0、Sources1)、Timer,那麼就直接退出RunLoop 2.自動釋放池什麼時候釋放? 通過Observer監聽RunLoop的狀態 3.在開發中如何使用RunLoop?什麼應用場景? 3.1 開啟一個常駐執行緒(讓一個子執行緒不進入消亡狀態,等待其他執行緒發來訊息,處理其他事件) 1)在子執行緒中開啟一個定時器 2)在子執行緒中進行一些長期監控 3.2 可以控制定時器在特定模式下執行 3.3可以讓某些事件(行為、任務)在特定模式下執行 3.4可以新增Observer監聽RunLoop的狀態,比如監聽點選事件的處理(在所有點選事件之前做一些事情)