教你如何轻松搞定 Runloop

认识 Runloop

  • Runloop 就是运行循环,如果没有 Runloop,程序一运行就会退出,有 Runloop 就相当于在程序内部开了一个死循环
  • 在 iOS 开发中,有两套 API 可以访问 Runloop:NSRunloopCFRunloopRef,它们是等价的,可以相互转换
  • NSRunloop 是基于 CFRunloopRef 的 OC 包装
  • 参考资料:苹果官方文档CFRunloopRef 源码

Runloop 的本质

  • Mach 是 XNU 的内核,进程、线程和虚拟内存等对象通过端口发消息进行通信,Runloop 通过 mach_msg() 函数发送消息
  • 如果没有 port 消息,内核会将线程置于等待状态 mach_msg_trap()
  • 如果有消息,判断消息类型处理事件,并通过 modeItem 的 callback 进行回调

Runloop 的作用

  • 保证程序的持续运行
  • 处理 APP 中的各类事件
  • 节省 CPU 资源,提高程序性能(有事情就做,没事情就休息)

Runloop 与线程的关系

  • Runloop 与线程一一对应
  • 主线程中的 Runloop 在程序运行时已经创建并启动了
  • 子线程中的 Runloop 需要我们手动创建并开启
  • Runloop 在线程结束时,也会销毁

获取 Runloop 对象

  • 获取主线程 Runloop 对象
NSRunLoop *mainRL = [NSRunLoop mainRunLoop];
CFRunLoopRef mainRLRef = CFRunLoopGetMain();
  • 获取当前线程 Runloop 对象
NSRunLoop *currentRL = [NSRunLoop currentRunLoop];
CFRunLoopRef currentRlRef = CFRunLoopGetCurrent();
  • 通过子线程创建 Runloop
NSRunLoop *curRunloop = [NSRunLoop currentRunLoop];

这个方法本身是懒加载的,如果是第一次调用该方法,那么就创建子线程对应的 Runloop。

  • 补充:Runloop 对象是利用字典进行存储的,key 值对应线程对象,value 值对应该线程的 Runloop,在子线程中 Runloop 不会自动创建。

Runloop 的相关类

与 Runloop 相关的共有五个类:CFRunLoopRef、CFRunLoopModeRef、CFRunLoopTimerRef、CFRunLoopSourceRef、CFRunLoopObserverRef

Runloop 五个相关类之间的关系
  1. CFRunLoopRef( Runloop 对象)

    • Runloop 对象就是 Runloop 本身
  2. CFRunLoopModeRef( Runloop 的运行模式)

    • 一个 Runloop 包含若干个 Mode,而每个 Mode 又包含若干个 Source/Timer/Observer,每次 RunLoop 启动时,只能指定其中一个 Mode,这个 Mode 被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入,这样就可以分隔开不同组的 Source/Timer/Observer,让其互不影响。
    • Mode 的分类,系统默认注册了 5 个 Mode:
      • kCFRunLoopDefaultMode:App 的默认 Mode,通常主线程是在这个 Mode 下运行
      • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
      • UIInitializationRunLoopMode: 在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用
      • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
      • kCFRunLoopCommonModes: 占位用的 Mode,不是一种真正的 Mode,就相当于 KCFRunLoopDefaultMode 和 UITrackingRunLoopMode的合体
  3. CFRunLoopTimerRef( Timer 事件)

    • 基于时间的触发器,基本等同于 NSTimer
    • NSTimer 在各种模式下的运行效果
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    

    以上 scheduledTimerWithTimeInterval 方法内部默认把创建的定时器对象添加到当前的 Runloop 中,并且指定运行模式为 NSDefaultRunLoopMode

    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
    

    以上 timerWithTimeInterval 方法创建定时器,如果想要定时器工作,还需要添加到 Runloop 中,并指定运行模式

    • 注意:当 Runloop 切换到非指定模式,定时器就会停止工作
  4. CFRunLoopSourceRef( Runloop 要处理的事件源)

    • 事件源也就是输入源,只需要对它的分类有所了解就可以了
    • 以前的分法(根据官方文档)
      • Port-Based Sources(基于端口的源)
      • Custom Input Sources(自定义输入源)
      • Cocoa Perform Selector Sources(可执行选择器源)
    • 现在的分法(基于函数的调用栈)
      • Source0:非基于 Port 的(用户操作事件)
      • Source1:基于 Port 的(系统事件)
  5. CFRunLoopObserverRef( Runloop 的监听者)

    • 主要用于监听 Runloop 的状态

    • Runloop 的状态主要有:

      typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
          kCFRunLoopEntry = (1UL << 0),   //即将进入 Runloop
          kCFRunLoopBeforeTimers = (1UL << 1),    //即将处理 NSTimer
          kCFRunLoopBeforeSources = (1UL << 2),   //即将处理 Sources
          kCFRunLoopBeforeWaiting = (1UL << 5),   //即将进入休眠
          kCFRunLoopAfterWaiting = (1UL << 6),    //刚从休眠中唤醒
          kCFRunLoopExit = (1UL << 7),            //即将退出 Runloop
          kCFRunLoopAllActivities = 0x0FFFFFFFU   //所有状态改变
      };
      
    • 实现 Runloop 的监听

      //创建监听对象,当 Runloop 的状态改变时就会调用该方法
      CFRunLoopObserverRef observer =   CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
          switch (activity) {
              case kCFRunLoopEntry:
                  NSLog(@"进入runloop");
                  break;
              case kCFRunLoopBeforeTimers:
                  NSLog(@"即将处理time事件");
                  break;
              case kCFRunLoopBeforeSources:
                  NSLog(@"即将处理source事件");
                  break;
              case kCFRunLoopBeforeWaiting:
                  NSLog(@"即将休眠");
                  break;
              case kCFRunLoopAfterWaiting:
                  NSLog(@"runloop被唤醒");
                  break;
              case kCFRunLoopExit:
                  NSLog(@"runloop退出");
                  break;
              default:
                  break;
          }
      });
      //设置监听
      CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
      

