Runloop

NSRunLoop简介

一. 什么是RunLoop

1、RunLoop

  • 从字面上了解, RunLoop即是运行循环, 就像是在一个圆形循环中去运作
  • RunLoop的基本作用
    • 他是App持续运行的保证, 如果RunLoop不存在了, 程序也就终止运行了
    • RunLoop会在循环中处理App的各种事件, 如触摸事件, 定时器事件, Selector事件
    • RunLoop最大的优势就是能节省CPU的资源, 提高程序的性能, 他会在需要执行任务的时候被唤醒, 当没有任务执行的时候进入休眠状态

2、Main函数中的RunLoop

  • 首先, 重温一遍App的启动原理
  • 当Main函数执行到UIApplicationMain时, 就开启了RunLoop运行循环
  • 在运行循环开启时, 就会保证程序的持续运行并且处理App的各种事件, 不会退出
  • Main函数中的RunLoop, 被称为主运行循环, 而主运行循环在整个App的声明周期中都不会被销毁, 他是程序运行的保证
// 程序在启动时,第一步就会执行main函数,在main函数中会执行以下操作:
int main(int argc, char * argv[]) {
    @autoreleasepool {
        /*
           nil:UIApplication类名或者子类名称,如果为nil,就等于@"UIApplication"
           NSStringFromClass([AppDelegate class]:UIApplication代理的名称
        */
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
     }
}

程序启动的完整流程
    1. 执行main函数
    2. 执行UIApplicationMain函数
      1> 指定UIApplication对象
      2> 指定UIApplication的代理
    3. 创建UIApplication对象,并且指定他的代理
    4. **创建一个事件循环:主循环(RunLoop),并且是一个死循环,保证程序的持续运行**
    5. 加载配置了所有应用程序信息的info.plist文件
      1> 判断 Main storyboard file base name中有没有指定Main,即需要加载的StoryBoard文件
      2> 如果指定了,就加载Main.storyboard
      3> 如果没有指定的话,就会黑屏
6. 应用程序启动完毕

二. NSRunLoop和CFRunLoopRef

1、简单介绍

  • CFRunLoopRef是在CoreFoundation框架中的, 它的内部API以及实现, 都是纯C语言编写, 这些API都是现成安全的
  • NSRunLoop是基于CFRunLoopRef的封装, 他提供了面向对象的API, 但是这些API不是线程安全的
  • 目前CFRunLoopRef已经开源了, 大家可以在官方文档中查看: 友情提示: 需要很好的C语言功底

2、NSRunloop和CFRunLoopRef都是RunLoop对象, 他们的区别是

  • 这两个对象的地址不同, 因为他们的对象来自于完全不同的类
  • CFRunLoop可以调用getCfRunLoop方法, 将NSRunLoop转化为CFRunLoop
  • 线程: RunLoop在主线程中, 保持持续循环状态, 当所有的事件处理结束, 就会进入休眠状态
  • 当外界传入各种时间时: Prot接口事件时, RunLoop就会被唤醒, 处理相关的事件, 当事件处理完毕时, 会再次进入休眠状态
    • UI交互事件
    • PerformSelector: onThread: 让线程执行任务
    • Timer: 定时器事件

3、RunLoop运行图解

4、简单的应用

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {     
    // 1. 获取当前线程对应的RunLoop对象     
    NSRunLoop *curRunLoop = [NSRunLoop currentRunLoop];     
    NSLog(@"%p", curRunLoop);     
    // 2. 获取主线程对应的RunLoop对象     
    NSRunLoop *mainRunLoop = [NSRunLoop mainRunLoop];     
    NSLog(@"%p", mainRunLoop);    
   
     /* Core Foundation */     
    // 1. 获得当前线程的RunLoop对象     
    CFRunLoopRef runloop = CFRunLoopGetCurrent();     
    NSLog(@"%p", runloop);    
     
    // 2. 获得主线程的RunLoop对象     
    CFRunLoopRef cfMainRunLoop = CFRunLoopGetMain();     
    NSLog(@"%p", cfMainRunLoop);     

    // 从这里可以看出这两种运行循环是完全不同的对象     
    // NSRunLoop --> CFRunLoopRef     
    NSLog(@"%p---%p", cfMainRunLoop, mainRunLoop.getCFRunLoop);     

    // 开启子线程,执行task方法    
    [NSThread detachNewThreadSelector:@selector(task) toTarget:self withObject:nil]; 
} 

