iOS底層原理之runloop
RunLoop簡介
- 從字面意思講跑圈,執行迴圈。
RunLoop
就是一個物件,這個物件管理了其需要處理的事件和訊息,並提供了一個入口函式來執行上面Event Loop
的邏輯。執行緒執行了這個函式後,就會一直處於這個函式內部 “接受訊息->等待->處理” 的迴圈中
,直到這個迴圈結束,函式返回。
- 應用範疇
定時器(Timer)、PerformSelector;
GCD Async Main Queue;
事件響應、手勢識別、介面重新整理;
網路請求;
AutoreleasePool。 - 沒有RunLoop時模擬如下:
- 有RunLoop時模擬如下:( 程式並不會馬上退出,而是保持執行狀態)
- RunLoop的基本作用
保持程式的持續執行;處理App中的各種事件(比如觸控事件、定時器事件等);節省CPU資源,提高程式效能:該做事時做事,該休息時休息。
RunLoop物件
- iOS中有2套API來訪問和使用RunLoop,
Foundation:NSRunLoop
,Core Foundation:CFRunLoopRef
,NSRunLoop和CFRunLoopRef都代表著RunLoop物件,NSRunLoop是基於CFRunLoopRef的一層OC包裝,CFRunLoopRef是開源的,地址為:https://opensource.apple.com/tarballs/CF/
NSRunLoop官方文件 - 獲取RunLoop物件
Foundation
[NSRunLoop currentRunLoop]; // 獲得當前執行緒的RunLoop物件
[NSRunLoop mainRunLoop]; // 獲得主執行緒的RunLoop物件
Core Foundation
CFRunLoopGetCurrent(); // 獲得當前執行緒的RunLoop物件
CFRunLoopGetMain(); // 獲得主執行緒的RunLoop物件
RunLoop與執行緒
- 每條執行緒都有唯一的一個與之對應的RunLoop物件;
- RunLoop儲存在一個全域性的Dictionary裡,執行緒作為key,RunLoop作為value;
- 執行緒剛建立時並沒有RunLoop物件,RunLoop會在第一次獲取它時建立(懶載入),會線上程結束時銷燬;只能在一個執行緒的內部獲取其 RunLoop(主執行緒除外)。
- 主執行緒的RunLoop已經自動獲取(建立),子執行緒預設沒有開啟RunLoop。
RunLoop相關的類
- Core Foundation中關於RunLoop的5個類:
CFRunLoopRef
RunLoop的物件
CFRunLoopModeRef
RunLoop的執行模式
CFRunLoopSourceRef
輸入源/事件源
CFRunLoopTimerRef
Timer事件
CFRunLoopObserverRef
監聽者,監聽RunLoop的狀態改變 - RunLoop主要結構如下
- CFRunLoopModeRef
CFRunLoopModeRef
代表RunLoop
的執行模式,一個RunLoop
包含若干個Mode
,每個Mode
又包含若干個Source0/Source1/Timer/Observer
,RunLoop
啟動時只能選擇其中一個Mode
,作為currentMode
,如果需要切換Mode
,只能退出當前Loop
,再重新選擇一個Mode
進入,不同組的Source0/Source1/Timer/Observer
能分隔開來,互不影響,如果Mode
裡沒有任何Source0/Source1/Timer/Observer
,RunLoop
會立馬退出。
常見的2種Mode:
kCFRunLoopDefaultMode(NSDefaultRunLoopMode)
:App的預設Mode,通常主執行緒是在這個Mode下執行;
UITrackingRunLoopMode
:介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響。 - CFRunLoopObserverRef
用來RunLoop監聽活動(Run Loop Observer Activities),(<CoreFoundation/CFRunLoop.h>
)有以下幾種狀態,
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), //即將處理Timer
kCFRunLoopBeforeSources = (1UL << 2), //即將處理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), //即將推出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
新增Observer監聽RunLoop的所有狀態
//建立監聽者
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"kCFRunLoopBeforeWaiting");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"kCFRunLoopAfterWaiting");
break;
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit");
break;
default:
break;
}
});
// 新增Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 釋放
CFRelease(observer);
RunLoop的執行邏輯
RunLoop官網執行邏輯圖如下:
Source0
觸控事件處理
performSelector:onThread:
Source1
基於Port的執行緒間通訊
系統事件捕捉
Timers
NSTimer
performSelector:withObject:afterDelay:
Observers
用於監聽RunLoop的狀態
UI重新整理(BeforeWaiting)
Autorelease pool(BeforeWaiting)
- 具體執行邏輯流程
01、通知Observers:進入Loop
02、通知Observers:即將處理Timers
03、通知Observers:即將處理Sources
04、處理Blocks
05、處理Source0(可能會再次處理Blocks)
06、如果存在Source1,就跳轉到第8步
07、通知Observers:開始休眠(等待訊息喚醒)
08、通知Observers:結束休眠(被某個訊息喚醒)
01> 處理Timer
02> 處理GCD Async To Main Queue
03> 處理Source1
09、處理Blocks
10、根據前面的執行結果,決定如何操作
01> 回到第02步
02> 退出Loop
11、通知Observers:退出Loop
RunLoop休眠的實現原理
RunLoop休眠其實是從使用者態切換到核心態進入休眠狀態,然當有事件處理的時候又從核心態切換到使用者態去處理事件,這樣能夠大大的降低效能消耗。
RunLoop在實際開中的應用
- 控制執行緒生命週期(執行緒保活)
有時候我們需要在一個子執行緒中做一些事情,並且不想在做完事情後就立馬銷燬子執行緒,這時候就需要在子執行緒裡開啟一個runLoop來保證這個現在一直存在,在適當的時候銷燬。這樣做的好處就是避免了多次建立執行緒造成的效能損耗。
#import "ViewController.h"
#import "HJThread.h"
@interface ViewController ()
@property (strong, nonatomic) HJThread *thread;
@property (assign, nonatomic, getter=isStoped) BOOL stopped;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
//標記是否停止
self.stopped = NO;
//建立執行緒
self.thread = [[HJThread alloc] initWithBlock:^{
NSLog(@"%@----begin----", [NSThread currentThread]);
// 往RunLoop裡面新增Source\Timer\Observer
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//如果控制器還存在並且沒有執行停止(這樣的做的目的是放控制器已經銷燬造成的懷記憶體訪問)開啟執行迴圈
while (weakSelf && !weakSelf.isStoped) {
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
//只要迴圈一直存在就不會來到下面,
NSLog(@"%@----end----", [NSThread currentThread]);
}];
//開啟x執行緒執行任務
[self.thread start];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{ //防止換記憶體訪問
if (!self.thread) return;
[self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}
// 子執行緒需要執行的任務
- (void)test
{
NSLog(@"%s %@", __func__, [NSThread currentThread]);
}
- (IBAction)stop {
if (!self.thread) return;
// 在子執行緒呼叫stop(waitUntilDone設定為YES,代表子執行緒的程式碼執行完畢後,這個方法才會往下走),這裡設定為YES防止在控制器退出時,在子執行緒中self已經置nil,而迴圈一直在開啟這,造成記憶體洩漏
//vc->thread->runloop->vc
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}
// 用於停止子執行緒的RunLoop
- (void)stopThread
{
// 設定標記為YES
self.stopped = YES;
// 停止RunLoop
CFRunLoopStop(CFRunLoopGetCurrent());
NSLog(@"%s %@", __func__, [NSThread currentThread]);
// 清空執行緒
self.thread = nil;
}
- (void)dealloc
{
NSLog(@"%s", __func__);
[self stop];
}
@end
- 執行緒保活封裝
這樣用起來還是比較麻煩的,尤其會忽略記憶體洩漏和壞記憶體訪問問題,最好是封裝為一個工具類,為了避免影響系統的一些方法,並且要用到成員變數,這裡就封裝一個繼承NSObject
的子類,而不適用分類和繼承。
HJPermenantThread.h檔案,公共介面
#import <Foundation/Foundation.h>
typedef void (^HJPermenantThreadTask)(void);
@interface HJPermenantThread : NSObject
/**
開啟執行緒
*/
//- (void)run;
/**
在當前子執行緒執行一個任務
*/
- (void)executeTask:(HJPermenantThreadTask)task;
/**
結束執行緒
*/
- (void)stop;
@end
HJPermenantThread.m檔案,封裝功能的實現
#import "MJPermenantThread.h"
/** MJThread **/
@interface HJThread : NSThread
@end
@implementation HJThread
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
/** MJPermenantThread **/
@interface HJPermenantThread()
@property (strong, nonatomic) HJThread *innerThread;
@property (assign, nonatomic, getter=isStopped) BOOL stopped;
@end
@implementation HJPermenantThread
#pragma mark - public methods
- (instancetype)init
{
if (self = [super init]) {
self.stopped = NO;
__weak typeof(self) weakSelf = self;
self.innerThread = [[HJThread alloc] initWithBlock:^{
[[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//每次呼叫__executeTask時都會來這裡判斷迴圈是否還在
while (weakSelf && !weakSelf.isStopped) {
NSLog(@"迴圈: %s",__func__);
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
}];
[self.innerThread start];
}
return self;
}
//執行任務
- (void)executeTask:(HJPermenantThreadTask)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
{
NSLog(@"%s", __func__);
[self stop];
}
#pragma mark - private methods
- (void)__stop
{
//設定停止迴圈標記
self.stopped = YES;
//停止當前執行緒的執行迴圈
CFRunLoopStop(CFRunLoopGetCurrent());
//防止記憶體洩漏
self.innerThread = nil;
}
//執行block任務
- (void)__executeTask:(HJPermenantThreadTask)task
{
task();
}
@end
呼叫
@interface ViewController ()
@property (strong, nonatomic) MJPermenantThread *thread;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[MJPermenantThread alloc] init];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.thread executeTask:^{
NSLog(@"執行任務 - %@", [NSThread currentThread]);
}];
}
- (IBAction)stop {
[self.thread stop];
}
- (void)dealloc
{
NSLog(@"%s", __func__);
}
- 用
CFRunLoop
封裝
#import "HJPermenantThread.h"
/** HJThread **/
@interface HJThread : NSThread
@end
@implementation HJThread
- (void)dealloc
{
NSLog(@"%s", __func__);
}
@end
/** HJPermenantThread **/
@interface HJPermenantThread()
@property (strong, nonatomic) HJThread *innerThread;
@end
@implementation HJPermenantThread
#pragma mark - public methods
- (instancetype)init
{
if (self = [super init]) {
self.innerThread = [[HJThread alloc] initWithBlock:^{
NSLog(@"begin----");
// 建立上下文(要初始化一下結構體)
CFRunLoopSourceContext context = {0};
// 建立source
CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
// 往Runloop中新增source
CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
// 銷燬source
CFRelease(source);
// 啟動
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
NSLog(@"end----");
}];
[self.innerThread start];
}
return self;
}
- (void)executeTask:(HJPermenantThreadTask)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
{
NSLog(@"%s", __func__);
[self stop];
}
#pragma mark - private methods
- (void)__stop
{
CFRunLoopStop(CFRunLoopGetCurrent());
self.innerThread = nil;
}
- (void)__executeTask:(MJPermenantThreadTask)task
{
task();
}
@end
- 解決NSTimer在滑動時停止工作的問題
用時候需要在UITableview
中新增定時器倒計時功能,例如京東天貓的搶單功能,但是當tableView
滾動時,定時器就會停止工作,原因是定時器是工作在NSDefaultRunLoopMode
模式下,當滾動tableView
時,RunLoop
會切換到UITrackingRunLoopMode
模式下,NSTimer
就會暫停等待,等滾動完再切換到預設模式下定時器就又開始工作了;我們只需要要將定時器新增到NSRunLoopCommonModes
模式下就可以了。
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"%d", ++count);
}];
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];//預設模式
// [[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];//使用者介面追蹤模式
// NSDefaultRunLoopMode、UITrackingRunLoopMode才是真正存在的模式
// NSRunLoopCommonModes並不是一個真的模式,它只是一個標記
// timer能在_commonModes陣列中存放的模式下工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
注意
:NSRunLoopCommonModes
並不是一個真的模式,它只是一個標記。
- 監控應用卡頓
- 效能優化
面試題
-
講講 RunLoop,專案中有用到嗎?
RunLoop
就是一個物件,執行迴圈,這個物件管理了其需要處理的事件和訊息,並提供了一個入口函式來執行上面Event Loop
的邏輯。執行緒執行了這個函式後,就會一直處於這個函式內部 “接受訊息->等待->處理” 的迴圈中
,直到這個迴圈結束,函式返回。資料請求,定時器等會用到。 -
runloop內部實現邏輯?
-
runloop和執行緒的關係?
一一對應的關係,主執行緒預設開啟runloop,子執行緒在第一次獲取時開啟執行緒(預設不開啟)。 -
timer 與 runloop 的關係?
timer預設是runloop預設模式下的一種事件。 -
程式中新增每3秒響應一次的NSTimer,當拖動tableview時timer可能無法響應要怎麼解決?程式中新增每3秒響應一次的NSTimer,當拖動tableview時timer可能無法響應要怎麼解決?
設定RunLoop的模式為NSRunLoopCommonModes
。 -
runloop 是怎麼響應使用者操作的, 具體流程是什麼樣的?
如果是使用者介面沒有操作,runloop會在預設模式下的核心態休眠,當要相應響應使用者操作,會切退出預設模式,進入使用者介面追蹤模式,並且切換到使用者態去響應使用者的操作,當響應完成後,就退出使用者介面追蹤模式,進入預設模式,並且切換到核心態進行休眠。 -
說說runLoop的幾種狀態說說runLoop的幾種狀態
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即將進入Loop
kCFRunLoopBeforeTimers = (1UL << 1), //即將處理Timer
kCFRunLoopBeforeSources = (1UL << 2), //即將處理Source
kCFRunLoopBeforeWaiting = (1UL << 5), //即將進入休眠
kCFRunLoopAfterWaiting = (1UL << 6), //剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), //即將推出Loop
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
- runloop的mode作用是什麼?
切換不同的模式來處理不同的事件。