RunLoop保证了程序不会在最后一行代码执行结束后立即退出程序,它保证了程序的持续运行,因为只有程序在运行阶段才可以接收和处理各种事件。
一、什么是RunLoop?
RunLoop,顾名思义,运行循环的意思,在程序运行过程中循环做一些事情。
应用范畴:
- 定时器(Timer)、PerformSelector
- GCD Async Main Queue
- 事件响应、手势识别、界面刷新
- 网络请求
- AutoreleasePool
如果没有RunLoop,执行完NSLog
代码后,会即将退出程序。
1 | int main(int argc, const char * argv[]) { |
如果有了RunLoop,程序并不会马上退出,而是保持运行状态。
1 | // 正常iOS程序的入口 |
UIApplicationMain()
函数内部创建了RunLoop对象,保证了程序的持续运行。
RunLoop的基本作用:
- 保持程序的持续运行
- 处理App中的各种事件(比如触摸事件、定时器事件等)
- 节省CPU资源,提高程序性能:该做事时做事,该休息时休息
- ……
二、RunLoop对象
iOS中有2套API来访问和使用RunLoop:
- Foundation框架:NSRunLoop(OC)
1 | // 获得当前线程的RunLoop对象 |
- Core Foundation框架:CFRunLoopRef(C)
1 | // 获得当前线程的RunLoop对象 |
NSRunLoop和CFRunLoopRef都代表着RunLoop对象。NSRunLoop是基于CFRunLoopRef的一层OC包装(两个框架打印RunLoop对象的地址是不一样的),CFRunLoopRef是开源的(https://opensource.apple.com/tarballs/CF/)。
RunLoop与线程的关系:
每条线程都有唯一的一个与之对应的RunLoop对象。
RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value。
线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建,RunLoop会在线程结束时销毁。
主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop。
三、RunLoop相关的类
Core Foundation中关于RunLoop的5个类:
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
以下是CFRunLoop的源码(实际内容有很多,仅摘取了有用部分):
1 | typedef struct __CFRunLoop * CFRunLoopRef; |
3.1. CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的运行模式。一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer。
RunLoop启动时只能选择其中一个Mode,作为currentMode。如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入。
不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响。如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。
常见的2种Mode:
kCFRunLoopDefaultMode
(对应OC的NSDefaultRunLoopMode
):App的默认Mode,通常主线程是在这个Mode下运行。UITrackingRunLoopMode
:界面跟踪 Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响(界面滑动时自动切换到该Mode,滑动结束后会退出该Mode到默认的Mode)。
注意:NSDefaultRunLoopMode
和UITrackingRunLoopMode
才是真正存在的模式。NSRunLoopCommonModes
并不是一个真的模式,它只是一个标记。被标记为通用模式的RunLoop,默认和滑动两种模式都支持。
3.2. CFRunLoopObserverRef
RunLoop的状态:
1 | /* Run Loop Observer Activities */ |
添加Observer监听RunLoop的所有状态(添加RunLoop的Observer只有C语言实现,没有OC版本):
1 | // 创建Observer |
添加Observer时传入的Mode如果是kCFRunLoopCommonModes
,会同时监听普通模式和滚动模式。
四、RunLoop的运行逻辑
Source0:
- 触摸事件处理
performSelector:onThread:
Source1
- 基于Port的线程间通信
- 系统事件捕捉(注意:触摸事件的捕捉是在Source1,处理是在Source0)
Timers
- NSTimer
performSelector:withObject:afterDelay:
(底层实现就是一个定时器)
Observers
- 用于监听RunLoop的状态
- UI刷新(BeforeWaiting,RunLoop进入休眠前执行UI刷新操作)
- Autorelease pool(BeforeWaiting)
RunLoop休眠的实现原理:
用户态如果需要休眠,会进入到内核态的mach_msg()
函数,在这个函数内部会自动让内核进入休眠状态(等待消息:没有消息就让线程休眠,有消息就唤醒线程)。当有消息唤醒线程时,会自动切换到用户态,让RunLoop继续处理消息。
五、RunLoop在实际开中的应用
5.1. 控制线程生命周期(线程保活)
1 | @implementation DBThread |
打印结果:
1 | -[ViewController run] |
线程启动后,run
方法执行完线程就结束了。有什么办法能够让run方法执行完线程不死呢?可以使用RunLoop。
往RunLoop里面添加Source/Timer/Observer可以让线程方法不死。
1 | - (void)run { |
线程执行后的输出结果:-[ViewController run]
。线程没有死掉,end
也没有输出,程序会一直卡在[[NSRunLoop currentRunLoop] run];
,在这里RunLoop是等待唤醒的状态。[[NSRunLoop currentRunLoop] run]
的API解释说开启run
方法后RunLoop是无法停止的,它专门用于开启一个永不销毁的线程,run
方法底层就是在不断地调用runMode:beforeDate:
方法(开启一个while循环,不断地调用,相当于开启了很多个runMode:beforeDate:
方法)。CFRunLoopStop(CFRunLoopGetCurrent());
仅仅是停止一个runMode:beforeDate:
,所以线程不会死掉,也不会执行run方法后面的代码。
1 | @interface ViewController () |
执行结果:
1 | 进入控制器 |
注意:如果使用C语言RunLoop的API把线程添加到RunLoop中,是不需要做while循环的。CFRunLoopRunInMode(kCFRunLoopDefaultMode, 1.0e10, false);
最后一个参数代表执行完source后是否退出当前loop,传false就不需要stoped标记和while循环。
5.2. 解决NSTimer在滑动时停止工作的问题
RunLoop只能在一种模式下工作,滑动时是在TrackingMode模式下工作,滑动结束后会自动切换到默认模式,所以会发生滑动时定时器停止工作的问题。
要想解决这个问题,可以把Timer放在RunLoop的通用模式下执行,因为通用模式是可以在默认模式和滑动模式下都工作的。
1 | static int count = 0; |
注意:定时器要使用timerWithTimeInterval:
方法获取Timer对象,然后把Timer放到指定模式的RunLoop中。scheduledTimerWithTimeInterval:
是定制方法,会自动把Timer添加到默认模式中。
5.3. 监控应用卡顿
5.4. 性能优化
面试题1:讲讲 RunLoop,项目中有用到吗?
解答:参考章节一。
面试题2:runloop内部实现逻辑?
解答:参考章节四。
面试题3:runloop和线程的关系?
解答:参考章节二。
面试题4:timer与runloop 的关系?
解答:参考章节三中的结构图。
面试题5:程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
解答:把timer添加到RunLoop中,并指定模式为通用模式(包含了默认和滑动两种情况)。
面试题6:runloop 是怎么响应用户操作的, 具体流程是什么样的?
解答:在Source1里面捕捉系统消息后,把消息事件封装成EventQueue,最后放到Source0中进行处理。
面试题7:说说runLoop的几种状态
解答:参考章节3.2。
面试题8:runloop的mode作用是什么?
解答:参考章节3.1。