- (void)task {     
    /* 子线程和RunLoop      
    1. 每一个子线程,都对应一个自己的RunLoop      
    2. 主线程的RunLoop在程序运行的时候就已经创建了,而子线程的RunLoop则需要手动开启      
    3. [NSRunLoop currentRunLoop],此方法会开启一个新的RunLoop      
    4. RunLoop需要执行run方法,来开启,但如果RunLoop中没有任何任务,就会关闭      
    */     

    // 1. 当前RunLoop     
    NSLog(@"%p--%p", [NSRunLoop currentRunLoop], [NSRunLoop mainRunLoop]);
     
    // 2. 开启一个新的RunLoop     
    [[NSRunLoop currentRunLoop] run];     
    NSLog(@"tast---%@", [NSThread currentThread]); 
}

三. RunLoop与线程

1、每一条线程, 都有一个与之相对应的RunLoop对象, 负责处理线程中的任务

2、线程的创建

  • 主线程: RunLoop是在程序已经启动的时候就创建好了, 当程序关闭的时候主线程才被销毁
  • 子线程: 子线程需要手动创建RunLoop, 并且手动开启, 当没有任务执行时, 该线程会被关闭, RunLoop被销毁

3、子线程和RunLoop

  • 子线程会单独开启RunLoop去执行任务
  • 子线程和RunLoop是一一对应的关系, 每个子线程都有自己的RunLoop(但需要主动创建)
  • 创建子线程的RunLoop: [NSRunloop currentRunLoop]
    • 通过对CFRunLoop原码的分析可以判断出, 这个方法是懒加载获取RunLoop对象的, 当第一次调用这个方法时, 他就会在对应的线程中创建一个RunLoop, 并且保存到一个字典中便于随时取出
    • 也就是说, 如果不主动去获取RunLoop, 那么默认是不会给子线程创建一个RunLoop的
  • 子线程的RunLoop需要手动开启: [[NSRunLoop currentRunLoop] run]
  • 如果RunLoop内部没有任何任务需要去处理时, 就会被关闭
// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRefstatic 
CFMutableDictionaryRef loopsDic;
// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;

// 获取一个 pthread 对应的 RunLoop
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }

    // 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));

    if (!loop) {
        // 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);

        // 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }

    OSSpinLockUnLock(&loopsLock);
    return loop;
}

CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}

CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

四. RunLoop的相关类

1、RunLoop的运行模式图
  • 一个RunLoop包含多个Mode, 而每个Mode又包含若干个Source/Timer/Observer
2、相关类
  • 1、RunLoop: RunLoop对象本身

  • 2、RunLoopMode: 即RunLoop的运行模式

    • 一个RunLoop至少要制定一个运行模式, 当运行模式指定之后, 至少有一个Source或者Timer任务在执行
    • RunLoop启动之后, 只能指定一个运行模式, 可以使用currentMode来获取
    • 如果要切换RunLoop的运行模式, 就要先退出当前的RunLoop, 重新指定Mode再次进入运行
  • 3、系统默认注册的5个Mode

    • kCFRunLoopDefaultMode: App的默认Mode, 通常主线程是在这个Mode下运行的
    • UITrackingRunLoopMode: 界面跟踪Mode, 用于ScrollView/TableView等追踪触摸滑动, 保证界面滑动的时候不受其他Mode影响
    • UIInitializationRunLoopMode: 当App启动时, 第一个进入的Mode, 启动完成之后就不会再使用这个Mode
    • GSEventReceiveRunLoopMode: 接收系统事件的内部Mode, 通常由系统自动管理
    • kCFRunLoopCommonMode: 一个类似于占位的Mode, 并不是一个真正的Mode
