ObjC RunLoop简析

栏目: Objective-C · 发布时间: 5年前

内容简介:当我们创建一个terminal项目的时候,此时的main函数中并没有一个RunLoop。所以程序运行完main函数之后就退出了。而一个iOS的application程序,默认在主线程开启了一个RunLoop,这样一个App就可以处理一些计时器事件,滑动事件等,不会马上退出。在iOS项目中每一条线程都对应着一个RunLoop对象,RunLoop存放在一个以线程作为key的散列表中。

当我们创建一个terminal项目的时候,此时的main函数中并没有一个RunLoop。所以程序运行完main函数之后就退出了。

ObjC RunLoop简析

而一个iOS的application程序,默认在主线程开启了一个RunLoop,这样一个App就可以处理一些计时器事件,滑动事件等,不会马上退出。

ObjC RunLoop简析

在iOS项目中每一条线程都对应着一个RunLoop对象,RunLoop存放在一个以线程作为key的散列表中。

主线程在创建的时候默认开启RunLoop,而其他子线程默认不开启,但是会在第一次获取RunLoop( [NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent() )的时候创建。

一般情况下RunLoop的生命周期跟随线程,线程结束的时候RunLoop也会被销毁。

iOS中提供了一套Foundation框架的NSRunLoop api和一套 基于Core Foundation的CFRunLoopRef api 来使用RunLoop。其中NSRunLoop是基于CFRunLoopRef做了一层OC的封装。

RunLoop 结构

在CFRunLoop的源码中RunLoop的基本结构如下:

ObjC RunLoop简析

CFRunLoopRef是一个 __CFRunLoop 的结构体,结构体中存放了许多mode相关的成员。

其中 _currentModeCFRunLoopModeRef 类型的,它是一个 __CFRunLoopMode 类型的结构体指针。RunLoop通过它来表征RunLoop的运行状态。

一个RunLoop中包含有许多Mode。 _commonModes 是一个可变的集合,集合中存放了许多mode。RunLoop在运行的时候只能选择一个Mode作为当前RunLoop执行的状态,也就是 _currentMode

mode是 CFRunLoopMode 类型的。而 CFRunLoopMode 是通过typedf __CFRunLoopMode 得到的。 __CFRunLoopMode 中存放了处理触摸事件的 source0 、系统时间捕捉的 source1 、处理计时器的 timers 、监听RunLoop状态的 observer 等。

另外,如果RunLoop需要切换运行状态的时候必须先退出当前的Mode,才能进入新的Mode。如果当前Mode中所有的 sourcetimerobserver 的时候RunLoop就会立刻退出。

常见的RunLoopMode有默认的mode kCFRunLoopDefaultMode 、跟踪界面(比如:保证滑动不受其他mode影响)的 UITrackingRunLoopMode 。另外在api中还有一个 kCFRunLoopCommonModes 但是这并不是一个真正的mode,它不存在于 _commonModes 中,它只是一个标记。

RunLoop 的监听器会监听RunLoop的一些状态:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0), // 即将进入runloop
    kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理计时器
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), // 即将从休眠中唤醒
    kCFRunLoopExit = (1UL << 7), // 即将退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU // 全部状态
};
复制代码

探究RunLoop的执行流程

我们可以通过Xcode自带的lldb 通过 bt 命令查看函数调用栈找到RunLoop的入口函数

ObjC RunLoop简析

CFRunLoop.c 文件中找到该函数,我们发现它通过 __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry) 监听进入RunLoop。接着有一个 __CFRunLoopRun 的函数调用,该函数中封装了RunLoop处理事件的逻辑。

我们只关注 __CFRunLoopRun 主要代码:我们发现该函数中存在着一个 do-while() 循环,当 retVal==0 的时候循环持续进行,当 retVal != 0 的时候,回返回给函数 CFRunLoopRunSpecific ,它调用 __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); 退出RunLoop。

