Runloop机制解析及应用

- Runloop学习背景

在调研卡顿监控的过程中,接触到了Runloop机制及其用法,其中一种方案是通过监听每次Runloop循环的时间来判断是否出现时间的阻塞,并同时dump出正在执行的调用栈来定位执行时间较长的函数,进一步进行优化。同时还有通过开启另外的线程给主线程发送消息,看主线程Runloop是否能够处理Source事件来进行处理返回的时间来判断阻塞状况。

- RunLoop定义

在有持续的异步任务需求场景下,我们会创建一个独立的生命周期可控的线程。每个线程都存在一个RunLoop,并且RunLoop就是控制线程生命周期并接收事件进行处理的机制。RunLoop是iOS事件响应与任务处理最核心的机制,它贯穿iOS整个系统。
通过RunLoop机制能够根据具体情况进行代码优化,达到省电,提升流畅度和响应速度的目的,从而提升用户体验。

- Runloop特性

  • 主线程的RunLoop在应用启动的时候就会自动创建
  • 其他线程则需要在该线程下自己启动
    不能自己创建RunLoop
  • RunLoop并不是线程安全的,所以需要避免在其他线程上调用当前线程的RunLoop
  • RunLoop负责管理autorelease pools
  • RunLoop负责处理消息事件,即输入源事件和计时器事件
  • RunLoop支持的消息事件(Events)

- Runloop结构

从Runloop.h文件中可以看到四个字段,如下图所示:
在这里插入图片描述
同时我们也可以通过以下代码来打印出Runloop的信息:

NSLog(@"%@", [NSRunLoop mainRunLoop]);

在这里插入图片描述
由上图可以看到一个Runloop包含了modes,而它当前只运行在一个mode下,而每个mode都包含CFRunLoopSource,CFRunLoopObserver还有一个CFRunLoopTimerRef。

- CFRunLoopMode

RunloopMode就是流水线上支持生产的产品类型,流水线在一个时刻只能在一种模式下运行,生产某一类型的产品。消息事件就是订单。

CFRunLoopModeRef

CFRunLoopModeRef 代表 RunLoop 的运行模式
每个RunLoop都包含若干个Mode,每个Mode又包含若干个 Source/Timer/Observer,每次RunLoop 启动时,只能指定其中一个 Mode,这个 Mode 被称作CurrentMode,如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入,这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响(可以通过切换 Mode,完成不同的 timer/source/observer)。

Cocoa系统默认注册了5个Mode:

NSDefaultRunLoopMode: App的默认 Mode,通常主线程是在这个 Mode 下运行(默认情况下运行)

UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响(操作 UI 界面的情况下运行)

UIInitializationRunLoopMode: 在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用

GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到(绘图服务)

NSRunLoopCommonModes: 这是一个占位用的 Mode,不是一种真正的 Mode (RunLoop无法启动该模式,设置这种模式下,默认和操作 UI 界面时线程都可以运行,但无法改变 RunLoop 同时只能在一种模式下运行的本质)

CFRunLoopTimerRef

CFRunLoopTimerRef 是基于事件的触发器
CFRunLoopTimerRef 基本上就是 NSTimer,它受 RunLoop的Mode 影响
创建 Timer 有两种方式,下面的这种方式必须手动添加到 RunLoop 中去才会被调用。

// 这种方式创建的timer 必须手动添加到RunLoop中去才会被调用
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(time) userInfo:nil repeats:YES];

[[NSRunLoop currentRunLoop] addTimer:timer
forMode:NSDefaultRunLoopMode];

Mode典型案例

RunLoop可以通过[acceptInputForMode:beforeDate:]和[runMode:beforeDate:]来指定在一段时间内的运行模式。如果不指定的话,RunLoop默认会运行在Default下(不断重复调用runMode:NSDefaultRunLoopMode beforDate:)

在主线程启动一个计时器Timer,然后拖动UITableView或者UIScrollView,计时器不执行。这是因为,为了更好的用户体验,在主线程中Event tracking模式的优先级最高。在用户拖动控件时,主线程的Run Loop是运行在Event tracking Mode下,而创建的Timer是默认关联为Default Mode,因此系统不会立即执行Default Mode下接收的事件。解决方法:

NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                                   target:self
                                                 selector:@selector(timerFireMethod:)
                                                 userInfo:nil
                                                  repeats:YES];
                                                  
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes]; 
//或 
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

[timer fire];

CFRunLoopSourceRef

CFRunLoopSourceRef 其实是事件源(输入源)

按照官方文档,Source的分类:

  • Port-Based Sources: 基于端口的:跟其他线程进行交互的,Mac内核发过来一些消息
  • Custom Input Sources: 自定义输入源
  • Cocoa Perform Selector Sources: (self performSelector:…)

按照函数调用栈,Source的分类:

  • Source0: 非基于Port的(触摸事件、按钮点击事件)
  • Source1: 基于Port的,通过内核和其他线程通信,接收分发系统事件
    (触摸硬件,通过 Source1 接收和分发系统事件到 Source0 处理)
    在这里插入图片描述
    可以看到在调用点击事件时,中间会经过 CFRunLoopSource0 ,这说明点击事件是属于 Source0 的。
    而 Source1 是基于 Port 的,就是说,Source1 是和硬件交互的,触摸首先在屏幕上被包装成一个 event 事件,再通过 Source1 进行分发到 Source0,最后通过 Source0 进行处理。
    在这里插入图片描述

CFRunLoopObserverRef

