9

RunLoop运行循环基础和应用

 4 years ago
source link: http://mp.weixin.qq.com/s?__biz=MzUyMDAxMjQ3Ng%3D%3D&%3Bmid=2247492104&%3Bidx=1&%3Bsn=52d37ea3506a088e953ae8bdb58989a6
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

我们每天都在使用手机里的各种 APP ,作为软件开发者的我们,有没有思考过这样一个问题,某个 APP 开始运行后,如果对它没有操作,它就会像静止了,不会主动退出,也不会主动发生任何动作。但当我们触动一下 APP 界面上的某个输入框、按钮等,这时就会有相应的响应事件发生。这个 APP 就像是一直待命,没有操作时它在休息,有操作时立刻能做出响应。这就要归功于 RunLoop

RunLoop的概念

RunLoop字面意思即运行循环,它是iOS的一个底层机制。在程序运行过程中循环做一些事,如果有RunLoop程序会一直运行,时刻等待着用户的操作。

程序启动时,以main函数为入口,main函数中进入一个自动释放池,池中return了一个叫做UIApplicationMain的函数。

AzUjQ3N.jpg!web

UIApplicationMain函数做了什么事情呢? UIApplicationMain函数有4个参数,argc、argv前两个参数是操作系统调用main函数,传递给UIApplicationMain 的必要参数,argc指参数的长度,argv指参数的value,第三个参数是应用程序对象所属的类,该类必须继承自UIApplication类,如果所属类字符串的值为nil, UIKit就缺省使用UIApplication类,最后一个参数确定UIApplication的代理。 以下是它的官方定义:

Declaration

int UIApplicationMain(int argc, char * _Nullable *argv, NSString *principalClassName, NSString *delegateClassName);

Summary

Creates the application object and the application delegate and sets up the event cycle.

Returns

Even though an integer return type is specified, this function never returns. When users exits an iOS app by pressing the Home button, the application moves to the background.

UIApplicationMain函数会建立起UIApplication类和代理,用来接收类似 didFinishLaunching 等与应用的生命周期相关的代理方法,并且建立起运行循环。虽然这个方法标示返回一个 int值,但它不会返回,它会一直存在于内存中,除非用户或者系统将其强制终止。

回到文章最初的问题,应用程序为什么不会退出?

因为UIApplicationMain函数会为main thread设置一个RunLoop对象,函数内部有一个保证应用程序不退出的死循环。当用户点击APP icon时,操作系统通过调用该应用程序的main函数来启动该应用程序。首先操作系统会为应用程序开启一条线程,CPU调度这条线程,这条线程是当前APP的主线程即常驻线程,之所以这条线程不会被释放,因为这条线程上的RunLoop被开启了,来保证应用程序不退出。

RunLoop的作用

通过如上解释,我们知道了RunLoop能保证线程不退出;

其次,RunLoop负责监听所有事件。比如时钟事件、selector事件、触摸事件等。

Runloop官方图解:

JVbai2V.png!web

RunLoop 在循环过程中收到 Input sources Timer sources 事件后,会交给对应的方法去处理,没有事件传入时, RunLoop 会一直循环,等待用户操作。

我们以时钟事件为例:

我们创建NSTimer有以下方式:第一种通过timerWithTimeInterval方法,创建一个timer。

Declaration

+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

Discussion

You must add the new timer to a run loop, using addTimer:forMode:. Then, after tiseconds have elapsed, the timer fires, sending the message aSelector to target. (If the timer is configured to repeat, there is no need to subsequently re-add the timer to the run loop.)

为了时钟事件能够响应,就必须把timer添加到当前的RunLoop中,从它的官方定义中就能看出。

aAJJreU.jpg!web

第二种通过scheduledTimerWithTimeInterval方法, 创建一个timer,同样能够响应事件。

Declaration

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(id)userInfo repeats:(BOOL)yesOrNo;

Summary

Creates a timer and schedules it on the current run loop in the default mode.

官方定义中该方法内部已经封装了将timer添加到当前的RunLoop。

bEJrmez.jpg!web

RunLoop一直保持等待接收事件的状态,当NSTimer添加到RunLoop运行循环中,它就会去处理时钟事件,即每2秒触发timerFire方法。

RunLoop在响应事件时,是分模式的。它可以在多种模式下进行切换,系统提供了5种模式。

NSDefaultRunLoopMode:默认模式

UITrackingRunLoopMode:UI界面模式

NSRunLoopCommonModes:占位模式

UIInitializationRunLoopMode:程序初始化模式

GSEventReceiveRunLoopMode:系统内核模式

当我们创建timer时钟事件把它加入RunLoop,设定运行模式为NSDefaultRunLoopMode时,在视图滚动时,时钟事件不会被触发。因为RunLoop在处理UI触摸事件,而忽略了时钟事件。RunLoop会优先处理UITrackingRunLoopMode模式下的事件,。即UITrackingRunLoopMode优先级高于NSDefaultRunLoopMode。为了解决这个问题,我们可以设定RunLoop运行模式为NSRunLoopCommonModes, 它是一种占位模式,代表在默认模式和UI模式下都添加某事件。

