iOS小结之Runloop

栏目: IOS · 发布时间: 7年前

内容简介:iOS小结之Runloop

学过操作系统的同学都知道,一般来说,一条线程只能执行一个任务,当任务结束后,线程就会退出,完成使命。但是,在很多场景中,我们并不想让线程执行完任务就退出,往往,我们需要其保持随时随地听从命令,可以在需要的时候执行任务,不需要的时候处于等待状态。

function loop() {
    initLoop();
    do {
        if(message_hasTask()) {
            message_execute(get_current_message());
        }
    } while (message != quit);
}

比如,在手游上,我们需要一个模型来一直监听用户的触摸事件,来对游戏画面进行切换和逻辑判断。这种模型一般都具有相似性。比如:当模型被事件唤醒之后,如何快速地进行响应;当模型处于等待状态时,如何减少资源耗用等等。

这种模型,在iOS中称作Runloop。Runloop所解决的问题,就是实现一个线程,使得可以随时响应用户的事件,而不退出。

在iOS中,提供了两种有关的对象,一种是 NSRunLoop ,另外一种是 CFRunLoopRefCFRunLoopRef 是基于 CoreFoundation 框架的,提供了纯C函数的API,所以这些API都是线程安全的。我们一般听到的 NSRunLoop ,是对 CFRunLoopRef 的封装,加上了面向对象的东西,所以这些API不是安全的。

所以,一般讲解 Runloop 的文章,其实都是在分析 CFRunLoopRef 这个东西。 CFRunLoopRef 的具体实现,苹果也将它开源了出来,可以在引用里看到其链接,这里就不再给出。

Runloop和线程

上面可以看出,需要实现一个Runloop模型,是基于线程之上的。所以,Runloop和线程是息息相关的,在iOS系统中,Runloop属于线程的基础架构部分。每个线程,包括程序的主线程,都有与之的Runloop模型。

其实,Runloop和线程的绑定,是保存在一个全局的Dictionary中,线程和Runloop之间是一一对应的。当线程创建后,默认并不会启动Runloop,只有主动获取的时候,Runloop才会被创建,当线程结束后,对应的Runloop也会随之销毁。但是有个例外,就是主线程对应的Runloop默认是启动的。当程序启动的时候,会执行下面的函数:

int main(int argc, char * argv[]) {
   @autoreleasepool {
       return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
   }
}

其中,在 UIApplicationMain 内部帮我们开启了主线程的Runloop,相当于说,主线程的Runloop是默认启动的。这样我们才可以监听触摸事件、页面刷新等等功能。

Runloop结构

Runloop主要由5个类组成。如下所示:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

其关系如下图:

iOS小结之Runloop

一个Runloop包含多个Mode,每个Mode里面又包含多个Source/Timer/Observer。但是,需要注意的是,每次开启Runloop的时候,只会使用其中一个Mode,一般叫做currentMode。有时候实际场景中需要切换Mode,切换的时候,只能先退出Mode,然后再添加上新的Mode。

Source0 和 Source1

CFRunLoopSourceRef有两个版本:Source0和Source1,Source的作用是给线程发送异步事件。其中Source0是接收App内部的事件,比如用户点击滑动等事件,App自己负责处理,需要手动来唤醒Runloop,来处理这个事件;Source1是通过内核和其他线程发送消息用的,可以自动唤醒Runloop的线程。

CFRunLoopTimerRef

顾名思义, CFRunLoopTimerRef 是基于时间来出发的,和NSTimer底层一样。当其加入到Runloop时,Runloop会注册对应的时间点,当时间点到时,Runloop会唤醒执行那个回调。常见的使用场景比如:延迟执行某个方法、CADisplayLink的使用等等。

- (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay;

+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;

CFRunLoopObserverRef

CFRunLoopObserverRef是基于观察者模式,和 CFRunLoopTimerRef 类似,每个Observer都会包含有一个回调,用于监听Runloop的状态变化。可以监听到的状态变化有如下几个:

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
};

上面所介绍的Source、Timer和Observer都可以添加到多个Model当中,但是如果在一个Mode当中添加多个,是不会起效果的,有一种特殊情况,就是当一个Mode里什么都没有,则这个Runloop就会直接退出。

Runloop Mode

上面已经说过,Runloop的运行是基于特定的Mode,其数据结构如下所示:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};

struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

这里需要注意一下,有一个很关键的属性叫 CommonModes ,每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

在iOS系统中包含了两个Mode,一个是 NSDefaultRunLoopModeUITrackingRunLoopMode 。这两个Mode都已经被注册成了CommonMode,一般App主线程的Runloop默认是 NSDefaultRunLoopMode ,当UIScrollView滑动的时候,Runloop就会退出,然后切换到 UITrackingRunLoopMode 。这两个是苹果公开的Mode,当然,还有一些私有Mode,就在这里不阐述了,有兴趣的读者可以Google一下。

出了上面这些,苹果还提供一个操作 CommonModes 的字符串,可以对所有CommonMode进行操作,叫做 kCFRunLoopCommonModes ,有些同学容易将这个东西和上面的Mode混淆,其实概念还是不一样的, kCFRunLoopCommonModes 并不是一个新的Mode,在App主线程的Runloop中,是一个 CommonModes 组合,包含 NSDefaultRunLoopModeUITrackingRunLoopMode 两种Mode。这里的话在下面NSTimer的应用场景中详细讲下。

