1. 程式人生 > >iOS底層原理之runloop

iOS底層原理之runloop

RunLoop簡介

  • 從字面意思講跑圈,執行迴圈。RunLoop就是一個物件,這個物件管理了其需要處理的事件和訊息,並提供了一個入口函式來執行上面 Event Loop 的邏輯。執行緒執行了這個函式後,就會一直處於這個函式內部 “接受訊息->等待->處理” 的迴圈中,直到這個迴圈結束,函式返回。
     RunLoop
  • 應用範疇
    定時器(Timer)、PerformSelector;
    GCD Async Main Queue;
    事件響應、手勢識別、介面重新整理;
    網路請求;
    AutoreleasePool。
  • 沒有RunLoop時模擬如下:
    沒有RunLoop
  • 有RunLoop時模擬如下:( 程式並不會馬上退出,而是保持執行狀態)
    有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主要結構如下
    RunLoop主要結構
  1. CFRunLoopModeRef
    CFRunLoopModeRef
    RunLoop內部結構
    CFRunLoopModeRef代表RunLoop的執行模式,一個RunLoop包含若干個Mode,每個Mode又包含若干個Source0/Source1/Timer/ObserverRunLoop啟動時只能選擇其中一個Mode,作為currentMode,如果需要切換Mode,只能退出當前Loop,再重新選擇一個Mode進入,不同組的Source0/Source1/Timer/Observer能分隔開來,互不影響,如果Mode裡沒有任何Source0/Source1/Timer/ObserverRunLoop會立馬退出。
    常見的2種Mode:
    kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的預設Mode,通常主執行緒是在這個Mode下執行;
    UITrackingRunLoopMode:介面跟蹤 Mode,用於 ScrollView 追蹤觸控滑動,保證介面滑動時不受其他 Mode 影響。
  2. 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官網執行邏輯圖如下:
 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休眠其實是從使用者態切換到核心態進入休眠狀態,然當有事件處理的時候又從核心態切換到使用者態去處理事件,這樣能夠大大的降低效能消耗。

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並不是一個真的模式,它只是一個標記。

  • 監控應用卡頓
  • 效能優化

面試題

  1. 講講 RunLoop,專案中有用到嗎?
    RunLoop就是一個物件,執行迴圈,這個物件管理了其需要處理的事件和訊息,並提供了一個入口函式來執行上面 Event Loop 的邏輯。執行緒執行了這個函式後,就會一直處於這個函式內部 “接受訊息->等待->處理” 的迴圈中,直到這個迴圈結束,函式返回。資料請求,定時器等會用到。

  2. runloop內部實現邏輯?

  3. runloop和執行緒的關係?
    一一對應的關係,主執行緒預設開啟runloop,子執行緒在第一次獲取時開啟執行緒(預設不開啟)。

  4. timer 與 runloop 的關係?
    timer預設是runloop預設模式下的一種事件。

  5. 程式中新增每3秒響應一次的NSTimer,當拖動tableview時timer可能無法響應要怎麼解決?程式中新增每3秒響應一次的NSTimer,當拖動tableview時timer可能無法響應要怎麼解決?
    設定RunLoop的模式為NSRunLoopCommonModes

  6. runloop 是怎麼響應使用者操作的, 具體流程是什麼樣的?
    如果是使用者介面沒有操作,runloop會在預設模式下的核心態休眠,當要相應響應使用者操作,會切退出預設模式,進入使用者介面追蹤模式,並且切換到使用者態去響應使用者的操作,當響應完成後,就退出使用者介面追蹤模式,進入預設模式,並且切換到核心態進行休眠。

  7. 說說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
};
  1. runloop的mode作用是什麼?
    切換不同的模式來處理不同的事件。