iOS_RunLoop介紹+使用示例

一丶RunLoop 介紹

官方文檔:
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
看任何文檔,都不如看官方文檔

簡介:
Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.

大概意思:
Run loops 是與線程相關(guān)的基礎(chǔ)架構(gòu)的一部分,是一個(gè)事件處理的循環(huán),用于工作調(diào)度并處理接收事件,Run loop 的可以使線程有工作需要做時(shí)可以忙碌起來,而當(dāng)沒有事可做時(shí),又可以使得線程睡眠。

其他:
1.Run Loop 管理不是完全自動(dòng)的;,設(shè)計(jì)線程的時(shí)候,適當(dāng)?shù)臅r(shí)候啟動(dòng)和響應(yīng)傳入事件;
2.每個(gè)線程都有一個(gè)關(guān)聯(lián)的Run Loop 除了主線程默認(rèn)開啟,分線程需要手動(dòng)運(yùn)行;采用懶加載的方式進(jìn)行加載;

1.1 Run Loop 結(jié)構(gòu):

Run Loop 接收2種不同類型的sources事件

1.1.1.輸入源:

異步傳遞事件,通常是來自不同的線程或不同的應(yīng)用的消息。輸入源異步傳遞事件到對應(yīng)的處理程序和在線程關(guān)聯(lián)的NSRunLoop對象調(diào)起runUntilDate:方法來退出事件處理。

1.1.2.timer源:

同步地傳遞事件,發(fā)生在每個(gè)定時(shí)器調(diào)用或周期性地調(diào)用。Timer源傳遞事件到他們的處理程序,但是不會調(diào)用run loop來退出處理。

運(yùn)行循環(huán)及其源的結(jié)構(gòu)圖


Paste_Image.png

1.2 Run Loop Modes

Run Loop模式是一個(gè)監(jiān)視輸入源和定時(shí)器的集合和注冊成為run loop的觀察者的集合。每次要運(yùn)行run loop,都需要顯示或隱式地指定某種運(yùn)行的mode。只有與這種指定的mode關(guān)聯(lián)的源才會被監(jiān)視和允許傳遞他們的事件,同樣地,只有與這種模式關(guān)聯(lián)的觀察者都會收到run loop行為變化的通知。與其它模式想關(guān)聯(lián)的源,直到隨后在合適的模式通過循環(huán)后,都會接收到新的事件

我們通過名稱來唯一標(biāo)識mode。在Cocoa和Core Foundation中都定義了default模式和幾個(gè)常用的模式,都是通過字符串名稱來指定。我們也可以自定義模式,但是我們需要手動(dòng)添加至少一個(gè)input source/timers/observers。

通過使用mode來過濾掉我們不希望接收到來自不想要的通過run loop的源。大部分情況下,我們都是使用系統(tǒng)定義的default模式。對于子線程,我們可以使用自定義模式在關(guān)鍵性操作時(shí)阻止低優(yōu)先級的源傳遞事件。

Paste_Image.png

二丶什么時(shí)候使用Run Loop

2.1什么時(shí)候應(yīng)該使用run loop呢?

只有當(dāng)我們需要?jiǎng)?chuàng)建子線程的時(shí)候,才會需要到顯示地運(yùn)行run loop。應(yīng)用程序的主線程的run loop是應(yīng)用啟動(dòng)的基礎(chǔ)任務(wù),在啟動(dòng)時(shí)就會自動(dòng)啟動(dòng)run loop。所以我們不需要手動(dòng)啟動(dòng)主線程的run loop。

對于子線程,我們需要確定線程是否需要run loop,如果需要,則配置它并啟動(dòng)它。我們并不問題需要啟動(dòng)run loop的。比如說,如果我們開一個(gè)子線程去執(zhí)行一些長時(shí)間的和預(yù)先決定的任務(wù),我們可能不需要啟動(dòng)run loop。Run loop是用于那么需要在線程中有更多地交互的場景。比如說,我們會在下面的任何一種場景中需要開啟run loop:

1.使用端口源或者自定義輸入源與其它線程通信
2.在線程中使用定時(shí)器
3.使用Cocoa中的任何performSelector…方法
4.保持線程來執(zhí)行周期性的任務(wù)

