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 是与线程相关的基础架构的一部分,是一个事件处理的循环,用于工作调度并处理接收事件,Run loop 的可以使线程有工作需要做时可以忙碌起来,而当没有事可做时,又可以使得线程睡眠。

其他:
1.Run Loop 管理不是完全自动的;,设计线程的时候,适当的时候启动和响应传入事件;
2.每个线程都有一个关联的Run Loop 除了主线程默认开启,分线程需要手动运行;采用懒加载的方式进行加载;

1.1 Run Loop 结构:

Run Loop 接收2种不同类型的sources事件

1.1.1.输入源:

异步传递事件,通常是来自不同的线程或不同的应用的消息。输入源异步传递事件到对应的处理程序和在线程关联的NSRunLoop对象调起runUntilDate:方法来退出事件处理。

1.1.2.timer源:

同步地传递事件,发生在每个定时器调用或周期性地调用。Timer源传递事件到他们的处理程序,但是不会调用run loop来退出处理。

运行循环及其源的结构图


Paste_Image.png

1.2 Run Loop Modes

Run Loop模式是一个监视输入源和定时器的集合和注册成为run loop的观察者的集合。每次要运行run loop,都需要显示或隐式地指定某种运行的mode。只有与这种指定的mode关联的源才会被监视和允许传递他们的事件,同样地,只有与这种模式关联的观察者都会收到run loop行为变化的通知。与其它模式想关联的源,直到随后在合适的模式通过循环后,都会接收到新的事件

我们通过名称来唯一标识mode。在Cocoa和Core Foundation中都定义了default模式和几个常用的模式,都是通过字符串名称来指定。我们也可以自定义模式,但是我们需要手动添加至少一个input source/timers/observers。

通过使用mode来过滤掉我们不希望接收到来自不想要的通过run loop的源。大部分情况下,我们都是使用系统定义的default模式。对于子线程,我们可以使用自定义模式在关键性操作时阻止低优先级的源传递事件。

Paste_Image.png

二丶什么时候使用Run Loop

2.1什么时候应该使用run loop呢?

只有当我们需要创建子线程的时候,才会需要到显示地运行run loop。应用程序的主线程的run loop是应用启动的基础任务,在启动时就会自动启动run loop。所以我们不需要手动启动主线程的run loop。

对于子线程,我们需要确定线程是否需要run loop,如果需要,则配置它并启动它。我们并不问题需要启动run loop的。比如说,如果我们开一个子线程去执行一些长时间的和预先决定的任务,我们可能不需要启动run loop。Run loop是用于那么需要在线程中有更多地交互的场景。比如说,我们会在下面的任何一种场景中需要开启run loop:

1.使用端口源或者自定义输入源与其它线程通信
2.在线程中使用定时器
3.使用Cocoa中的任何performSelector…方法
4.保持线程来执行周期性的任务

2.2 使用Run Loop 对象

2.2.1

Run Loop对象给添加输入源、定时器和观察者到run loop提供了主接口。每个线程都有一个单独的run loop与之关联(对于子线程,若没有调用过任何获取run loop的方法是不会有run loop的,只有调用过,才会创建或者直接使用)。

采用懒加载的方式获取方式:

2.2.2通过以下两种方式来获取run loop对象:
1.在Cocoa中,使用[NSRunLoop currentRunLoop]获取
2.使用CFRunLoopGetCurrent()函数获取
2.2.3配置RunLoop

在子线程运行run loop之前,你必须至少添加一种输入源或者定时器。如果run loop没有任何的源需要监视,它就会立刻退出。

除了添加sources之外,你还可以添加观察者来检测runloop不同的执行状态。要添加观察者,可以使用CFRunLoopObserverRef指针类型和使用CFRunLoopAddObserver函数来添加到run loop中。我们只能通过Core Foundation来创建run loop观察者,即使是Cocoa应用。

下面这段代码展示主线程如何添加观察者到run loop以及如何创建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 启动Run Loop

只有子线程才有可能需要启动run loop。Run loop必须至少有一种输入源或者timer源来监视。如果没有任何源,则run loop会退出。

下面的几种方式可以启动run loop:

  • 无条件地:无条件进入run loop是最简单的方式,但也是最不希望这么做的,因为这样会导致run loop会进入永久地循环。可以添加、删除输入源和timer源,但是只能通过kill掉run loop才能停止。而且还不能使用自定义mode。
  • 限时:与无条件运行run loop不同,最好是给run loop添加一个超时时间。
  • 在特定的mode:除了添加超时时间,还可以指定mode。
    下面是运行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设定超时时间
2.告诉run loop要stop

设定超时时间是比较推荐的。我们可以通过CFRunLoopStop函数来停止run loop。

2.3 线程安全

CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。

NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

三丶使用示例:

3.1 检测卡顿监控

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

NSRunLoop调用方法主要就是在kCFRunLoopBeforeSources和kCFRunLoopBeforeWaiting之间,还有kCFRunLoopAfterWaiting之后,也就是如果我们发现这两个时间内耗时太长,那么就可以判定出此时主线程卡顿.

技术要点:
1.利用CFRunLoopObserverRef 实时获得这些状态值的变化
2.知道RunLoop的Activity
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
};

  1. dispatch_semaphore_t 利用保证计算精确

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

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

@interface PerformanceMonitor ()
{
    int timeoutCount;
    
    //实时获得这些状态值的变化
    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状态观察
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                       kCFRunLoopAllActivities,
                                       YES,
                                       0,
                                       &runLoopObserverCallBack,
                                       &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
    
    // 在子线程监控时长
    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(@"---卡顿计时器:%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];
 
//滑动Scroll,或者tabview等,由于主线程只能执行一个程序,正在运行的timer会被停止,所以,加入以下字段,能提升计时器的优先级别
 
[[NSRunLoop currentRunLoop] addTimer:_myTimer forMode:UITrackingRunLoopMode];
 
二丶分线程情况:
 
//创建分线程
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]创建RunLoop
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    //分线程必须运行
    [[NSRunLoop currentRunLoop] run];
});
 
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容