__CFRunLoopRun 中主要的流程都在下图中进行了描述。总结来说:

  1. 首先会通知监听者Observers:即将处理Timers
  2. 通知监听者Observers:即将处理Sources
  3. 处理blocks
  4. 处理source0,如果处理完了会再次处理blocks
  5. 如果存在source1,则跳到handle_msg处理,如果没有则通知监听器即将进入休眠
  6. 休眠时期等待消息来唤醒当前线程
  7. 如果有消息唤醒则进入handle_msg处理计时器,gcd,source1这些信息
  8. 再次处理blocks
  9. 获取返回值retVal
  10. 进入 do-while() ,如果 retVal == 0 则循环持续进行。否则返回给 CFRunLoopRunSpecific 函数,退出RunLoop
ObjC RunLoop简析

RunLoop的应用

Timer

当我们使用 + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block; 创建一个计时器,并且页面存在scrollView的时候。滑动scrollView计时器就会停止运行。这是因为一开始runloop存在于 NSDefaultRunLoopMode ,当滑动事件响应的时候runloop会进入 UITrackingRunLoopMode 模式处理滑动事件,所有timer就会失去处理。

我们可以使用 + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block; 创建timer,然后将它放在一个 NSRunLoopCommonModes 标记的模式下进行工作,timer是可以t在_commonModes数组中存放的模式下工作的。这样就解决了滑动事件和timer计时器事件冲突的问题。

线程保活

当我们创建一条线程的时候,这条线程并没有一个RunLoop。当我们第一次获取RunLoop的时候这条线程中才会创建RunLoop。

所以创建一条一直存在的线程,我们需要在线程中加入一个不会被回收的RunLoop,也就是让 do-while() 一直存在,也就是RunLoop一直有事情在处理,而 retVal 不会为不是0的其他值。

实例代码:

#import "SoCPermanentThread.h"
@interface SoCPermanentThread ()

@property (nonatomic, strong) NSThread *thread;

@end

@implementation SoCPermanentThread

- (instancetype)init {
    if (self = [super init]) {
        self.thread = [[NSThread alloc] initWithBlock:^{
            CFRunLoopSourceContext context = {0};
            CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);
            CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);
            CFRelease(source);
            CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
        }];
        [self.thread start];
    }
    return self;
}

- (void)executeTask:(SoCPermenantThreadTask)task {
    if (!_thread || !task) return;
    [self performSelector:@selector(__task:) onThread:_thread withObject:task waitUntilDone:NO];
}

- (void)stop {
    if (!_thread) return;
    [self performSelector:@selector(__stop) onThread:_thread withObject:nil waitUntilDone:YES];
}

- (void)__task:(SoCPermenantThreadTask)task {
    task();
}

- (void)__stop {
    CFRunLoopStop(CFRunLoopGetCurrent());
    _thread = nil;
}

- (void)dealloc {
    [self stop];
}

@end
复制代码

上述代码使用 Core Foundation 实现的线程保活,其中重要的就是首先往RunLoop中添加Source保证RunLoop有事情可以做,另外就是 CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false); 这个方法的最后一个参数BOOL参数 returnAfterSourceHandled ,其值为 flase 代表执行完函数(处理完source)不会返回,而 true 则相反,表示执行完函数(处理完source)会立即返回。

总结

本篇主要以 Core Foundation api 为基础( Core Foundation 开源)简述了RunLoop的基本概念,和调用流程,由于 NSRunLoop 是基于 CFRunLoop 做的OC封装,其原理和流程都是一样的。另外介绍了两个使用RunLoop的案例。


以上所述就是小编给大家介绍的《ObjC RunLoop简析》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

天涯虚拟社区

天涯虚拟社区

刘华芹 / 民族出版社 / 2005-11 / 23.00元

网络空间很复杂,好多人并不完全了解或者只是了解到一些皮毛。比如说好多人对于见网友一事总是抱着浪漫或者暖昧的想法,而事实却并不总是想象的那样。作者在做虚拟社区研究甚至是在有这个想法之前并不常呆在网上,互联网对于作者来说就是查查资料、收发信年、看看新闻的工具。担是看着越来越多的人把时间花在网上,一处文化上的直觉告诉作者:有一种新的生活方式产生了。强烈的好奇心驱使着作者走到了网上,走到了天涯虚拟社区,并......一起来看看 《天涯虚拟社区》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

MD5 加密
MD5 加密

MD5 加密工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换