2.2 使用Run Loop 對象

2.2.1

Run Loop對象給添加輸入源、定時(shí)器和觀察者到run loop提供了主接口。每個(gè)線程都有一個(gè)單獨(dú)的run loop與之關(guān)聯(lián)(對于子線程,若沒有調(diào)用過任何獲取run loop的方法是不會有run loop的,只有調(diào)用過,才會創(chuàng)建或者直接使用)。

采用懶加載的方式獲取方式:

2.2.2通過以下兩種方式來獲取run loop對象:
1.在Cocoa中,使用[NSRunLoop currentRunLoop]獲取
2.使用CFRunLoopGetCurrent()函數(shù)獲取
2.2.3配置RunLoop

在子線程運(yùn)行run loop之前,你必須至少添加一種輸入源或者定時(shí)器。如果run loop沒有任何的源需要監(jiān)視,它就會立刻退出。

除了添加sources之外,你還可以添加觀察者來檢測runloop不同的執(zhí)行狀態(tài)。要添加觀察者,可以使用CFRunLoopObserverRef指針類型和使用CFRunLoopAddObserver函數(shù)來添加到run loop中。我們只能通過Core Foundation來創(chuàng)建run loop觀察者,即使是Cocoa應(yīng)用。

下面這段代碼展示主線程如何添加觀察者到run loop以及如何創(chuàng)建run loop觀察者:

- (void)threadMain {
    // The application uses garbage collection, so no autorelease pool is needed.
    NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];
    // Create a run loop observer and attach it to the run loop.
    CFRunLoopObserverContext  context = {0, self, NULL, NULL, NULL};
    CFRunLoopObserverRef    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
            kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
    if (observer) {
        CFRunLoopRef    cfLoop = [myRunLoop getCFRunLoop];
        CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
    }
    // Create and schedule the timer.
    [NSTimer scheduledTimerWithTimeInterval:0.1 target:self
                selector:@selector(doFireTimer:) userInfo:nil repeats:YES];
    NSInteger    loopCount = 10;
    do {
        // Run the run loop 10 times to let the timer fire.
        [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
        loopCount--;
    } while (loopCount);
}
2.2.4 啟動(dòng)Run Loop

只有子線程才有可能需要啟動(dòng)run loop。Run loop必須至少有一種輸入源或者timer源來監(jiān)視。如果沒有任何源,則run loop會退出。

下面的幾種方式可以啟動(dòng)run loop:

  • 無條件地:無條件進(jìn)入run loop是最簡單的方式,但也是最不希望這么做的,因?yàn)檫@樣會導(dǎo)致run loop會進(jìn)入永久地循環(huán)。可以添加、刪除輸入源和timer源,但是只能通過kill掉run loop才能停止。而且還不能使用自定義mode。
  • 限時(shí):與無條件運(yùn)行run loop不同,最好是給run loop添加一個(gè)超時(shí)時(shí)間。
  • 在特定的mode:除了添加超時(shí)時(shí)間,還可以指定mode。
    下面是運(yùn)行run loop的一段代碼:
- (void)skeletonThreadMain {
    // Set up an autorelease pool here if not using garbage collection.
    BOOL done = NO;
    // Add your sources or timers to the run loop and do any other setup.
    do {
        // Start the run loop but return after each source is handled.
        SInt32    result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);
        // If a source explicitly stopped the run loop, or if there are no
        // sources or timers, go ahead and exit.
        if ((result == kCFRunLoopRunStopped) || (result == kCFRunLoopRunFinished))
            done = YES;
        // Check for any other exit conditions here and set the
        // done variable as needed.
    } while (!done);
    // Clean up code here. Be sure to release any allocated autorelease pools.
}

2.2.5 退出Run Loop

有兩種方法使run loop在處理事件之前,退出run loop:

1.給run loop設(shè)定超時(shí)時(shí)間
2.告訴run loop要stop

設(shè)定超時(shí)時(shí)間是比較推薦的。我們可以通過CFRunLoopStop函數(shù)來停止run loop。

2.3 線程安全