Runloop常见应用场景

AutoreleasePool

AutoreleasePool其实和Runloop是有关系的,有一个经典的面试题是问 AutoreleasePool对象是什么时候释放的? ,什么答案都有,标准答案应该是AutoreleasePool对象应该是在当前Runloop迭代结束之后释放的。

当App启动之后,iOS会在主线程注册两个Observer,第一个Observer是监听即将进入Runloop的状态,监听到后,来创建AutoreleasePool对象,其优先级也是最高的,要保证创建AutoreleasePool发生在其他所有回调之前;第二个Observer是监听Runloop准备休眠状态,来释放旧的AutoreleasePool对象,并且创建新的AutoreleasePool对象以供使用,另外还需要监听即将退出Runloop的状态,优先级是最低的,以此来保证释放操作在其他所有回调之后。

所以,如果以后有人再问你 AutoreleasePool对象是什么时候释放的? ,一定要说这和Runloop有关系,当Runloop准备休眠的时候,会释放旧的AutoreleasePool对象,创建新的AutoreleasePool对象,当Runloop即将退出的时候,会释放掉相关所有的AutoreleasePool对象。

NSTimer

NSTimer在时间点的触发,是基于Runloop运行的,使用NSTimer之前,都需要将其注册到Runloop上。其实NSTimer就是CFRunLoopTimerRef,一个NSTimer注册好之后,Runloop会自动在时间节点注册好事件。但是Runloop为了节省资源,并不会在非常准确的时间节点调用定时器。为此,NSTimer专门提供了一个tolerance属性,来设置宽容度,标记当时间节点到来之后,容许有多少误差可以触发回调。如果错过了某个时间节点,就只能等下一个时间节点的到来。

回到之前Mode的话题,当UISCrollView进行滚动的时候,NSTimer就无法正常工作,停止滑动又回恢复正常。这个原因是,添加到Runloop的NSTimer默认是以 NSDefaultRunLoopMode 模式在工作,当UIScrollView进行滑动的时候,Runloop会退出,然后切换到 UITrackingRunLoopMode 模式。由于NSTimer不是在这个模式下运行的,所以不会触发定时任务,无法工作。

要解决这个问题,我们就需要将NSTimer在 NSDefaultRunLoopModeUITrackingRunLoopMode 两个模式下工作,在UIScrollView滑动的时候也可以进行触发定时任务。所以,我们需要使用到 kCFRunLoopCommonModes 来完成任务。一般使用如下语句:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

事件响应和手势识别

事件响应其实和Runloop也是有关系的,为此,苹果专门注册了一个Source1用来接收系统事件。当手机的硬件发生感应,比如触摸、锁屏和摇晃,苹果注册的这个Source1都会收到回调,然后会将事件进行应用内部分发。

然后,App内主线程的Runloop会触发Source0事件,Source0收到回调进行下一步操作。

不同于事件响应,对于手势识别,收到苹果Source1对应用内部分发事件后,首先先将所有的手势回调打断,将所对应的UIGestureRecognizer事件标记为待处理,然后苹果会注册一个Observer来监听Runloop即将进入休眠的状态,然后在Observer回调里执行GestureRecognizer的回调。

UI更新

UI更新也和Runloop有关,当在操作UI时,改变了UI的大小、层次等,这个 UIView/CALayer就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个Observer来监听Runloop即将进入休眠和即将退出的状态,然后在其Observer回调里遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

GCD

GCD底层也和Runloop有关,当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch会向主线程的RunLoop发送消息,RunLoop会被唤醒,并从消息中取得这个block,并在回调里执行这个block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

PerformSelecter

PerformSelecter其实是创建了一个Timer,然后添加到当前的线程中。如果当前线程没有Runloop,这个方法则走不通的。

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;

另外,还有AFNetworking2.0、NSURLConnection的使用其实都和Runloop有关,AFNetworking2.0希望能在后台线程接收到Delegate回调,单独创建了一条线程,并在这条这个线程中启动了Runloop。但是AFNetworking换成基于 NSURLSession 之后,并没有看到相关Runloop代码。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

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

失控的未来

失控的未来

[美]约翰·C·黑文斯 / 仝琳 / 中信出版集团 / 2017-4-1 / 59.00元

【编辑推荐】 20年前,尼古拉•尼葛洛庞帝的《数字化生存》描绘了数字科技给人们的工作、生活、教育和娱乐带来的冲击和各种值得思考的问题。数字化生存是一种社会生存状态,即以数字化形式显现的存在状态。20年后,本书以一种畅想的形式,展望了未来智能机器人与人类工作、生活紧密相联的场景。作者尤其对智能机器人与人类的关系,通过假设的场景进行了大胆有趣的描述,提出了人工智能的未来可能会面临的一些问题。黑文......一起来看看 《失控的未来》 这本书的介绍吧!

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

在线压缩/解压 JS 代码

MD5 加密
MD5 加密

MD5 加密工具

RGB HSV 转换
RGB HSV 转换

RGB HSV 互转工具