李洪强经典面试题145-Runloop
Runloop
什么是 Runloop?
- 从字面上讲就是运行循环。
- 它内部就是do-while循环,在这个循环内部不断地处理各种任务。
- 一个线程对应一个RunLoop,主线程的RunLoop默认已经启动,子线程的RunLoop得手动启动(调用run方法)
RunLoop只能选择一个Mode启动,如果当前Mode中没有任何Source(Sources0、Sources1)、Timer,那么就直接退出RunLoop
基本的作用就是保持程序的持续运行,处理app中的各种事件。通过runloop,有事运行,没事就休息,可以节省cpu资源,提高程序性能。
Runloop对象
iOS中有2套API来访问和使用RunLoop
Foundation:NSRunLoop
Core Foundation:CFRunLoopRef
NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API。
Runloop与线程
每条线程都有唯一的一个与之对应的RunLoop对象
主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建
RunLoop在第一次获取时创建,在线程结束时销毁
获得RunLoop对象
- Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
- Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
RunLoop相关类
Core Foundation中关于RunLoop的5个类
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的运行模式。
一个RunLoop包含若干个Mode,每个Mode又包含若干个(set)Source/(array)Timer/(array)Observer
每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作CurrentMode
如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入
这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
mode主要是用来指定事件在运行循环中的优先级的,分为:
• NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
• UITrackingRunLoopMode:ScrollView滑动时会切换到该Mode
• UIInitializationRunLoopMode:run loop启动时,会切换到该mode
• NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
苹果公开提供的Mode有两个:
• NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
• NSRunLoopCommonModes(kCFRunLoopCommonModes)
CFRunLoopTimerRef
CFRunLoopTimerRef是基于时间的触发器
CFRunLoopTimerRef基本上说的就是NSTimer,它受RunLoop的Mode影响
GCD的定时器不受RunLoop的Mode影响
CFRunLoopSourceRef
CFRunLoopSourceRef是事件源(输入源)
按照官方文档,Source的分类
Port-Based Sources
Custom Input Sources
Cocoa Perform Selector Sources
按照函数调用栈,Source的分类
Source0:非基于Port的
Source1:基于Port的,通过内核和其他线程通信,接收、分发系统事件
CFRunLoopObserverRef
CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
-
可以监听的时间点有以下几个
kcfRunLoopEntry(即将进入loop)//1
kcfRunLoopBeforeTimers(即将处理timer)//2
kcfRunLoopBeforeSources(即将处理source)//4
kcfRunLoopBeforeWaiting(即将进入休眠)//32
kcfRunLoopAfterWaiting(刚从休眠中唤醒)//64
kcfRunLoopExit(即将退出loop)//128
添加Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
// 释放Observer
CFRelease(observer);
RunLoop处理逻辑
通知Observer:即将进入Loop(1)
通知Observer:将要处理Timer(2)
通知Observer:将要处理Source0(3)
处理Source0(4)
如果有Source0,跳到第9步(5)
通知Observer:线程即将休眠(6)
-
休眠,等待唤醒:(7)
Source0(port)。
timer启动
RunLoop设置的timer已经超时
Runloop被外部手动唤醒
通知Observer:线程将被唤醒(8)
-
处理未处理的时间(9)
如果用户定义的定时器启动,处理定时器事件并重启Runloop。进入步骤2.
如果输入源启动,传递相应的消息。
如果RunLopp被显式唤醒而且时间还没超时,重启RunLoop,进入步骤2.
- 通知Observer:即将退出Loop
Runloop的应用
- NSTimer
- ImageView显示
- PerformSelector
- 常驻线程
- 自动释放池
runloop定时源和输入源
Runloop处理的输入事件有两种不同的来源:输入源(input source)和定时源(timer source)
输入源传递异步消息,通常来自于其他线程或者程序。
-
定时源则传递同步消息,在特定时间或者一定的时间间隔发生
NSRunLoop的实现机制,及在多线程中如何使用
```
实现机制:回答runloop的基本作用,处理逻辑,前面都有。
程序创建子线程的时候,才需要手动启动runloop。主线程的runloop已经默认启动。
在多线程中,你需要判断是否需要runloop。如果需要runloop,那么你要负责配置runloop并启动。你不需要在任何情况下都去启动runloop。比如,你使用线程去处理一个预先定义好的耗时极长的任务时,你就可以无需启动runloop。Runloop只在你要和线程有交互时才需要
####runloop和线程有什么关系?
主线程的run loop默认是启动的。
iOS的应用程序里面,程序启动后会有一个如下的main()函数
( argc, * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, , NSStringFromClass([AppDelegate class]));
}
}
重点是UIApplicationMain()函数,这个方法会为main thread设置一个NSRunLoop对象,这就解释了:为什么我们的应用可以在无人操作的时候休息,需要让它干活的时候又能立马响应。对其它线程来说,runloop默认是没有启动的,runloop只在你要和线程有交互时才需要。
在任何一个 Cocoa 程序的线程中,都可以通过以下代码来获取到当前线程的 run loop 。
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
```
autorelease 对象在什么情况下会被释放?
- 分两种情况:手动干预释放和系统自动释放
- 手动干预释放就是指定autoreleasepool,当前作用域大括号结束就立即释放
- 系统自动去释放:不手动指定autoreleasepool,Autorelease对象会在当前的 runloop 迭代结束时释放
- kCFRunLoopEntry(1):第一次进入会自动创建一个autorelease
- kCFRunLoopBeforeWaiting(32):进入休眠状态前会自动销毁一个autorelease,然后重新创建一个新的autorelease
- kCFRunLoopExit(128):退出runloop时会自动销毁最后一个创建的autorelease
对于runloop的理解不正确的是
A 每一个线程都有其对应的RunLoop
B 默认非主线程的RunLoop是没有运行的
C 在一个单独的线程中没有必要去启用RunLoop
D 可以将NSTimer添加到runloop中
- 参考答案:C
- 理由:说到RunLoop,它可是多线程的法定。通常来说,一个线程一次只能执行一个任务,执行完任务后就会退出线程。但是,对于主线程是不能退出的,因此我们需要让主线程即时任务执行完毕,也可以继续等待是接收事件而不退出,那么RunLoop就是关键法宝了。但是非主线程通常来说就是为了执行某一任务的,执行完毕就需要归还资源,因此默认是不运行RunLoop的。NSRunLoop提供了一个添加NSTimer的方法,这个方法是在应用正常状态下会回调。
runloop的mode作用是什么?
mode主要是用来指定事件在运行循环中的优先级的,分为:
• NSDefaultRunLoopMode(kCFRunLoopDefaultMode):默认,空闲状态
• UITrackingRunLoopMode:ScrollView滑动时会切换到该Mode
• UIInitializationRunLoopMode:run loop启动时,会切换到该mode
• NSRunLoopCommonModes(kCFRunLoopCommonModes):Mode集合
苹果公开提供的Mode有两个:
• NSDefaultRunLoopMode(kCFRunLoopDefaultMode)
• NSRunLoopCommonModes(kCFRunLoopCommonModes)
如果我们把一个NSTimer对象以NSDefaultRunLoopMode(kCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度。当我们滚动的时候,也希望不调度,那就应该使用默认模式。但是,如果希望在滚动时,定时器也要回调,那就应该使用common mode。
请写出NSTimer使用时的注意事项(两项即可)
思路和上一题一样,如果想要销毁timer,则必须先将timer置为失效,否则timer就一直占用内存而不会释放。造成逻辑上的内存泄漏。该泄漏不能用xcode及instruments测出来。 另外对于要求必须销毁timer的逻辑处理,未将timer置为失效,若每次都创建一次,则之前的不能得到释放,则会同时存在多个timer的实例在内存中。
参考答案:
• 注意timer添加到runloop时应该设置为什么mode
• 注意timer在不需要时,一定要调用invalidate方法使定时器失效,否则得不到释放
UITableViewCell上有个UILabel,显示NSTimer实现的秒表时间,手指滚动cell过程中,label是否刷新,为什么?
和上一题一样的思路,如果要cell滚动过程中定时器正常回调,UI正常刷新,那么要将timer放入到CommonModes下,因为是NSDefaultRunLoopMode,只有在空闲状态下才会回调。
为什么 UIScrollView 的滚动会导致 NSTimer 失效?
- 思路和上一题一样,解决办法有2个,一个是更改mode为NSRunLoopCommonModes(无论runloop运行在哪个mode,都能运行),还有种办法是切换到主线程来更新UI界面的刷新
//将timer添加到NSDefaultRunLoopMode中
[NSTimer scheduledTimerWithTimeInterval: target: selector:@selector(timerTick:) userInfo: repeats:];
//然后再添加到NSRunLoopCommonModes里
NSTimer *timer = [NSTimer timerWithTimeInterval: target: selector:@selector(timerTick:) userInfo: repeats:];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
在滑动页面上的列表时,timer会暂定回调,为什么?如何解决?
- 思路和上一题一样
在开发中如何使用RunLoop?什么应用场景?
- 开启一个常驻线程(让一个子线程不进入消亡状态,等待其他线程发来消息,处理其他事件)
- 在子线程中开启一个定时器
在子线程中进行一些长期监控
可以控制定时器在特定模式下执行
可以让某些事件(行为、任务)在特定模式下执行
可以添加Observer监听RunLoop的状态,比如监听点击事件的处理(在所有点击事件之前做一些事情)
文章如有问题,请留言,我将及时更正。