RunLoop对象

RunLoop不能直接被创建,但苹果提供了自动获取的方法

1. Core Foundation框架下的CFRunLoopRef对象,通过以下方式获取当前线程与主线程的RunLoop对象:

CFRunLoopGetCurrent();

CFRunLoopGetMain();

2. Foundation框架下的NSRunLoop对象,是对CFRunLoopRef的一层OC的封装。通过如下方式获取:

[NSRunLoop currentRunLoop];

[NSRunLoop mainRunLoop];

RunLoop相关类

Runloop在Core Foundation框架下5 个类:

CFRunLoopRef

CFRunLoopModeRef

CFRunLoopSourceRef

CFRunLoopObserverRef

CFRunLoopTimerRef

图示了这5个类的关系: 一个RunLoop对象包含若干个运行模式,每个运行模式由三种元素组成:Source(事件源)、Timer(定时器)、Observer(观察者)。

yeERJru.jpg!web

CFRunLoopSourceRef

Source:即一切事件的来源,所有的事件都被包装成source。叫做事件源(输入源)

按照上文中列举的Runloop官方图解来分类:

1.port-Based Source  基于port端口系统内核事件

2.custom input Source 自定义事件源

3.cocoa perform selector sources

按照函数调用栈的分类

Source0:基于port的非系统内核事件,如触摸事件、点击事件

Source1:系统内核事件

CFRunLoopTimerRef

即定时器NSTimer

NSTimer可以通过timerWithTimeInterval:target:selector:userInfo:repeats创建。为了时钟事件能够响应,就必须手动把timer添加到当前的RunLoop中。

也可通过scheduledTimerWithTimeInterval:target:selector:userInfo:repeats创建,同样能够响应事件。因为方法内部已经封装了将timer添加到RunLoop。

CFRunLoopObserverRef 

观察者,可以监听 RunLoop 的状态改变,包括kCFRunLoopBeforeSource, kCFRunLoopAfterWaiting等。我们可以通过制造NSTimer事件唤醒RunLoop去监听它的状态,在休眠时唤醒让他去处理一下耗时任务等。

RunLoop与线程

线程是用来执行特定任务的,执行完成就会退出。但我们在实际开发中,可能会遇到这样的情况,我们需要让某个线程在某个特定条件下不退出,持续地处理任务。

举例来说,NSTimer的回调事件里有耗时操作,耗时操作是需要放入子线程的,且我们期望时钟事件能被持续执行。

3UvIviM.jpg!web

但是按照上面的写法,timerFire方法中打印结果是没有的,它并没有被触发。因为firstThread子线程在viewDidLoad方法执行结束后,已经被释放了。我们知道,线程是由CPU调用和执行的,那线程怎样保活,就是让当前线程所在的RunLoop运行起来,线程才不会被释放,一直被CPU所调度,时钟事件才会被执行。这就需要我们在添加NSTimer到当前的RunLoop后,启动它。

67JnIjF.jpg!web

这样,firstThread子线程就能一直不被释放,去执行NSTimer的事件。

那当某个特定条件已经达成时,我们需要退出这个子线程,怎么做呢?

YrQfAnu.jpg!web

我们可以在NSTimer回调方法里,通过[NSThread exit]来直接退出线程。

所以,RunLoop与线程是息息相关的:每条线程都有唯一一个与之对应的Runloop对象;子线程的RunLoop在第一次获取时创建,需要手动创建,在线程结束时销毁。而主线程的RunLoop已经被操作系统创建并开启了。

RunLoop的应用

上文我们讲到RunLoop可以保活线程,多线程在我们开发时经常用到,一个子线程中的任务完成后会被销毁,如果我们希望每次它不退出持续地执行任务时,就可以把它加入运行循环,从而避免了子线程频繁的创建和销毁。

通过把创建NSTimer对象添加到RunLoop,把运行模式设定为NSRunLoopCommonModes,来保证NSTimer在视图滑动的情况下也正常计时。

另外,RunLoop还可以用来解决界面上处理耗时操作时的卡顿。

可以提前把耗时操作存入数组,然后为主线程的RunLoop添加观察者CFRunLoopObserverRef,在观察者的回调方法中执行数组中的单个耗时操作,即每次RunLoop循环只处理一次耗时操作,这样单次循环的耗时变短,界面变得流畅。以下可做参考:

eiuYBzU.jpg!web

FzI7ZvU.jpg!web

本文从 App 能随时响应用户交互引出 RunLoop, 介绍了它的概念、作用及应用等,如果你决定使用 RunLoop ,它的启动很简单,如果想研究更多,可以对应源码去加深了解。

QFZjEr2.png!web

参考资料:

iOS底层原理总结 - 探寻RunLoop本质

《iOS性能优化实战》


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK