iOS RunLoop

关于 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.网络请求 √ AutoreleasePool

  • RunLoop在实际开中的应用
    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:退出Loop

  • RunLoop的结构组成

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内部运行原理

通过图片可以看出,RunLoop在跑圈过程中,当接收到Input sources 或者 Timer sources时就会交给对应的处理方去处理。当没有事件消息传入的时候,RunLoop就休息了。这里只是简单的理解一下这张图,接下来我们来了解RunLoop对象和其一些相关类,来更深入的理解RunLoop运行流程。

三. RunLoop在哪里开启

UIApplicationMain函数内启动了Runloop,程序不会马上退出,而是保持运行状态。因此每一个应用必须要有一个runloop,

我们知道主线程一开起来,就会跑一个和主线程对应的RunLoop,那么RunLoop一定是在程序的入口main函数中开启。

Runloop入口

进入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 的接口进行了封装

RunLoopMode

一个 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分别代表什么

  1. Source1 : 基于Port的线程间通信

  2. Source0 : 触摸事件,PerformSelectors

我们通过代码验证一下


- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    NSLog(@"点击了屏幕");
}

打断点之后打印堆栈信息,当xcode工具区打印的堆栈信息不全时,可以在控制台通过“bt”指令打印完整的堆栈信息,由堆栈信息中可以发现,触摸事件确实是会触发Source0事件。

touchesBegan堆栈信息

同样的方式验证performSelector堆栈信息

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    [self performSelectorOnMainThread:@selector(test) withObject:nil waitUntilDone:YES];
});

可以发现PerformSelectors同样是触发Source0事件

performSelector堆栈信息
  1. Timers : 定时器,NSTimer

通过代码验证


[NSTimer scheduledTimerWithTimeInterval:3.0 repeats:NO block:^(NSTimer * _Nonnull timer) {
    NSLog(@"NSTimer ---- timer调用了");
}];

打印完整堆栈信息

Timer 堆栈信息
  1. 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 是不安全的

你可能感兴趣的:(iOS RunLoop)