iOS - RunLoop

190 阅读4分钟

RunLoop概述

macOS/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef
RunLoop指的是NSRunloop或者CFRunloopRef,CFRunloopRef是C语言的函数,而NSRunloop仅仅是CFRunloopRef的OC封装
源代码

RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件/消息,有事件/消息处理就保持运行,没有事件/消息处理的时候就会进入休眠状态,从而节省CPU资源,提高程序性能
RunLoop内部其实是一个_do while_循环,这也正是Runloop运行的本质。执行了这个函数以后就一直处于“等待-处理”的循环之中,直到循环结束。只是不同于我们自己写的循环它在休眠时几乎不会占用系统资源

  • 没有事件/消息需要处理时,进入休眠避免资源占用
    用户态 —> 内核态
  • 有事件/消息需要处理时,立即被唤醒
    内核态 —> 用户态

RunLoop运行逻辑,官方图

runloop1 (1).jpg

RunLoop结构

Runloop Mode

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;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;	/* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    dispatch_source_t _timerSource;
    dispatch_queue_t _queue;
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
    DWORD _msgQMask;
    void (*_msgPump)(void);
#endif
    uint64_t _timerSoftDeadline; /* TSR */
    uint64_t _timerHardDeadline; /* TSR */
};

一个RunLoop包含了多个Mode,每个Mode又包含了若干个Source/Timer/Observer。每次调用 RunLoop的主函数时,只能指定其中一个Mode,这个Mode被称作CurrentMode。如果需要切换 Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同Mode中的Source/Timer/Observer,让其互不影响

  • NSDefaultRunLoopMode
    系统默认的Runloop Mode,在主线程运行。例如进入iOS程序默认不做任何操作就处于这种Mode中
  • UITrackingRunLoopMode
    滑动视图,主线程就切换Runloop到到UITrackingRunLoopMode,不再接受其他事件操作
  • UIInitializationRunLoopMode
    在刚启动App时第进入的第一个Mode,启动完成后就不再使用
  • GSEventReceiveRunLoopMode
    用于接受系统事件,属于内部的RunLoop模式
  • NSRunLoopCommonModes
    一种组合模式,包含了NSDefaultRunLoopMode、NSTaskDeathCheckMode、UITrackingRunLoopMode

Source

Source事件分为Source0和Source1

  • Source0 非系统事件,只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1 系统事件,包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。

Observe

Observe消息监听器

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities;		/* immutable */
    CFIndex _order;			/* immutable */
    CFRunLoopObserverCallBack _callout;	/* immutable */
    CFRunLoopObserverContext _context;	/* immutable, except invalidation */
};

Observe状态

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
	kCFRunLoopEntry = (1UL << 0),           // 进入RunLoop 
	kCFRunLoopBeforeTimers = (1UL << 1),    // 即将开始Timer处理
	kCFRunLoopBeforeSources = (1UL << 2),   // 即将开始Source处理
	kCFRunLoopBeforeWaiting = (1UL << 5),   // 即将进入休眠
	kCFRunLoopAfterWaiting = (1UL << 6),    //从休眠状态唤醒
	kCFRunLoopExit = (1UL << 7),            //退出RunLoop
	kCFRunLoopAllActivities = 0x0FFFFFFFU
    };

Call out

事件/消息回调(无论是Observer的状态通知还是Timer、Source的处理)
static void __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__();\
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__();\
static void __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__();\
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__();\
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__();\
static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__();\

RunLoop与线程

苹果只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent(), 这两个函数内部的实现逻辑大概是下面这样:

/// 全局的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());
}

从上面代码得知:

  • runloop是来管理线程的,当线程的runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务
  • 线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里
  • runloop在第一次获取时被创建,在线程结束时被销毁。

RunLoop底层实现

RunLoop 实现是基于 mach port,RunLoop 的核心就是一个 mach_msg(),RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。
为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作,如下图:

runloop2.png

RunLoop应用

  • AutoreleasePool

  • 事件响应

  • 定时器

  • 网络请求

  • Texture
    ……