iOS面试题:RunLoop剖析
更多:iOS面试题大全
一、RunLoop概念
RunLoop是通过内部维护的事件循环(Event Loop)来对事件/消息进行管理的一个对象。
1、没有消息解决时,休眠已避免资源占用,由客户态切换到内核态(CPU-内核态和客户态)
2、有消息需要解决时,立刻被唤醒,由内核态切换到客户态
为什么main函数不会退出?
int main(int argc, char * argv[]) { @autoreleasepool { return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); }}UIApplicationMain内部默认开启了主线程的RunLoop,并执行了一段无限循环的代码(不是简单的for循环或者while循环)
//无限循环代码模式(伪代码)int main(int argc, char * argv[]) { BOOL running = YES; do { // 执行各种任务,解决各种事件 // ...... } while (running); return 0;}UIApplicationMain函数一直没有返回,而是不断地接收解决消息以及等待休眠,所以运行程序之后会保持持续运行状态。
二、RunLoop的数据结构
NSRunLoop(Foundation)是CFRunLoop(CoreFoundation)的封装,提供了面向对象的API
RunLoop 相关的主要涉及五个类:
CFRunLoop:RunLoop对象CFRunLoopMode:运行模式CFRunLoopSource:输入源/事件源CFRunLoopTimer:定时源CFRunLoopObserver:观察者
1、CFRunLoop
由pthread(线程对象,说明RunLoop和线程是逐个对应的)、currentMode(当前所处的运行模式)、modes(多个运行模式的集合)、commonModes(模式名称字符串集合)、commonModelItems(Observer,Timer,Source集合)构成
2、CFRunLoopMode
由name、source0、source1、observers、timers构成
3、CFRunLoopSource
分为source0和source1两种
source0:
即非基于port的,也就是客户触发的事件。需要手动唤醒线程,将当前线程从内核态切换到客户态source1:
基于port的,包含一个 mach_port 和一个回调,可监听系统端口和通过内核和其余线程发送的消息,能主动唤醒RunLoop,接收分发系统事件。
具有唤醒线程的能力
4、CFRunLoopTimer
基于时间的触发器,基本上说的就是NSTimer。在预设的时间点唤醒RunLoop执行回调。由于它是基于RunLoop的,因而它不是实时的(就是NSTimer 是不精确的。 由于RunLoop只负责分发源的消息。假如线程当前正在解决繁重的任务,就有可能导致Timer本次延时,或者者少执行一次)。
5、CFRunLoopObserver
监听以下时间点:CFRunLoopActivity
kCFRunLoopEntry
RunLoop准备启动kCFRunLoopBeforeTimers
RunLoop将要解决少量Timer相关事件kCFRunLoopBeforeSources
RunLoop将要解决少量Source事件kCFRunLoopBeforeWaiting
RunLoop将要进行休眠状态,即将由客户态切换到内核态kCFRunLoopAfterWaiting
RunLoop被唤醒,即从内核态切换到客户态后kCFRunLoopExit
RunLoop退出kCFRunLoopAllActivities
监听所有状态
6、各数据结构之间的联络
线程和RunLoop逐个对应, RunLoop和Mode是一对多的,Mode和source、timer、observer也是一对多的
image
三、RunLoop的Mode
关于Mode首先要知道一个RunLoop 对象中可能包含多个Mode,且每次调用 RunLoop 的主函数时,只能指定其中一个 Mode(CurrentMode)。切换 Mode,需要重新指定一个 Mode 。主要是为了分隔开不同的 Source、Timer、Observer,让它们之间互不影响。
image
当RunLoop运行在Mode1上时,是无法接受解决Mode2或者Mode3上的Source、Timer、Observer事件的
总共是有五种CFRunLoopMode:
kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行UITrackingRunLoopMode:跟踪客户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其余Mode影响)UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种处理方案
四、RunLoop的实现机制
image
这张图在网上流传比较广。
对于RunLoop而言最核心的事情就是保证线程在没有消息的时候休眠,在有消息时唤醒,以提高程序性能。RunLoop这个机制是依靠系统内核来完成的(苹果操作系统核心组件Darwin中的Mach)。
image
RunLoop通过mach_msg()函数接收、发送消息。它的本质是调用函数mach_msg_trap(),相当于是一个系统调用,会触发内核状态切换。在客户态调用 mach_msg_trap()时会切换到内核态;内核态中内核实现的mach_msg()函数会完成实际的工作。
即基于port的source1,监听端口,端口有消息就会触发回调;而source0,要手动标记为待解决和手动唤醒RunLoop
Mach消息发送机制
大致逻辑为:
1、通知观察者 RunLoop 即将启动。
2、通知观察者即将要解决Timer事件。
3、通知观察者即将要解决source0事件。
4、解决source0事件。
5、假如基于端口的源(Source1)准备好并处于等待状态,进入步骤9。
6、通知观察者线程即将进入休眠状态。
7、将线程置于休眠状态,由客户态切换到内核态,直到下面的任一事件发生才唤醒线程。
- 一个基于 port 的Source1 的事件(图里应该是source0)。
- 一个 Timer 到时间了。
- RunLoop 自身的超时时间到了。
- 被其余调用者手动唤醒。
8、通知观察者线程将被唤醒。
9、解决唤醒时收到的事件。
- 假如客户定义的定时器启动,解决定时器事件并重启RunLoop。进入步骤2。
- 假如输入源启动,传递相应的消息。
- 假如RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2
10、通知观察者RunLoop结束。
五、RunLoop与NSTimer
一个比较常见的问题:滑动tableView时,定时器还会生效吗?
默认情况下RunLoop运行在kCFRunLoopDefaultMode下,而当滑动tableView时,RunLoop切换到UITrackingRunLoopMode,而Timer是在kCFRunLoopDefaultMode下的,就无法接受解决Timer的事件。
怎样去处理这个问题呢?把Timer增加到UITrackingRunLoopMode上并不能处理问题,由于这样在默认情况下就无法接受定时器事件了。
所以我们需要把Timer同时增加到UITrackingRunLoopMode和kCFRunLoopDefaultMode上。
那么如何把timer同时增加到多个mode上呢?就要用到NSRunLoopCommonModes了
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];Timer就被增加到多个mode上,这样即便RunLoop由kCFRunLoopDefaultMode切换到UITrackingRunLoopMode下,也不会影响接收Timer事件
六、RunLoop和线程
- 线程和RunLoop是逐个对应的,其映射关系是保存在一个全局的 Dictionary 里
- 自己创立的线程默认是没有开启RunLoop的
1、怎样创立一个常驻线程?
1、为当前线程开启一个RunLoop(第一次调用 [NSRunLoop currentRunLoop]方法时实际是会先去创立一个RunLoop)
1、向当前RunLoop中增加一个Port/Source等维持RunLoop的事件循环(假如RunLoop的mode中一个item都没有,RunLoop会退出)
2、启动该RunLoop
@autoreleasepool { NSRunLoop *runLoop = [NSRunLoop currentRunLoop]; [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; [runLoop run]; }2、输出下边代码的执行顺序
NSLog(@"1");dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"2"); [self performSelector:@selector(test) withObject:nil afterDelay:10]; NSLog(@"3");});NSLog(@"4");- (void)test{ NSLog(@"5");}答案是1423,test方法并不会执行。
起因是假如是带afterDelay的延时函数,会在内部创立一个 NSTimer,而后增加到当前线程的RunLoop中。也就是假如当前线程没有开启RunLoop,该方法会失效。
那么我们改成:
dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"2"); [[NSRunLoop currentRunLoop] run]; [self performSelector:@selector(test) withObject:nil afterDelay:10]; NSLog(@"3"); });然而test方法仍然不执行。
起因是假如RunLoop的mode中一个item都没有,RunLoop会退出。即在调用RunLoop的run方法后,因为其mode中没有增加任何item去维持RunLoop的时间循环,RunLoop随即还是会退出。
所以我们自己启动RunLoop,肯定要在增加item后
dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"2"); [self performSelector:@selector(test) withObject:nil afterDelay:10]; [[NSRunLoop currentRunLoop] run]; NSLog(@"3"); });3、怎么保证子线程数据回来升级UI的时候不打断客户的滑动操作?
当我们在子请求数据的同时滑动浏览当前页面,假如数据请求成功要切回主线程升级UI,那么就会影响当前正在滑动的体验。
我们即可以将升级UI事件放在主线程的NSDefaultRunLoopMode上执行就可,这样就会等客户不再滑动页面,主线程RunLoop由UITrackingRunLoopMode切换到NSDefaultRunLoopMode时再去升级UI
[self performSelectorOnMainThread:@selector(reloadData) withObject:nil waitUntilDone:NO modes:@[NSDefaultRunLoopMode]];收录:原文地址
更多:iOS面试题大全
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » iOS面试题:RunLoop剖析