内容简介:iOS小结之Runloop
学过操作系统的同学都知道,一般来说,一条线程只能执行一个任务,当任务结束后,线程就会退出,完成使命。但是,在很多场景中,我们并不想让线程执行完任务就退出,往往,我们需要其保持随时随地听从命令,可以在需要的时候执行任务,不需要的时候处于等待状态。
function loop() { initLoop(); do { if(message_hasTask()) { message_execute(get_current_message()); } } while (message != quit); }
比如,在手游上,我们需要一个模型来一直监听用户的触摸事件,来对游戏画面进行切换和逻辑判断。这种模型一般都具有相似性。比如:当模型被事件唤醒之后,如何快速地进行响应;当模型处于等待状态时,如何减少资源耗用等等。
这种模型,在iOS中称作Runloop。Runloop所解决的问题,就是实现一个线程,使得可以随时响应用户的事件,而不退出。
在iOS中,提供了两种有关的对象,一种是 NSRunLoop
,另外一种是 CFRunLoopRef
。 CFRunLoopRef
是基于 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
其关系如下图:
一个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,一个是 NSDefaultRunLoopMode
和 UITrackingRunLoopMode
。这两个Mode都已经被注册成了CommonMode,一般App主线程的Runloop默认是 NSDefaultRunLoopMode
,当UIScrollView滑动的时候,Runloop就会退出,然后切换到 UITrackingRunLoopMode
。这两个是苹果公开的Mode,当然,还有一些私有Mode,就在这里不阐述了,有兴趣的读者可以Google一下。
出了上面这些,苹果还提供一个操作 CommonModes
的字符串,可以对所有CommonMode进行操作,叫做 kCFRunLoopCommonModes
,有些同学容易将这个东西和上面的Mode混淆,其实概念还是不一样的, kCFRunLoopCommonModes
并不是一个新的Mode,在App主线程的Runloop中,是一个 CommonModes
组合,包含 NSDefaultRunLoopMode
和 UITrackingRunLoopMode
两种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在 NSDefaultRunLoopMode
和 UITrackingRunLoopMode
两个模式下工作,在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代码。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
High Performance JavaScript
Nicholas C. Zakas / O'Reilly Media / 2010-4-2 / USD 34.99
If you're like most developers, you rely heavily on JavaScript to build interactive and quick-responding web applications. The problem is that all of those lines of JavaScript code can slow down your ......一起来看看 《High Performance JavaScript》 这本书的介绍吧!
MD5 加密
MD5 加密工具
HSV CMYK 转换工具
HSV CMYK互换工具