1. 程式人生 > >iOS開發多執行緒-RunLoop

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.NSRunLoopCFRunLoopRef都代表著RunLoop物件

  3.NSRunLoop是基於CFRunLoopRef

的一層OC包裝, 所以要了解RunLoop內部結構,需要多研究CFRunLoopRef層面的API (Core Foundation 層面)

三、RunLoop資料

  1.蘋果官方文件

  2.CFRunLoopRef是開源的

四、RunLoop與執行緒

  1.每條執行緒都有唯一的一個與之對應的RunLoop物件

  2.主執行緒的RunLoop已經自動建立好了,子執行緒的RunLoop需要主動建立

  3.RunLoop在第一次獲取時建立,線上程結束時銷燬

  4.獲取RunLoop物件

  1)Foundation

[NSRunLoop currentRunLoop]; // 獲得當前執行緒的RunLoop物件
[NSRunLoop mainRunLoop]; // 獲得主執行緒的RunLoop物件
  2)Core Foundation
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)CFRunLoopObserverRef

  6.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)kCFRunLoopDefaultModeApp的預設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

    CFRunLoopObserverRef是觀察者,能夠監聽RunLoop的狀態改變     可以監聽的時間點有以下幾個

  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.NSTimer

  2.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的狀態,比如監聽點選事件的處理(在所有點選事件之前做一些事情)