1. 程式人生 > 實用技巧 >iOS-RunLoop,為手機省電,節省CPU資源,程式離不開的機制

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的使用場合:

  1. 使用port或是自定義的input source來和其他執行緒進行通訊
  2. 線上程(非主執行緒)中使用timer
  3. 使用 performSelector…系列(如performSelectorOnThread, …)
  4. 使用執行緒執行週期性工作
  • 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的狀態,比如監聽點選事件的處理(在所有點選事件之前做一些事情)