美文网首页
iOS RunLoop

iOS RunLoop

作者: 齐玉婷 | 来源:发表于2019-09-25 16:11 被阅读0次

    关于 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<UITouch *> *)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

          本文链接:https://www.haomeiwen.com/subject/xrdructx.html