3、Mode中的类
  • 1、CFRunLoopSourceRef: 事件源, 事件, 输入等都属于事件源, 他有两个分类

    • Source0, 非基于Port, 用户触发的事件, 例如点击事件等
    • Source1, 基于Port的事件, 他用于系统内部与线程之间交互
  • 2、CFRunLoopTimerRef: 定时器事件

    • NSTimer:
      • 如果将NSTimer添加到子线程中, 需要先创建一个RunLoop, 然后再启动RunLoop
      • NSTimer受到RunLoop的印象, 一般会有一些轻微的误差, 所以对于精密计时,GCD定时器较为精准
    • GCD定时器: 精确到纳秒, 较为精准
      • GCD定时器的创建步骤: 创建定时器 -> 设置定时器 -> 设置定时器的回调方法 -> 恢复定时器
      • GCD定时器一定要添加一个强引用, 否则会被立即释放
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    // 1. 创建GCD定时器
    /*
    DISPATCH_SOURCE_TYPE_TIMER 定时器
    uintptr_t handle 描述信息
    unsigned long mask 传入0
    dispatch_queue_t queue 定时器运行的队列,决定定时器在哪个线程中运行
    */
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));

    // 2. 设置定时器
    /*dispatch_source_t source, 定时器的对象
    dispatch_time_t start, 定时器什么时候开始
    uint64_t interval, 定时器多长时间执行一次
    uint64_t leeway 精准度,0为绝对精准
    */
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);
    // 3. 设置定时器的任务
    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"---GCD---%@", [NSThread currentThread]);
    });

    // 4. 恢复定时器
    dispatch_resume(timer);

    // 5. 强引用定时器,否则创建出来就会被释放
    self.timer = timer;
}
4、CFRunLoopObserverRef: 观察者
  • 观察者可以观察到RunLoop不同的运行状态
  • 通过判断RunLoop的运行状态, 可以执行一些操作
// 1. 创建监听者
/*
CFAllocatorRef allocator 分配存储空间
CFOptionFlags activities 要监听哪个状态,kCFRunLoopAllActivities监听所有状态
Boolean repeats 是否持续监听RunLoop的状态
CFIndex order 优先级,默认为0
Block activity RunLoop当前的状态
*/
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    /*
    kCFRunLoopEntry = (1UL << 0), 进入工作
    kCFRunLoopBeforeTimers = (1UL << 1), 即将处理Timers事件
    kCFRunLoopBeforeSources = (1UL << 2), 即将处理Source事件
    kCFRunLoopBeforeWaiting = (1UL << 5), 即将休眠
    kCFRunLoopAfterWaiting = (1UL << 6), 被唤醒
    kCFRunLoopExit = (1UL << 7), 退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU 监听所有事件
    */

    // 当activity处于什么状态的时候,调用一次
    switch (activity) {
        case kCFRunLoopEntry:
            NSLog(@"进入");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"即将处理Timer事件");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"即将处理Source事件");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"即将休眠");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"被唤醒");
            break;
        case kCFRunLoopExit:
            NSLog(@"退出RunLoop");
            break;default:break;
    }
});

// 2. 给对应的RunLoop添加一个监听者,并制定监听的是那种运行模式
/*
CFRunLoopRef rl 要添加监听者的RunLoop
CFRunLoopObserverRef observer, 要添加的监听者
CFStringRef mode RunLoop的运行模式
*/
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
常驻线程

怎样实现一个常驻线程?
1、为当前线程开启一个RunLoop。
2、向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环。
3、启动该RunLoop。

#import <Foundation/Foundation.h>
@interface MCObject : NSObject
@end
#import "MCObject.h"
@implementation MCObject
static NSThread *thread = nil;
// 标记是否要继续事件循环
static BOOL runAlways = YES;
+ (NSThread *)threadForDispatch{
    if (thread == nil) {
        @synchronized(self) {
            if (thread == nil) {
                // 线程的创建
                thread = [[NSThread alloc] initWithTarget:self selector:@selector(runRequest) object:nil];
                [thread setName:@"com.imooc.thread"];
                //启动
                [thread start];
            }
        }
    }
    return thread;
}
+ (void)runRequest
{
    // 创建一个Source
    CFRunLoopSourceContext context = {0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL};
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
    
    // 创建RunLoop,同时向RunLoop的DefaultMode下面添加Source
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    
    // 如果可以运行
    while (runAlways) {
        @autoreleasepool {
            // 令当前RunLoop运行在DefaultMode下面
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, true);
        }
    }
    
    // 某一时机 静态变量runAlways = NO时 可以保证跳出RunLoop,线程退出
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
    CFRelease(source);
}
@end
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,529评论 5 475
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,015评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,409评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,385评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,387评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,466评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,880评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,528评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,727评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,528评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,602评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,302评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,873评论 3 306
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,890评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,132评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,777评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,310评论 2 342

推荐阅读更多精彩内容