CFRunLoopRef 是在 CoreFoundation 框架內(nèi)的,它提供了純 C 函數(shù)的 API,所有這些 API 都是線程安全的。

NSRunLoop 是基于 CFRunLoopRef 的封裝,提供了面向?qū)ο蟮?API,但是這些 API 不是線程安全的。

三丶使用示例:

3.1 檢測卡頓監(jiān)控

通過CFRunLoop源代碼
https://opensource.apple.com/source/CF/CF-1151.16/CFRunLoop.c

NSRunLoop調(diào)用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之間,還有kCFRunLoopAfterWaiting之后,也就是如果我們發(fā)現(xiàn)這兩個(gè)時(shí)間內(nèi)耗時(shí)太長,那么就可以判定出此時(shí)主線程卡頓.

技術(shù)要點(diǎn):
1.利用CFRunLoopObserverRef 實(shí)時(shí)獲得這些狀態(tài)值的變化
2.知道RunLoop的Activity
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出Loop
};

  1. dispatch_semaphore_t 利用保證計(jì)算精確

參考:https://github.com/suifengqjn/PerformanceMonitor

#import "PerformanceMonitor.h"
#import <CrashReporter/CrashReporter.h>

@interface PerformanceMonitor ()
{
    int timeoutCount;
    
    //實(shí)時(shí)獲得這些狀態(tài)值的變化
    CFRunLoopObserverRef observer;
    
    @public
    dispatch_semaphore_t semaphore;
    CFRunLoopActivity activity;
}
@end

@implementation PerformanceMonitor

+ (instancetype)sharedInstance
{
    static id instance = nil;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        instance = [[self alloc] init];
    });
    return instance;
}

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    PerformanceMonitor *moniotr = (__bridge PerformanceMonitor*)info;
    
    moniotr->activity = activity;
    
    dispatch_semaphore_t semaphore = moniotr->semaphore;
    dispatch_semaphore_signal(semaphore);
}

- (void)stop
{
    if (!observer)return;
    CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    CFRelease(observer);
    observer = NULL;
}

- (void)start
{
    if (observer)return;
    
    // 信號
    semaphore = dispatch_semaphore_create(0);
    // 注冊RunLoop狀態(tài)觀察
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                       kCFRunLoopAllActivities,
                                       YES,
                                       0,
                                       &runLoopObserverCallBack,
                                       &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    
    // 在子線程監(jiān)控時(shí)長
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
            if (st != 0)
            {
                if (!observer)
                {
                    timeoutCount = 0;
                    semaphore = 0;
                    activity = 0;
                    return;
                }
                
                if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
                {
                     NSLog(@"---卡頓計(jì)時(shí)器:%ld",(long)timeoutCount);
                    if (++timeoutCount < 5)
                        continue;
                       NSLog(@"...我被卡住了");
                }
            }
            timeoutCount = 0;
        }
    });
}

@end

Paste_Image.png
3.2 讓線程常在

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
    [[NSThread currentThread] setName:@"AFNetworking"];
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    //添加MachPort源,保證線程不銷毀
    [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
    [runLoop run];
}
}
+ (NSThread *)networkRequestThread {
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
    _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
    [_networkRequestThread start];
});
 
return _networkRequestThread; 
3.3 NStimer使用
一丶主線程情況:
 
NSTimer *timer =[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(run) userInfo:nil repeats:YES];
 
//滑動(dòng)Scroll,或者tabview等,由于主線程只能執(zhí)行一個(gè)程序,正在運(yùn)行的timer會被停止,所以,加入以下字段,能提升計(jì)時(shí)器的優(yōu)先級別
 
[[NSRunLoop currentRunLoop] addTimer:_myTimer forMode:UITrackingRunLoopMode];
 
二丶分線程情況:
 
//創(chuàng)建分線程
dispatch_queue_t globalQ = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(globalQ, ^{
 
    NSTimer *timer =[NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(run) userInfo:nil repeats:YES];
    //通過[NSRunLoop currentRunLoop]創(chuàng)建RunLoop
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    //分線程必須運(yùn)行
    [[NSRunLoop currentRunLoop] run];
});
 
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容