CFRunLoopObserverRef 是观察者,能够监听 RunLoop 的状态改变,主要监听以下几个时间节点:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) 
{
    kCFRunLoopEntry = (1UL << 0), // 1 即将进入 Loop
    kCFRunLoopBeforeTimers = (1UL << 1), // 2 即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 4 即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), // 32 即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6), // 64 刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7), // 128 即将退出 Loop

    kCFRunLoopAllActivities = 0x0FFFFFFFU // 监听所有事件
};
// 1.创建观察者 监听 RunLoop
// 参1: 有个默认值 CFAllocatorRef :CFAllocatorGetDefault()
// 参2: CFOptionFlags activities 监听RunLoop的活动 枚举 见上面
// 参3: 重复监听 Boolean repeats YES
// 参4: CFIndex order 传0
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    // 该方法可以在添加timer之前做一些事情,  在添加source之前做一些事情
    NSLog(@"%zd", activity);
});

// 2.添加观察者,监听当前的RunLoop对象
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

CFRelease(observer);

通过打印可以观察的 RunLoop 的状态

- RunLoop工作机制

在CFRunLoop.c中,可以看到RunLoop的执行代码大致如下:

{
    /// 1. 通知Observers,即将进入RunLoop
    /// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
    do {

        /// 2. 通知 Observers: 即将触发 Timer 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
        /// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

        /// 4. 触发 Source0 (非基于port的) 回调。
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

        /// 6. 通知Observers,即将进入休眠
        /// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);

        /// 7. sleep to wait msg.
        mach_msg() -> mach_msg_trap();

        /// 8. 通知Observers,线程被唤醒
        __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

        /// 9. 如果是被Timer唤醒的,回调Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

        /// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

        /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);


    } while (...);

    /// 10. 通知Observers,即将退出RunLoop
    /// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

- Runloop的实例运用

通过Runloop实现卡顿的监控

主线程绝大部分计算或者绘制任务都是以Runloop为单位发生。单次Runloop如果时长超过16ms,就会导致UI体验的卡顿。那如何检测单次Runloop的耗时呢?
Runloop的生命周期及运行机制虽然不透明,但苹果提供了一些API去检测部分行为。我们可以通过如下代码监听Runloop每次进入的事件:

- (void)setupRunloopObserver{
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        CFRunLoopRef runloop = CFRunLoopGetCurrent(); 
        CFRunLoopObserverRef enterObserver;
        enterObserver = CFRunLoopObserverCreate(CFAllocatorGetDefault(),
                                               kCFRunLoopEntry | kCFRunLoopExit,
                                               true,
                                               -0x7FFFFFFF,
                                               _RunloopObserverCallBack,
                                               NULL);
        CFRunLoopAddObserver(runloop, enterObserver, kCFRunLoopCommonModes);
        CFRelease(enterObserver);
    });
}
static void _RunloopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    switch (activity) {
        case kCFRunLoopEntry: {
            NSLog(@"Enter Runloop...");
        }
            break;
        case kCFRunLoopExit: {
            NSLog(@"Leave Runloop...");
        }
            break;
        default: break;
    }

最终kCFRunLoopExit的时间,减去kCFRunLoopEntry的时间,即为一次Runloop所耗费的时间,这样就能找出大于16ms的runloop。

通过Runloop解决界面图片过多的滑动卡顿问题

+ (void)_registerRunLoopWorkDistributionAsMainRunloopObserver:(DWURunLoopWorkDistribution *)runLoopWorkDistribution {
    static CFRunLoopObserverRef defaultModeObserver;
    _registerObserver(kCFRunLoopBeforeWaiting, defaultModeObserver, NSIntegerMax - 999, kCFRunLoopDefaultMode, (__bridge void *)runLoopWorkDistribution, &_defaultModeRunLoopWorkDistributionCallback);
}

static void _registerObserver(CFOptionFlags activities, CFRunLoopObserverRef observer, CFIndex order, CFStringRef mode, void *info, CFRunLoopObserverCallBack callback) {
    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    CFRunLoopObserverContext context = {
        0,
        info,
        &CFRetain,
        &CFRelease,
        NULL
    };
    observer = CFRunLoopObserverCreate(     NULL,
                                            activities,
                                            YES,
                                            order,
                                            callback,
                                            &context);
    CFRunLoopAddObserver(runLoop, observer, mode);
    CFRelease(observer);
}

static void _runLoopWorkDistributionCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    DWURunLoopWorkDistribution *runLoopWorkDistribution = (__bridge DWURunLoopWorkDistribution *)info;
    if (runLoopWorkDistribution.tasks.count == 0) {
        return;
    }
    BOOL result = NO;
    while (result == NO && runLoopWorkDistribution.tasks.count) {
        //加入Runloop分配的加载图片代码块
        RunLoopWorkDistributionUnit unit  = runLoopWorkDistribution.tasks.firstObject;
        result = unit();
    }
}

static void _defaultModeRunLoopWorkDistributionCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    _runLoopWorkDistributionCallback(observer, activity, info);
}

其原理是通过监听Runloop的运行状态,并在kCFRunLoopBeforeWaiting的时候也就是下一个Loop执行代码前去执行耗时操作,减少主线程Loop的负担。

AFNetworking中network的单开请求

下面是AFNetworking中的一部分源码

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
         // 通过Runloop监听某个port,保持Thread不退出
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode]; 
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread =
        [[NSThread alloc] initWithTarget:self
                                selector:@selector(networkRequestThreadEntryPoint:)
                                  object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}
发布了71 篇原创文章 · 获赞 34 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/TuGeLe/article/details/85087194