runloop 面试题
runloop 面试题
基于最近的几次面试,整理了runloop 的相关知识
-
1、Runloop 是什么?
Run loops是线程相关的的基础框架的一部分。一个run loop就是一个事件处理的循环,用来不停的调度工作以及处理输入事件。其实内部就是do-while循环,这个循环内部不断地处理各种任务(比 如Source,Timer,Observer)。使用run loop的目的是让你的线程在有工作的时候忙于工作,而没工作的时候处于休眠状态。
CFRunLoopRef 的代码是开源的,你可以在这里 http://opensource.apple.com/tarballs/CF/ 下载到整个 CoreFoundation 的源码来查看。 -
2、NSRunLoop 和 CFRunLoopRef?
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
主线程的run loop默认是启动的,子线程的runloop 是手动创建的,创建与子线程相关联的RunLoop, 苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数,
所以我们不能在一个线程中去操作另外一个线程的runloop,那很可能会造成意想不到的后果。但是CoreFundation中的不透明类CFRunLoopRef是线程安全的,而且两种类型的run loop完全可以混合使用。
Foundation
[NSRunLoop currentRunLoop]; 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; 获得主线程的RunLoop对象
Core Foundation
CFRunLoopGetCurrent(); 获得当前线程的RunLoop对象
CFRunLoopGetMain(); 获得主线程的RunLoop对象
底层代码:(基于CF-1151.16-CFRunLoop.c)
// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;
// 访问 loopsDic 时的锁
static CFLock_t loopsLock = CFLockInit;
//从一个字典里面获取runloop,CFDictionaryGetValue
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
//如果runloop 不存在
if (!loop) {
//__CFRunLoopCreate 创建一个runloop
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
//
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
//重新给字典赋值 key : pthreadPointer(t) value :newLoop
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)
- 2、Runloop 包含哪些类
- CFRunLoopRef
- CFRunLoopModeRef
- CFRunLoopSourceRef
- CFRunLoopTimerRef
- CFRunLoopObserverRef
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes; _commonModes是一个可变的集合,集合中存放了许多mode。RunLoop在运行的时候只能选择一个Mode作为当前RunLoop执行的状态 == _currentMode。
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode; 它是一个__CFRunLoopMode类型的结构体指针。RunLoop通过它来表征RunLoop的运行状态。
CFMutableSetRef _modes; //数组集合。(CFRunLoopModeRef)
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFAbsoluteTime _runTime;
CFAbsoluteTime _sleepTime;
CFTypeRef _counterpart;
};
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _sources0; //承载CFRunLoopSourceRef 对象
CFMutableSetRef _sources1; //承载CFRunLoopSourceRef 对象
CFMutableArrayRef _observers; //承载CFRunLoopObserverRef
CFMutableArrayRef _timers; //承载CFRunLoopTimerRef
};
其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:
image image
一个 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会被唤醒以执行那个回调。
//主要作用
- NSTimer
- performSelector:withObject:afterDelay:
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
NSLog(@"---");
[self performSelector:@selector(test) withObject:nil afterDelay:0.0];
}
-(void)test{
NSLog(@“111-");
}
CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。
//主要作用
- 用于监听RunLoop的状态
- UI刷新(BeforeWaiting)
- Autorelease pool(BeforeWaiting)
可以观测的时间点有以下几个
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 会直接退出,不进入循环。
-
3、Runloop 有哪些mode
Runloop有5种常见掌握mode
- kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
- UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
- UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
- GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
- kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。默认包括:kCFRunLoopDefaultMode、UITrackingRunLoopMode
当需要设置监听runtime 的时候
设置监听时,
CFRunLoopMode mode 参数可以传 kCFRunLoopDefaultMode,UITrackingRunLoopMode,或者kCFRunLoopCommonModes,(默认包括:kCFRunLoopDefaultMode、UITrackingRunLoopMode两种模式)
CFRunLoopAddObserver(<#CFRunLoopRef rl#>, <#CFRunLoopObserverRef observer#>, <#CFRunLoopMode mode#>)
-
4、Runloop 的运行过程
循环过程、运行逻辑
image
可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。
- 5、子线程的Runloop 如何启动
调用 [NSRunLoop currentRunLoop] 方法
以Timer 为例
占位模式:common modes标记
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
-
6、relasePool 什么时候释放
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用_objc_autoreleasePoolPush()
创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()
和_objc_autoreleasePoolPush()
释放旧的池并创建新池;Exit(即将退出Loop) 时调用_objc_autoreleasePoolPop()
来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。 -
7、dispatch after 方法在子线程中是否生效,原因
不生效,因为 dispatch_after 底层依赖一个Timer ,Timer 依赖runloop,子线程没有runloop, -
8、Timer
NSTimer 其实就是 CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个 NSTimer 注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer 有个属性叫做 Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。
CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop -
9、怎么检测页面的卡顿
FPS (Frames Per Second) 是图像领域中的定义,表示每秒渲染帧数,通常用于衡量画面的流畅度,每秒帧数越多,则表示画面越流畅,60fps 最佳,一般我们的APP的FPS 只要保持在 50-60之间,用户体验都是比较流畅的。其实FPS中CADisplayLink的使用也是基于RunLoop,都依赖main RunLoop。
简版的RunLoop的代码
// 1.进入loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled)
// 2.RunLoop 即将触发 Timer 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 3.RunLoop 即将触发 Source0 (非port) 回调。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 4.RunLoop 触发 Source0 (非port) 回调。
sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle)
// 5.执行被加入的block
__CFRunLoopDoBlocks(runloop, currentMode);
// 6.RunLoop 的线程即将进入休眠(sleep)。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
// 7.调用 mach_msg 等待接受 mach_port 的消息。线程将进入休眠, 直到被下面某一个事件唤醒。
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort)
// 进入休眠
// 8.RunLoop 的线程刚刚被唤醒了。
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting
// 9.如果一个 Timer 到时间了,触发这个Timer的回调
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
// 10.如果有dispatch到main_queue的block,执行bloc
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
// 11.如果一个 Source1 (基于port) 发出事件了,处理这个事件
__CFRunLoopDoSource1(runloop, currentMode, source1, msg);
// 12.RunLoop 即将退出
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
我们可以看到RunLoop调用方法主要集中在kCFRunLoopBeforeSources
和kCFRunLoopAfterWaiting
之间,有人可能会问kCFRunLoopAfterWaiting
之后也有一些方法调用,为什么不监测呢,我的理解,大部分导致卡顿的的方法是在kCFRunLoopBeforeSources
和kCFRunLoopAfterWaiting
之间,比如source0主要是处理App内部事件,App自己负责管理(出发),如UIEvent(Touch事件等,GS发起到RunLoop运行再到事件回调到UI)、CFSocketRef。开辟一个子线程,然后实时计算 kCFRunLoopBeforeSources
和 kCFRunLoopAfterWaiting
两个状态区域之间的耗时是否超过某个阀值,来断定主线程的卡顿情况。
- (void)start
{
if (observer)
return;
// 信号
semaphore = dispatch_semaphore_create(0);
// 注册RunLoop状态观察
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 在子线程监控时长
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (st != 0)
{
if (!observer)
{
timeoutCount = 0;
semaphore = 0;
activity = 0;
return;
}
if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
{
if (++timeoutCount < 5)
continue;
PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD
symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
NSData *data = [crashReporter generateLiveReport];
PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter
withTextFormat:PLCrashReportTextFormatiOS];
NSLog(@"------------\n%@\n------------", report);
}
}
timeoutCount = 0;
}
});
}
当然还有其他别的方法可以用来检测页面卡顿,这里就不在介绍,大家可以参看文章iOS卡顿监测方案总结
- 9、就是UIButton点击事件打印堆栈看的话是从source0调出的,文中说的是source1事件,不知道哪个是正确的呢?
首先是由那个Source1 接收IOHIDEvent,之后在回调 __IOHIDEventSystemClientQueueCallback() 内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。所以UIButton事件看到是在 Source0 内的。可以在 __IOHIDEventSystemClientQueueCallback 处下一个 Symbolic Breakpoint 看一下。
-
10、 线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有”,如果我一直不获取runloop的话,这个线程就不能处理事件吗?
线程创建时有入口函数,线程里能做的东西都是在那个函数里的。RunLoop也好你自己的逻辑也好,都是在那个函数里完成的。你要是不用RunLoop的话,也可以自己实现一个类似的机制来处理你定义的事件。 -
11、手势识别
当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。
参考文章: