美文网首页
runloop 面试题

runloop 面试题

作者: 开了那么 | 来源:发表于2020-09-07 11:43 被阅读0次

    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

      1. kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
      2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
      3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
      4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
      5. 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调用方法主要集中在kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting之间,有人可能会问kCFRunLoopAfterWaiting之后也有一些方法调用,为什么不监测呢,我的理解,大部分导致卡顿的的方法是在kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting之间,比如source0主要是处理App内部事件,App自己负责管理(出发),如UIEvent(Touch事件等,GS发起到RunLoop运行再到事件回调到UI)、CFSocketRef。开辟一个子线程,然后实时计算 kCFRunLoopBeforeSourceskCFRunLoopAfterWaiting 两个状态区域之间的耗时是否超过某个阀值,来断定主线程的卡顿情况。

    - (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 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

    参考文章:

    深入理解RunLoop

    iOS卡顿监测方案总结

    相关文章

      网友评论

          本文标题:runloop 面试题

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