Runloop 的启动

  1. 选择一个运行模式,且只能选择一个
  2. 判断当前选择的运行模式是否为空
  3. 检查当前运行模式里面是否有 Source 或 Timer,如果都没有,则 Runloop 立即退出
  4. 至少有 Source 或 Timer 中的任意一个,则 Runloop 开启
  5. 在检查的时候不会检查 Observer

Runloop 的运行处理逻辑

Runloop 的运行处理逻辑
  • 在运行 Runloop 时,RUnloop 会自动处理之前未处理的消息,并通知相关的监听者,具体的处理逻辑如下:
    1. 通知观察者 Runloop 已经启动
    • 通知观察者即将要开始的定时器
    • 通知观察者即将启动的非基于端口的源
    • 启动已经准备好的非基于端口的源
    • 如果有基于端口的源并处于等待状态,立即启动,跳到第九步
    • 通知观察者 Runloop 进入休眠
    • Runloop 进入休眠,等待发生以下事件时唤醒
      • 有事件到达基于端口的源
      • 定时器启动
      • Runloop 超时
      • Runloop 被外界手动唤醒
    • 通知观察者,线程刚被唤醒
    • 处理唤醒时收到的消息,之后跳到第二步
      • 如果有用户定义的定时器启动,处理定时器事件并重启 Runloop
      • 如果输入源启动,传递相应的信息
      • 如果 Runloop 被外界手动唤醒且未超时,重启 Runloop
    • 通知观察者,Runloop 即将结束

Runloop 的应用

  • 常驻线程

    • 在子线程中创建一个 Runloop
    • 需要至少指定 Runloop 的 Source 或者 Timer 中的任意一个(一般情况下指定 Source,比较简单)
    • 需要指定 Runloop 的运行模式(保证 Runloop 不退出)
    • 需要手动开启 Runloop
    //线程常驻
    //创建子线程
    -(IBAction)createThread:(id)sender {
        NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
        [thread start];
        self.thread = thread;
    }
    //继续执行任务
    -(IBAction)goOn:(id)sender {
        NSLog(@"继续执行任务");
        //线程间的通信
        [self performSelector:@selector(goOnRun) onThread:self.thread withObject:nil waitUntilDone:YES];
    }
    -(void)run {
        NSLog(@"run --- %@", [NSThread currentThread]);
        NSRunLoop *curRunloop = [NSRunLoop currentRunLoop];
        [curRunloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        [curRunloop run];
    }
    -(void)goOnRun {
        NSLog(@"goOnRun --- %@", [NSThread currentThread]);
    }
    
  • imageView 的显示

    • 控制方法在特定模式下可用
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip2016"] afterDelay:3.0];
    

    以上方法默认添加到当前的 Runloop 中,并且指定运行模式为默认 KCFRunLoopDefaultMode,如果 Runloop 切换运行模式,则图片不会加载到 imageView 上。

    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip2016"] afterDelay:3.0 inModes:@[NSRunLoopCommonModes]];
    

    以上方法添加到当前 Runloop 中,且指定运行模式为 NSRunLoopCommonModes

  • 自动释放池

    • 进入 Runloop 的时候第一次创建
    • 退出 Runloop 的时候最后一次释放(超时或线程销毁)
    • 其他时候的创建与释放
      • 当 Runloop 即将休眠的时候会把之前的自动释放池释放,再重新创建一个新的自动释放池
void msg(int n)
{
        NSLog(@"runloop被唤醒");
        NSLog(@"runloop处理事件---%zd",n);
}
int main(int argc, const char * argv[]) {
    @autoreleasepool {
         NSLog(@"runloop启动了");
        
            do {
                NSLog(@"runloop询问,还有事情需要我处理吗?");
                NSLog(@"没有事情的话,我就睡觉了");
                NSLog(@"runloop进入到休眠");
          
                int number = 0;
                scanf("%zd",&number);
                msg(number);
            
            } while (1);
        }
        return 0;
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容

  • Run loop 剖析:Runloop 接收的输入事件来自两种不同的源:输入源(intput source)和定时...
    Mitchell阅读 12,360评论 17 111
  • 一、什么是runloop 字面意思是“消息循环、运行循环”。它不是线程,但它和线程息息相关。一般来讲,一个线程一次...
    WeiHing阅读 8,036评论 11 109
  • 基本概念 进程 进程是指在系统中正在运行的一个应用程序,而且每个进程之间是独立的,它们都运行在其专用且受保护的内存...
    小枫123阅读 829评论 0 1
  • RunLoop的基本了解 **1 . RunLoop字面的意思 : **运行循环 / 跑圈 **2 . 基本作用 ...
    Mario_ZJ阅读 470评论 1 3
  • 如果没有RunLoop main函数中的RunLoop 第14行代码的UIApplicationMain函数内部就...
    JonesCxy阅读 498评论 0 4