iOS RunLoop分析

375 阅读4分钟

RunLoop是一个对象,用于处理程序运行中出现的各种事件(触摸事件、定时器、Selector),从而保证程序的持续运行;特点是没有事件处理的时候,会让线程进入休眠模式,从而节约CPU资源,提好程序性能。

// defaultMode启动
void CFRunLoopRun(void) { 
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

从上面代码中可以看出runloop本质上就是do...while的循环,通过阀值kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result控制代码的运行。

1.线程与runloop的关系

  1. 主线程默认开启线程RunLoop,获取方法CFRunLoopGetMain();
  2. 苹果不允许直接创建线程RunLoop,在子线程中可以使用CFRunLoopGetCurrent 获取子线程的RunLoop对象;
  3. 线程与RunLoop关系是一一对应的,其关系保存在一个全局的字典中,子线程的RunLoop的创建是第一次获取时,销毁是在线程结束时。

CFRunLoopGetCurrent 调用时,会先看一下字典里有没有存子线程相对用的RunLoop,如果有则直接返回RunLoop,如果没有则会创建一个,并将与之对应的子线程存入字典中。

主线程CFRunLoopRef相关源码:

// 创建字典
 CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
// 创建主线程 根据传入的主线程创建主线程对应的RunLoop
 CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
// 保存主线程 将主线程-key和RunLoop-Value保存到字典中
 CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);

子线程CFRunLoopRef相关源码:

// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    // 加锁
    OSSpinLockLock(&loopsLock);
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    // 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    if (!loop) {
        // 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        // 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

2. CFRunLoopMode类型

  1. kCFRunLoopDefaultMode: 默认mode, 主线程一般在这个mode下运行
  2. UITrackingRunLoopMode: scrollView追踪触摸滑动,保证界面滑动中不受其他mode影响
  3. UIInitializationRunLoopMode: App启动时进入的第一个mode,后面用不到
  4. GSEventReceiveRunLoopMode: 系统内部时间mode
  5. kCFRunLoopCommonModes: 占位mode,标记kCFRunLoopDefaultMode和UITrackingRunLoopMode,不是真正的mode

3. CFRunLoopMode和CFRunLoop关系

CFRunLoopMode源码:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set 
    CFMutableSetRef _sources1;    // Set 
    CFMutableArrayRef _observers; // Array 
    CFMutableArrayRef _timers;    // Array 
    ...
};

CFRunLoop源码:

struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set 
    CFMutableSetRef _commonModeItems; // Set 
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set 
    ...
};

CFRunLoopSourceRef

  1. 是事件产生的地方。Source有两个版本:Source0 和 Source1。
  2. Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  3. Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程.

CFRunLoopTimerRef

基于时间触发器,可以和NSTimer混用。包含一个时间长度和一个函数指针(用于回调)。将其加入RunLoop时,RunLoop会注册这个时间点。时间点到时,RunLoop会被唤醒执行回调函数。

CFRunLoopObserverRef

观察者(观察RunLoop的状态),每个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
};

流程如图:

一个RunLoop包含若干mode,每个mode又包含observer\timer\source,调用RunLoop的主函数是,只能指定一种mode即CurrentMode。如果切换mode,只能先退出Loop,重新指定一个mode,目的是分隔不同组的observer\timer\source,让其不互相影响。

Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

CFRunLoop对外暴露的管理 Mode 接口:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);

Mode 暴露的管理 mode item 的接口:

CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);

参考链接