关于 runloop 面试中经常被问到:
讲讲 RunLoop,项目中有用到吗?
RunLoop内部实现逻辑?
Runloop和线程的关系?
timer 与 Runloop 的关系?
程序中添加每3秒响应一次的NSTimer,当拖动tableview时timer可能无法响应要怎么解决?
Runloop 是怎么响应用户操作的, 具体流程是什么样的?
说说RunLoop的几种状态?
Runloop的mode作用是什么?
一. RunLoop简介
运行循环,在程序运行过程中循环做一些事情,如果没有Runloop程序执行完毕就会立即退出,如果有Runloop程序会一直运行,并且时时刻刻在等待用户的输入操作。RunLoop可以在需要的时候自己跑起来运行,在没有操作的时候就停下来休息。充分节省CPU资源,提高程序性能。
定义
RunLoop的实质是一个死循环,用于保证程序的持续运行,只有当程序退出的时候才会结束(由main函数开启主线程的RunLoop)作用
保持程序的持续运行
处理App中的各种事件(触摸、定时器、Selector事件)
节省CPU资源,提高程序性能(该做事做事,没事做休息)获取方法
使用NSRunLoop(面向对象)或者CFRunLoopRef(底层C语言)
在任何一个Cocoa程序的线程中,都可以通过:
NSRunLoop *runloop = [NSRunLoopcurrentRunLoop];
来获取到当前线程的run loop。原理
RunLoop开启一个循环事件,并接受输入事件,接受的事件来自两种不同的来源:
1.输入源(input source)(传递异步事件)
2.定时源(timer source)(传递同步事件)
RunLoop接收到消息后采用handlePort、customSrc、mySelector和timerFired等四个方法处理对应的事件
当RunLoop没有接收到消息时,则进入休眠状态,以保持程序持续运行。应用范畴:
1.定时器(Timer)
2.PerformSelector
3.GCD Async Main Queue
4.事件响应、手势识别、界面刷新
5.网络请求 √ AutoreleasePoolRunLoop在实际开中的应用
1.控制线程生命周期(线程保活)
2.解决NSTimer在滑动时停止工作的问题
3.监控应用卡顿
4.性能优化运行逻辑
01、通知Observers:进入Loop 02、通知Observers:即将处理Timers 03、通知Observers:即将处理Sources 04、处理Blocks 05、处理Source0(可能会再次处理Blocks) 06、如果存在Source1,就跳转到第8步 07、通知Observers:开始休眠(等待消息唤醒) 08、通知Observers:结束休眠(被某个消息唤醒) 01> 处理Timer 02> 处理GCD Async To Main Queue 03> 处理Source1 09、处理Blocks 10、根据前面的执行结果,决定如何操作 01> 回到第02步 02> 退出Loop 11、通知Observers:退出LoopRunLoop的结构组成
RunLoop位于苹果的Core Foundation库中,而Core Foundation库则位于iOS架构分层的Core Service层中(值得注意的是,Core Foundation是一个跨平台的通用库,不仅支持Mac,iOS,同时也支持Windows):
- 六个被调起方法
主线程 (有 RunLoop 的线程) 几乎所有函数都从以下六个之一的函数调起:
CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION
CFRunloop is calling out to an abserver callback function
用于向外部报告 RunLoop 当前状态的更改,框架中很多机制都由 RunLoopObserver 触发,如 CAAnimation
CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK
CFRunloop is calling out to a block
消息通知、非延迟的perform、dispatch调用、block回调、KVO
CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE
CFRunloop is servicing the main desipatch queue
CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION
CFRunloop is calling out to a timer callback function
延迟的perform, 延迟dispatch调用
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION
CFRunloop is calling out to a source 0 perform function
处理App内部事件、App自己负责管理(触发),如UIEvent、CFSocket。普通函数调用,系统调用
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION
CFRunloop is calling out to a source 1 perform function
由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort
二. RunLoop基本作用
保持程序持续运行,程序一启动就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop,RunLoop保证主线程不会被销毁,也就保证了程序的持续运行
处理App中的各种事件(比如:触摸事件,定时器事件,Selector事件等)
节省CPU资源,提高程序性能,程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CUP,现在没有事情做,我要去休息,这时CUP就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情
我们先通过API内一张图片来简单看一下RunLoop内部运行原理
通过图片可以看出,RunLoop在跑圈过程中,当接收到Input sources 或者 Timer sources时就会交给对应的处理方去处理。当没有事件消息传入的时候,RunLoop就休息了。这里只是简单的理解一下这张图,接下来我们来了解RunLoop对象和其一些相关类,来更深入的理解RunLoop运行流程。
三. RunLoop在哪里开启
UIApplicationMain函数内启动了Runloop,程序不会马上退出,而是保持运行状态。因此每一个应用必须要有一个runloop,
我们知道主线程一开起来,就会跑一个和主线程对应的RunLoop,那么RunLoop一定是在程序的入口main函数中开启。
进入UIApplicationMain
我们发现它返回的是一个int数,那么我们对main函数做一些修改
运行程序,我们发现只会打印开始,并不会打印结束,这说明在UIApplicationMain函数中,开启了一个和主线程相关的RunLoop,导致UIApplicationMain不会返回,一直在运行中,也就保证了程序的持续运行。
我们来看到RunLoop的源码
// 用DefaultMode启动
voidCFRunLoopRun(void) {/* DOES CALLOUT */
int32_t result;
do{
result =CFRunLoopRunSpecific(CFRunLoopGetCurrent(),kCFRunLoopDefaultMode,1.0e10,false); CHECK_FOR_FORK();
}while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
我们发现RunLoop确实是do while通过判断result的值实现的。因此,我们可以把RunLoop看成一个死循环。如果没有RunLoop,UIApplicationMain函数执行完毕之后将直接返回,也就没有程序持续运行一说了。
四. RunLoop对象
Fundation框架 (基于CFRunLoopRef的封装)
NSRunLoop对象
CoreFoundation
CFRunLoopRef对象
因为Fundation框架是基于CFRunLoopRef的一层OC封装,这里我们主要研究CFRunLoopRef源码
如何获得RunLoop对象
Foundation[NSRunLoopcurrentRunLoop];// 获得当前线程的RunLoop对象
[NSRunLoopmainRunLoop];// 获得主线程的RunLoop对象Core
FoundationCFRunLoopGetCurrent();// 获得当前线程的RunLoop对象
CFRunLoopGetMain();// 获得主线程的RunLoop对象
RunLoop接收几种输入源,系统默认定义了几种模式?
- 输入源有两种
基于端口的输入源(port)
自定义的输入源(custom) - 系统定义的RunLoop模式有五种
最常用的有三种,如下所示:
1.NSDefaultRunLoopMode
默认模式,主线程中默认是NSDefaultRunLoopMode
2.UITrackingRunLoopMode
视图滚动模式,RunLoop会处于该模式下
3.NSRunLoopCommonModes
并不是真正意义上的Mode,是一个占位用的“Mode”,默认包含了NSDefaultRunLoopMode和UITrackingRunLoopMode两种模式
RunLoop模式的原理和使用注意点?
原理和注意点
- 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source、Observer、Timer(如下图所示)
- 每次RunLoop启动,只能指定一个Mode,这个Mode被称为CurrentMode
- 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入, 以使不同组之间的Source、Observer、Timer互不受影响
在 CoreFoundation 里面关于 RunLoop 有5个类:
CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef
其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装
一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。
CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。
• Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
• Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。
CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
CFRunLoopObserverRef 是观察者,每个 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 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。
通过上面分析我们知道,CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer,而RunLoop启动时只能选择其中一个Mode作为currentMode。
Source1/Source0/Timers/Observer分别代表什么
Source1 : 基于Port的线程间通信
Source0 : 触摸事件,PerformSelectors
我们通过代码验证一下
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
NSLog(@"点击了屏幕");
}
打断点之后打印堆栈信息,当xcode工具区打印的堆栈信息不全时,可以在控制台通过“bt”指令打印完整的堆栈信息,由堆栈信息中可以发现,触摸事件确实是会触发Source0事件。
同样的方式验证performSelector堆栈信息
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
});
可以发现PerformSelectors同样是触发Source0事件
- Timers : 定时器,NSTimer
通过代码验证
[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
NSLog(@"NSTimer ---- timer调用了");
}];
打印完整堆栈信息
- Observer : 监听器,用于监听RunLoop的状态
Source
即可以唤醒Runloop
的一些事件。比如用户点击了屏幕,就会创建一个input source。
-
source0
: 非系统事件
只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
-
source1
: 系统事件
包含了一个 mach_port和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程
Timer
我们经常用的NSTimer
就属于这一类。
Observer
某个observer可以监听runloop
的状态变化,并作出一定反应。
RunLoop运行流程
RunLoop 结构组成
RunLoop位于苹果的Core Foundation库中,而Core Foundation库则位于iOS架构分层的Core Service层中(值得注意的是,Core Foundation是一个跨平台的通用库,不仅支持Mac,iOS,同时也支持Windows):
五. RunLoop和线程间的关系
每条线程都有唯一的一个与之对应的RunLoop对象
RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建
RunLoop在第一次获取时创建,在线程结束时销毁
通过源码查看上述对应
// 拿到当前Runloop 调用_CFRunLoopGet0CFRunLoopRefCFRunLoopGetCurrent(void) { CHECK_FOR_FORK();
CFRunLoopRefrl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);if(rl)returnrl;return_CFRunLoopGet0(pthread_self());
}
// 查看_CFRunLoopGet0方法内部CF_EXPORTCFRunLoopRef_CFRunLoopGet0(pthread_t t) {if(pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);if(!__CFRunLoops) { __CFUnlock(&loopsLock);CFMutableDictionaryRefdict =CFDictionaryCreateMutable(kCFAllocatorSystemDefault,0,NULL, &kCFTypeDictionaryValueCallBacks);
// 根据传入的主线程获取主线程对应的RunLoop
CFRunLoopRefmainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主线程 将主线程-key和RunLoop-Value保存到字典中
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if(!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void*volatile*)&__CFRunLoops)) {CFRelease(dict);
}CFRelease(mainLoop); __CFLock(&loopsLock);
}
// 从字典里面拿,将线程作为key从字典里获取一个loop
CFRunLoopRefloop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
// 如果loop为空,则创建一个新的loop,所以runloop会在第一次获取的时候创建
if(!loop) {
CFRunLoopRefnewLoop = __CFRunLoopCreate(t); __CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
// 创建好之后,以线程为key runloop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runloop
if(!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop); loop = newLoop;
}
// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if(pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void*)loop,NULL);
if(0== _CFGetTSD(__CFTSDKeyRunLoopCntr)) { _CFSetTSD(__CFTSDKeyRunLoopCntr, (void*)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void(*)(void*))__CFFinalizeRunLoop);
}
}
returnloop;
}
从上面的代码可以看出,线程和 RunLoop 之间是一一对应的,其关系是保存在一个 Dictionary 里。所以我们创建子线程RunLoop时,只需在子线程中获取当前线程的RunLoop对象即可[NSRunLoop currentRunLoop];如果不获取,那子线程就不会创建与之相关联的RunLoop,并且只能在一个线程的内部获取其 RunLoop
[NSRunLoop currentRunLoop];方法调用时,会先看一下字典里有没有存子线程相对用的RunLoop,如果有则直接返回RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。当线程结束时,RunLoop会被销毁。
NSTimer和RunLoop的关系?
- NSTimer需要添加到Runloop中, 才能执行的情况
NSTimer *timer = [NSTimer timerWithTimeInterval:1.f target:self selector:@selector(update) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
- NSTimer默认被添加到Runloop中, 直接执行的情况
[NSTimer scheduledTimerWithTimeInterval:1.f target:self selector:@selector(update) userInfo:nil repeats:YES];
NSTimer准确吗,如果不准确,如何设计一个准确的timer?
不准确
准确的Timer应该和当前线程的RunLoopMode保持一致
TableView/ScrollView/CollectionView滚动时为什么NSTimer会停止?
一个RunLoop不能同时共存两个mode
当滚动视图滚动时,当前RunLoop处于UITrackingRunLoopMode,
NSTimer的RunLoopMode和当前线程的RunLoopMode不一致,所以会停止
解决方法:将timer的runloopMode改为UITrackingRunLoopMode或者NSRunLoopCommonModes
如果NSTimer在分线程中创建,会发生什么,应该注意什么?
- NSTimer没有启动
-- 在主线程中,系统默认创建并启动主线程的runloop
-- 在分线程中,系统不会自动启动runloop,需要手动启动 - 解决方法:
启动分线程的runLoop
在异步线程中下载很多图片,如果失败了,该如何处理?请结合RunLoop来谈谈解决方案
在异步线程中启动一个RunLoop重新发送网络请求,下载图片
如果程序启动就需要执行一个耗时操作,你会怎么做?
开启一个异步的子线程,并启动它的RunLoop来执行该耗时操作
runloop与autoreleasepool的关系,如果在分线程中启动一个异步请求,会有什么问题?
判断其是否请求结束,如果未结束,要保持当前线程一直启动,直到结束
while(!isFinish)
{
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
}
可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
程序启动时,runloop是如何工作的?如果程序启动就需要执行一个耗时操作,你会怎么做?
程序启动时,系统默认创建并启动主线程的runloop,runloop会默认创建两个Observe来进行监听runloop的进出和睡眠,有事情的时候就去做,没事的休眠。
(线程(创建)-->runloop将进入-->最高优先级OB创建释放池-->runloop将睡-->最低优先级OB销毁旧池创建新池-->runloop将退出-->最低优先级OB销毁新池-->线程(销毁))
线程刚创建时并没有runloop,如果你不主动去获取,那么一直都不会有。
耗时操作可以放在分线程中进行,结束后回到主线程。
经典面试题
Runloop和线程是什么关系?
每条线程都有唯一的一个与之对应的RunLoop对象,其关系是保存在一个全局的 Dictionary 里;主线程的RunLoop已经自动创建,子线程的RunLoop需要主动创建;RunLoop在第一次获取时创建,在线程结束时销毁
Runloop的mode作用是什么?
指定事件在运行循环中的优先级的,
线程的运行需要不同的模式,去响应各种不同的事件,去处理不同情境模式。(比如可以优化tableview的时候可以设置UITrackingRunLoopMode下不进行一些操作,比如设置图片等。)
以+scheduledTimerWithTimeInterval:的方式触发的timer,在滑动页面上的列表时,timer会暂停回调, 为什么?
滑动scrollView时,主线程的RunLoop会切换到UITrackingRunLoopMode这个Mode,执行的也是UITrackingRunLoopMode下的任务(Mode中的item),而timer是添加在NSDefaultRunLoopMode下的,所以timer任务并不会执行,只有当UITrackingRunLoopMode的任务执行完毕,runloop切换到NSDefaultRunLoopMode后,才会继续执行timer。
如何解决在滑动页面上的列表时,timer会暂停回调?
将Timer放到NSRunLoopCommonModes中执行即可
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; [[NSRunLoop currentRunLoop] run];复制代码
NSTImer使用时需要注意什么?
注意timer添加到runloop时应该设置为什么mode
注意timer在不需要时,一定要调用invalidate方法使定时器失效,否则得不到释放
RunLoop 有哪些应用?
常驻内存、AutoreleasePool 自动释放池
AutoreleasePool 和 RunLoop 有什么联系?
iOS应用启动后会注册两个 Observer 管理和维护 AutoreleasePool。应用程序刚刚启动时默认注册了很多个Observer,其中有两个Observer的 callout 都是 _ wrapRunLoopWithAutoreleasePoolHandler,这两个是和自动释放池相关的两个监听。
第一个 Observer 会监听 RunLoop 的进入,它会回调objc_autoreleasePoolPush() 向当前的 AutoreleasePoolPage 增加一个哨兵对象标志创建自动释放池。这个 Observer 的 order 是 -2147483647 优先级最高,确保发生在所有回调操作之前。
第二个 Observer 会监听 RunLoop 的进入休眠和即将退出 RunLoop 两种状态,在即将进入休眠时会调用 objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出 RunLoop 时会调用objc_autoreleasePoolPop() 释放自动自动释放池内对象。这个Observer 的 order 是 2147483647 ,优先级最低,确保发生在所有回调操作之后。
NSRunLoop 和 CFRunLoopRef 区别
CFRunLoopRef 基于C 线程安全,NSRunLoop 基于 CFRunLoopRef 面向对象的API 是不安全的