iOS中的RunLoop

Posted by AndyCao on September 17, 2019

什么是RunLoop

顾名思义,就是运行循环,在程序运行过程中,循环做一些事情。先简单看下下面两个示例:

示例1

int main(int argc, char * argv[]) {
    @autoreleasepool {
       NSLog(@"Hello World!");
    }
    return 0;
}

由于没有RunLoop,在执行完NSLog之后,程序即将退出

示例2

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

在初始化UIApplication的时候,会创建一个Main RunLoop。由于Main RunLoop的存在,程序不会立马退出,而是保持运行状态。

应用范畴

  • 定时器(Timer)、PerformSelector
  • GCD Async Main Queue
  • 事件响应、手势识别、界面刷新
  • 网络请求
  • AutoreleasePool

RunLoop对象

iOS中,有2套API可以访问和使用RunLoop

  • Foundation框架中的NSRunLoop
  • CoreFoundation框架中的CFRunLoopRef

NSRunLoop是基于CFRunLoopRef的一层OC包装;CFRunLoopRef是开源的

RunLoop与线程

  • 每条线程都有唯一的一个与之对应的RunLoop对象
  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
  • RunLoop会在线程结束时销毁;线程没了,那么RunLoop也就没了
  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop(敲重点!!!)

获取RunLoop对象

Foundation

[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

CoreFoundation

CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象

CoreFoundation中关于RunLoop的5个类

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

CFRunLoopRef在源码中的定义

CFRunLoopModeRef在源码中的定义

CFRunLoopModeRef代表RunLoop的的运行模式,常用的模式有2种:

  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode):默认模式,通常主线程就是在这个Mode下运行
  • UITrackingRunLoopMode:滚动模式,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响,在滚动模式下RunLoop只处理与滚动相关的事件

RunLoop的内部组成

一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer

  • Timer表示定时器;主要处理NSTimer、performSelector:withObject:afterDelay:
  • Observer表示监听器;主要处理UI刷新(beforeWaiting)、监听RunLoop的状态、AutoreleasePool(beforeWaiting)等
  • Source0表示要处理的事情;比如触摸事件、performSelector:onThread:
  • Source1表示要处理的事情;比如基于Port的线程间的通信,系统事件捕捉

RunLoop启动时只能选择其中一个Mode,作为currentMode 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会马上退出

例如创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。

有时候需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 “commonModeItems” 中。”commonModeItems” 被 RunLoop 自动更新到所有具有”Common”属性的 Mode 里去。

CFRunLoopObserverRef

/* Run Loop Observer Activities */
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
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

可以添加Observer来监听RunLoop的所有状态

// 创建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    switch (activity) {
        case kCFRunLoopEntry: {
            NSLog(@"kCFRunLoopEntry");
            break;
        }
        case kCFRunLoopBeforeTimers: {
            NSLog(@"kCFRunLoopBeforeTimers");
            break;
        }
        case kCFRunLoopBeforeSources: {
            NSLog(@"kCFRunLoopBeforeSources");
            break;
        }
        case kCFRunLoopBeforeWaiting: {
            NSLog(@"kCFRunLoopBeforeWaiting");
            break;
        }
        case kCFRunLoopAfterWaiting: {
            NSLog(@"kCFRunLoopAfterWaiting");
            break;
        }
        case kCFRunLoopExit: {
            NSLog(@"kCFRunLoopExit");
            break;
        }
            
        default:
            break;
    }
});
// 添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);

RunLoop运行逻辑

int32_t __CFRunLoopRun()
{
    // 通知即将进入runloop
    __CFRunLoopDoObservers(KCFRunLoopEntry);
    
    do
    {
        // 通知将要处理timer和source
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
        
        // 处理非延迟的主线程调用
        __CFRunLoopDoBlocks();
        // 处理UIEvent事件
        __CFRunLoopDoSource0();
        
        // GCD dispatch main queue
        CheckIfExistMessagesInMainDispatchQueue();
        
        // 即将进入休眠
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
        
        // 等待内核mach_msg事件
        mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
        
        // Zzz...
        
        // 从等待中醒来
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
        
        // 处理因timer的唤醒
        if (wakeUpPort == timerPort)
            __CFRunLoopDoTimers();
        
        // 处理异步方法唤醒,如dispatch_async
        else if (wakeUpPort == mainDispatchQueuePort)
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
            
        // UI刷新,动画显示
        else
            __CFRunLoopDoSource1();
        
        // 再次确保是否有同步的方法需要调用
        __CFRunLoopDoBlocks();
        
    } while (!stop && !timeout);
    
    // 通知即将退出runloop
    __CFRunLoopDoObservers(CFRunLoopExit);
}

RunLoop的实际应用

NSTimer失效

默认情况下,添加NSTimer执行timerWithTimeInterval方法,当有滚动事件触发时,会导致NSTimer失效

- (void)logCount {
    NSLog(@"1111");
}

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(logCount) userInfo:nil repeats:YES];
}

RunLoop在同一时间只能运行一种模式,一旦有滚动事件触发时,RunLoop会切换到UITrackingRunLoopMode模式。 可以这样解决:

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

也可以这样:

// NSDefaultRunLoopMode 和 UITrackingRunLoopMode默认是被标记为common mode的
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];