iOS——runLoop

news2024/11/24 17:06:37

什么是runloop

RunLoop实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行相应的处理逻辑。线程执行了这个函数后,就会处于这个函数内部的循环中,直到循环结束,函数返回。

RunLoop是一种高级的循环机制,让程序持续运行,并处理程序中的各种事件,让线程在需要做事的时候忙起来,不需要的话就让线程休眠。

RunLoop和线程是绑定在一起的,每条线程都有唯一一个与之对应的RunLoop对象。
不能自己创建RunLoop对象,但是可以获取系统提供的RunLoop对象。
主线程的RunLoop对象是由系统自动创建好的,在应用程序启动的时候会自动完成启动,而子线程中的RunLoop对象需要我们手动获取并启动。

Runloop解决了什么问题?

比如使用UIScrollView时:默认情况下,主RunLoop始终在应用程序中运行;它处理来自系统的消息并将其传输到应用程序。例如用户单击屏幕时的事件。
应用程序中的所有计时器都在运行循环上执行。

它是如何工作的?

RunLoop是一个循环,它有几种操作模式,可以帮助我们了解何时运行特定任务。

RunLoop可以采用以下模式:

  • Default 模式:在这种模式下,RunLoop是自由的,可以处理各种常规事件,比如用户触摸、计时器、网络请求回调等。因为它适合处理各种任务,所以称为“默认模式”。例如,在这个模式下,可以安全地执行耗时的计算或其他需要占用大量资源的操作。

  • Tracking 模式:当用户在进行一些需要快速响应的操作时(如滚动视图),RunLoop会切换到Tracking模式。这种模式下,RunLoop会暂时停止处理其他不重要的事件,以确保用户的操作能够得到迅速响应,从而避免卡顿。例如,当用户拖动列表时,这种模式会保证滚动的流畅性,而不被其他后台任务打断。

  • Initialization 模式:Initialization模式仅在初始化阶段使用。比如在应用程序刚启动或者线程刚创建时,RunLoop会进入这个模式来执行初始化工作。一旦初始化完成,RunLoop会退出这个模式。一般情况下,开发者不需要直接处理这个模式。

  • EventReceive 模式:这是一个系统级别的内部模式,用于接收和处理系统事件。开发者通常不会直接与这个模式交互,它主要用于系统内部的事件管理,例如处理硬件中断或系统通知等。

  • Common 模式:Common模式是一个占位符,用于将多个模式组合在一起。在实际使用中,开发者可以将某些事件源(如输入源和定时器)加入到Common模式,这样这些事件源可以在多个模式下共享。例如,可以将定时器添加到Common模式,使其在Default和Tracking模式下都能被触发。这有助于简化事件管理,避免在不同模式下重复添加事件源。

在主RunLoop上,这些模式会自动切换;开发人员可以使用它们来执行耗时的任务,这样用户就不会注意到界面挂起。

runloop的获取

两种获取方式:

// Foundation
NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // 获得当前RunLoop对象
NSRunLoop *runloop = [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
// Core Foundation
CFRunLoopRef runloop = CFRunLoopGetCurrent(); // 获得当前RunLoop对象
CFRunLoopRef runloop = CFRunLoopGetMain(); // 获得主线程的RunLoop对象

runloop的三种启动方式

//该方法会让 RunLoop 一直运行,除非有特定条件让它停止。
- (void)run;
//该方法设置了超时时间,如果超过了这个时间就会停止。但是如果提前把所有事件都处理完毕也会提前退出。
- (void)runUntilDate:(NSDate *)limitDate;
//方法RunLoop会运行一次,超时时间到达或者第一个source被处理,则RunLoop就会退出。
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

runloop和线程

  • RunLoop和线程是绑定在一起的,每条线程都有唯一一个与之对应的RunLoop对象。
  • 不能自己创建RunLoop对象,但是可以获取系统提供的RunLoop对象。
  • 主线程的RunLoop对象是由系统自动创建好的,在应用程序启动的时候会自动完成启动,而子线程中的RunLoop对象需要我们手动获取并启动。
    runloop和线程的关系:
    在这里插入图片描述

RunLoop在线程中不断检测,通过input source和timer source接受事件,然后通知线程进行处理事件。

runloop的结构

struct __CFRunLoop {
    CFRuntimeBase _base;// 基本的运行时结构,包含对象的引用计数等信息
    _CFRecursiveMutex _lock;// 递归锁,用于保护模式列表的访问
    __CFPort _wakeUpPort;// 用于CFRunLoopWakeUp的端口
    volatile _per_run_data *_perRunData;// 每次运行RunLoop时重置的数据
    _CFThreadRef _pthread;// 线程引用
    uint32_t _winthread; // Windows线程引用
    CFMutableSetRef _commonModes;// 通用模式集合
    CFMutableSetRef _commonModeItems; // 通用模式的项目集合
    CFRunLoopModeRef _currentMode; // 当前运行的模式
    CFMutableSetRef _modes;// 所有模式的集合
    struct _block_item *_blocks_head;// 块项链表的头部
    struct _block_item *_blocks_tail;// 块项链表的尾部
    CFAbsoluteTime _runTime;// RunLoop的运行时间
    CFAbsoluteTime _sleepTime;// RunLoop的休眠时间
    CFTypeRef _counterpart;// 对应的类型引用
    _Atomic(uint8_t) _fromTSD;// 原子变量,用于线程本地存储数据
    Boolean _perCalloutARP;// 每次回调的自动参考计数标志
    CFLock_t _timerTSRLock;// 定时器TSR锁
};

一个RunLoop对象包含一个线程(_pthread),若干个mode(_modes),若干个commonMode(_commonModes)。 不管是mode还是commonMode其类型都是CFRunLoopMode,只是在处理上有所不同。

CFRunLoopModeRef

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    //锁, 必须runloop加锁后才能加锁
    pthread_mutex_t _lock;    /* must have the run loop locked before locking this */
    //mode的名称
    CFStringRef _name;
    //mode是否停止
    Boolean _stopped;
    char _padding[3];
    //sources0事件
    CFMutableSetRef _sources0;
    //sources1事件
    CFMutableSetRef _sources1;
    //observers事件
    CFMutableArrayRef _observers;
    //timers事件
    CFMutableArrayRef _timers;
    //字典  key是mach_port_t,value是CFRunLoopSourceRef
    CFMutableDictionaryRef _portToV1SourceMap;
    //保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中
    __CFPortSet _portSet;
    CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    //GCD定时器
    dispatch_source_t _timerSource;
    //GCD队列
    dispatch_queue_t _queue;
    // 当定时器触发时设置为true
    Boolean _timerFired; // set to true by the source when a timer has fired
    Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
    //MK_TIMER的port
    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 */
};

从CFRunLoopMode的源码不难看出,一个CFRunLoopMode对象包含以下内容:

  • 唯一的一个name: 每个CFRunLoopMode对象都有一个唯一的名字来标识它。
  • 若干个sources0事件: sources0是指不需要内核参与的事件源,通常是一些用户定义的事件。
  • 若干个sources1事件: sources1是指需要内核参与的事件源,比如基于端口的事件。
  • 若干个timer事件: 定时器事件,用于在特定时间点或周期性地触发。
  • 若干个observer事件: 观察者事件,用于监听RunLoop状态的变化,比如即将进入休眠、即将退出等。
  • 若干个port: 端口,用于线程间的通信。
    RunLoop总是在某种特定的CFRunLoopMode下运行,这个特定的模式就是_currentMode。每次运行RunLoop时,都必须指定一个模式,这个模式决定了RunLoop在本次运行中会处理哪些事件。

而从CFRunloopRef对应结构体的定义可以知道,一个RunLoop对象包含若干个模式。也就是说,一个RunLoop可以在不同的模式下运行,每种模式包含不同的事件和端口。当切换模式时,RunLoop必须退出当前模式,然后重新进入另一个模式,以保证不同模式中的事件源、定时器和观察者互不影响。

通过这种机制,RunLoop可以在不同的上下文中运行,处理不同类型的事件,提供灵活的事件处理能力。

CFRunLoopMode有五种,其中在iOS中公开暴露只有NSDefaultRunLoopModeNSRunLoopCommonModes

  • NSDefaultRunLoopMode:默认模式是用于大多数操作的模式。大多数时候使用此模式来启动RunLoop并配置输入源。
  • NSConnectionReplyMode:Cocoa将此模式与NSConnection对象结合使用以监测回应。几乎不需要自己使用此模式。
  • NSModalPanelRunLoopMode:Cocoa使用此模式来识别用于模式面板的事件。
  • NSEventTrackingRunLoopMode:Cocoa使用此模式来限制鼠标拖动loop和其他类型的用户界面跟踪loop期间的传入事件。通常用不到。
  • NSRunLoopCommonModes:是NSDefaultRunLoopModeNSEventTrackingRunLoopMode集合,在这种模式下RunLoop分别注册了NSDefaultRunLoopModeUITrackingRunLoopMode。当然也可以通过调用CFRunLoopAddCommonMode()方法将自定义Mode放到CFRunLoopCommonModes组合。

CFRunLoopSourceRef-事件源

struct __CFRunLoopSource {
    CFRuntimeBase _base;
    //用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;            /* immutable */
    CFMutableBagRef _runLoops;
    //联合体 
    union {
        CFRunLoopSourceContext version0;    /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};

CFRunLoopSource是输入源的抽象,分为source0和source1两个版本。

  • source0:是App内部事件,只包含一个函数指针回调,并不能主动触发事件,使用时,你需要先调用CFRunLoopSourceSignal(source),将这个source标记为待处理,然后手动调用CFRunLoopWakeUp(runloop)来唤醒RunLoop,让其处理这个事件。
  • source1:source1包含一个mach_port和一个函数回调指针。source1是基于port的,通过读取某个port上内核消息队列上的消息来决定执行的任务,然后再分发到sources0中处理的。source1只供系统使用,并不对开发者开放。

CFRunLoopTimerRef–Timer事件

CFRunLoopTimer是定时器。下面是CFRunLoopTimer的源码:

struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    //timer对应的runloop
    CFRunLoopRef _runLoop;
    //timer对应的mode
    CFMutableSetRef _rlModes;
    //下一次触发的时间
    CFAbsoluteTime _nextFireDate;
    //定时的间隔
    CFTimeInterval _interval;        /* immutable */
    //定时器允许的误差
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;            /* TSR units */
    //优先级
    CFIndex _order;            /* immutable */
    //任务回调
    CFRunLoopTimerCallBack _callout;    /* immutable */
    //上下文
    CFRunLoopTimerContext _context;    /* immutable, except invalidation */
}; 

从上面的代码可以看出,timer是依赖于runloop的,而且有函数指针回调,那么便可以在设定的时间点抛出回调执行任务。同时苹果官方文档也有提到CFRunLoopTimer和NSTimer是toll-free bridged(指在 Core Foundation 框架和 Foundation 框架中某些类型可以直接互换使用。)的,这就意味着两者之间可以相互转换。

CFRunLoopObserverRef–观察者

CFRunLoopObserver是观察者,监测RunLoop的各种状态的变化。

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    //对应的runLoop对象
    CFRunLoopRef _runLoop;
    // 当前的观察的runloop个数
    CFIndex _rlCount;
    //runloop的状态
    CFOptionFlags _activities;        /* immutable */
    CFIndex _order;            /* immutable */
    //回调
    CFRunLoopObserverCallBack _callout;    /* immutable */
    //上下文
    CFRunLoopObserverContext _context;    /* immutable, except invalidation */
};

RunLoop的source事件源来监测是否有需要执行的任务,而observer则是监测RunLoop本身的各种状态的变化,在合适的时机抛出回调,执行不同类型的任务。
runloop用于观察的状态:

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

在这里插入图片描述

runloop实现原理

runloop启动

RunLoop启动有两个方法可供调用,分别是CFRunLoopRunCFRunLoopRunInMode

//启动当前线程的RunLoop,并使其在默认模式下运行,直到RunLoop被停止或完成。
void CFRunLoopRun(void) {    /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

//在指定模式下运行RunLoop,并允许设定超时时间和是否在处理完一个source事件后返回。
SInt32 CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) {     /* DOES CALLOUT */
    CHECK_FOR_FORK();
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

CFRunLoopRun:

  • 调用 CFRunLoopRunSpecific 函数,将当前线程的RunLoop(通过 CFRunLoopGetCurrent() 获取)运行在默认模式下( kCFRunLoopDefaultMode ),并设置一个极大的超时时间( 1.0e10 ),表示基本上不会超时,且不在处理完一个source事件后立即返回( false )。
  • 调用 CHECK_FOR_FORK() 检查是否有进程fork(这是一种保护措施,确保fork后RunLoop的状态是安全的)。
  • 如果 result 不等于 kCFRunLoopRunStoppedkCFRunLoopRunFinished,则继续运行。 kCFRunLoopRunStoppedkCFRunLoopRunFinished 是RunLoop运行结束的标志。

获取runloop

  • CFRunLoopGetCurrent:获取当前线程的runloop对象
  • CFRunLoopGetMain:获取主线程的runloop对象

CFRunLoopGetCurrent

CFRunLoopRef CFRunLoopGetCurrent(void) {
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

该方法内部调用了_CFRunLoopGet0方法,传入的参数是当前线程pthread_self(),由此可见CFRunLoopGetCurrent函数必须要在线程内部调用才能获取到RunLoop对象。

CFRunLoopGetMain

CFRunLoopRef CFRunLoopGetMain(void) {
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

该方法内部调用了_CFRunLoopGet0方法,传入的参数是主线程pthread_main_thread_np(),由此可见CFRunLoopGetCurrent函数不管是在主线程中还是在子线程中都可以获取到主线程的RunLoop。

_CFRunLoopGet0


static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFLock_t loopsLock = CFLockInit;

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t)
{
    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
        //创建一个字典
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        //创建主线程的RunLoop
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        //把主线程的RunLoop保存到dict中,key是线程,value是RunLoop
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        //此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoops
        if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void *volatile *)&__CFRunLoops)) {
            CFRelease(dict);
        }
        //释放主线程RunLoop
        CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    // 根据线程从__CFRunLoops获取RunLoop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    //如果在__CFRunLoops中没有找到
    if (!loop) {
        //创建一个新的RunLoop
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            //把新创建的RunLoop存放到__CFRunLoops中
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
        CFRelease(newLoop);
    }
    //如果传入的线程就是当前的线程
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            //注册一个回调,当当前线程销毁时销毁对应的RunLoop
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS - 1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}

  • 检查传入线程是否为主线程,如果是,将其转换为主线程。
  • 加锁并初始化全局字典,如果字典尚未初始化,则创建并初始化它。
  • 从字典中获取当前线程的 CFRunLoop,如果未找到则创建新的并存入字典。
  • 为当前线程设置特定数据,以便在线程销毁时清理对应的 CFRunLoop。
  • 返回获取或创建的 CFRunLoop。

由上面这段代码可知:
RunLoop和线程是一一对应的,是以线程为key,RunLoop对象为value存放在一个全局字典中的。
主线程的RunLoop会在初始化全局化字典时创建。
子线程的RunLoop会在第一次获取时创建。
当线程销毁时,对应的RunLoop也会随之销毁。

CFRunLoopRunSpecific

CFRunLoop 的核心运行逻辑。CFRunLoopRunSpecific 函数根据指定的模式和超时时间运行
在这里插入图片描述

unLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled)
{
    // 检查是否在 fork 之后的子进程中运行
    CHECK_FOR_FORK();
    // 如果 rl 正在被释放,返回 kCFRunLoopRunFinished
    if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
    // 锁定 CFRunLoop 实例,确保线程安全
    __CFRunLoopLock(rl);
    
    // 根据 modeName 找到本次运行的 mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    
    // 如果没有找到 mode 或者找到的 mode 中没有注册事件则退出,不进入循环
    if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
        Boolean did = false;
        // 如果找到了 mode,则解锁该 mode
        if (currentMode)
            __CFRunLoopModeUnlock(currentMode);
        // 解锁 CFRunLoop 实例
        __CFRunLoopUnlock(rl);
        // 根据是否处理了事件返回相应的结果
        return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
    }
    
    // 保存当前的运行数据
    volatile _per_run_data *previousPerRun = __CFRunLoopPushPerRunData(rl);
    // 取上一次运行的 mode
    CFRunLoopModeRef previousMode = rl->_currentMode;
    // 设置当前模式为找到的 mode
    rl->_currentMode = currentMode;
    // 初始化结果为 kCFRunLoopRunFinished
    int32_t result = kCFRunLoopRunFinished;
    
    // 如果当前模式有 kCFRunLoopEntry 类型的观察者,通知这些观察者即将进入 RunLoop
    if (currentMode->_observerMask & kCFRunLoopEntry)
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    // 调用实际的 RunLoop 运行函数,返回运行结果
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    // 如果当前模式有 kCFRunLoopExit 类型的观察者,通知这些观察者即将退出 RunLoop
    if (currentMode->_observerMask & kCFRunLoopExit)
        __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);

    // 解锁当前模式
    __CFRunLoopModeUnlock(currentMode);
    // 恢复之前的运行数据
    __CFRunLoopPopPerRunData(rl, previousPerRun);
    // 恢复之前的模式
    rl->_currentMode = previousMode;
    // 解锁 CFRunLoop 实例
    __CFRunLoopUnlock(rl);
    // 返回运行结果
    return result;
}

传入四个参数:

  • rl:当前运行的RunLoop对象。
  • modeName:指定RunLoop对象的mode的名称。
  • seconds:RunLoop的超时时间
  • returnAfterSourceHandled:是否在处理完事件之后返回。

从上面的代码我们可以获取到如下几点信息:

  • RunLoop运行必须要指定一个mode,否则不会运行RunLoop。
  • 如果指定的mode没有注册时间任务,RunLoop不会运行。
  • 通知observer进入runloop,调用__CFRunLoopRun方法处理任务,通知observer退出runloop。

__CFRunLoopRun

// __CFRunLoopRun函数,启动RunLoop
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
    // 获取系统启动后的CPU运行时间,用于控制超时时间
    uint64_t startTSR = mach_absolute_time();
    
    // 如果RunLoop或者mode是stop状态,则直接return,不进入循环
    if (__CFRunLoopIsStopped(rl)) {
        __CFRunLoopUnsetStopped(rl);
        return kCFRunLoopRunStopped;
    } else if (rlm->_stopped) {
        rlm->_stopped = false;
        return kCFRunLoopRunStopped;
    }
    
    // mach端口,在内核中,消息在端口之间传递。初始为0
    mach_port_name_t dispatchPort = MACH_PORT_NULL;
    // 判断是否为主线程
    Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
    // 如果在主线程 && runloop是主线程的runloop && 该mode是commonMode,则给mach端口赋值为主线程收发消息的端口
    if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) dispatchPort = _dispatch_get_main_queue_port_4CF();
    
#if USE_DISPATCH_SOURCE_FOR_TIMERS
    mach_port_name_t modeQueuePort = MACH_PORT_NULL;
    if (rlm->_queue) {
        // mode赋值为dispatch端口_dispatch_runloop_root_queue_perform_4CF
        modeQueuePort = _dispatch_runloop_root_queue_get_port_4CF(rlm->_queue);
        if (!modeQueuePort) {
            CRASH("Unable to get port for run loop mode queue (%d)", -1);
        }
    }
#endif
    
    // GCD管理的定时器,用于实现runloop超时机制
    dispatch_source_t timeout_timer = NULL;
    struct __timeout_context *timeout_context = (struct __timeout_context *)malloc(sizeof(*timeout_context));
    
    // 立即超时
    if (seconds <= 0.0) { // instant timeout
        seconds = 0.0;
        timeout_context->termTSR = 0ULL;
    }
    // seconds为超时时间,超时时执行__CFRunLoopTimeout函数
    else if (seconds <= TIMER_INTERVAL_LIMIT) {
        dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, DISPATCH_QUEUE_OVERCOMMIT);
        timeout_timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
        dispatch_retain(timeout_timer);
        timeout_context->ds = timeout_timer;
        timeout_context->rl = (CFRunLoopRef)CFRetain(rl);
        timeout_context->termTSR = startTSR + __CFTimeIntervalToTSR(seconds);
        dispatch_set_context(timeout_timer, timeout_context); // source gets ownership of context
        dispatch_source_set_event_handler_f(timeout_timer, __CFRunLoopTimeout);
        dispatch_source_set_cancel_handler_f(timeout_timer, __CFRunLoopTimeoutCancel);
        uint64_t ns_at = (uint64_t)((__CFTSRToTimeInterval(startTSR) + seconds) * 1000000000ULL);
        dispatch_source_set_timer(timeout_timer, dispatch_time(1, ns_at), DISPATCH_TIME_FOREVER, 1000ULL);
        dispatch_resume(timeout_timer);
    }
    // 永不超时
    else { // infinite timeout
        seconds = 9999999999.0;
        timeout_context->termTSR = UINT64_MAX;
    }
    
    // 标志位默认为true
    Boolean didDispatchPortLastTime = true;
    // 记录最后runloop状态,用于return
    int32_t retVal = 0;
    do {
        // 初始化一个存放内核消息的缓冲池
        uint8_t msg_buffer[3 * 1024];
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        mach_msg_header_t *msg = NULL;
        mach_port_t livePort = MACH_PORT_NULL;
#elif DEPLOYMENT_TARGET_WINDOWS
        HANDLE livePort = NULL;
        Boolean windowsMessageReceived = false;
#endif
        // 取所有需要监听的port
        __CFPortSet waitSet = rlm->_portSet;
        
        // 设置RunLoop为可以被唤醒状态
        __CFRunLoopUnsetIgnoreWakeUps(rl);
        
        // 2.通知observer,即将触发timer回调,处理timer事件
        if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        // 3.通知observer,即将触发Source0回调
        if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        
        // 执行加入当前runloop的block
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 4.处理source0事件,有事件处理返回true,没有事件返回false
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        if (sourceHandledThisLoop) {
            // 执行加入当前runloop的block
            __CFRunLoopDoBlocks(rl, rlm);
        }
        
        // 如果没有Sources0事件处理 并且 没有超时,poll为false
        // 如果有Sources0事件处理 或者 超时,poll都为true
        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
        
        // 第一次do..while循环不会走该分支,因为didDispatchPortLastTime初始化是true
        if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
            // 从缓冲区读取消息
            msg = (mach_msg_header_t *)msg_buffer;
            // 5.接收dispatchPort端口的消息,(接收source1事件)
            if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0)) {
                // 如果接收到了消息的话,前往第9步开始处理msg
                goto handle_msg;
            }
#elif DEPLOYMENT_TARGET_WINDOWS
            if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
                goto handle_msg;
            }
#endif
        }
        
        didDispatchPortLastTime = false;
        
        // 6.通知观察者RunLoop即将进入休眠
        if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        // 设置RunLoop为休眠状态
        __CFRunLoopSetSleeping(rl);
        // do not do any user callouts after this point (after notifying of sleeping)
        
        // Must push the local-to-this-activation ports in on every loop
        // iteration, as this mode could be run re-entrantly and we don't
        // want these ports to get serviced.
        
        __CFPortSetInsert(dispatchPort, waitSet);
        
        __CFRunLoopModeUnlock(rlm);
        __CFRunLoopUnlock(rl);
        
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
#if USE_DISPATCH_SOURCE_FOR_TIMERS
        // 这里有个内循环,用于接收等待端口的消息
        // 进入此循环后,线程进入休眠,直到收到新消息才跳出该循环,继续执行run loop
        do {
            if (kCFUseCollectableAllocator) {
                objc_clear_stack(0);
                memset(msg_buffer, 0, sizeof(msg_buffer));
            }
            msg = (mach_msg_header_t *)msg_buffer;
            // 7.接收waitSet端口的消息
            __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
            // 收到消息之后,livePort的值为msg->msgh_local_port,
            if (modeQueuePort != MACH_PORT_NULL && livePort == modeQueuePort) {
                // Drain the internal queue. If one of the callout blocks sets the timerFired flag, break out and service the timer.
                while (_dispatch_runloop_root_queue_perform_4CF(rlm->_queue));
                if (rlm->_timerFired) {
                    // Leave livePort as the queue port, and service timers below
                    rlm->_timerFired = false;
                    break;
                } else {
                    if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
                }
            } else {
                // Go ahead and leave the inner loop.
                break;
            }
        } while (1);
#else
        // 7.接收waitSet端口的消息
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY);
#endif
#elif DEPLOYMENT_TARGET_WINDOWS
        // 7.接收waitSet端口的消息
        __CFRunLoopWaitForMultipleObjects(rl, rlm->_ports, (poll ? 0 : INFINITE), &livePort, &windowsMessageReceived);
#endif
        
        __CFRunLoopLock(rl);
        __CFRunLoopModeLock(rlm);
        __CFRunLoopUnsetSleeping(rl);
        // 8.通知观察者RunLoop被唤醒
        if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
        
        __CFPortSetRemove(dispatchPort, waitSet);
        if (MACH_PORT_NULL != livePort) {
            // 通知port已被使用
            didDispatchPortLastTime = (livePort == dispatchPort);
            
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        handle_msg:;
            // 9.处理消息,触发Source1回调
            CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE(msg);
            __CFRunLoopDoSource1(rl, rlm, rls, msg, livePort);
            if (msg && msg != (mach_msg_header_t *)msg_buffer) free(msg);
#elif DEPLOYMENT_TARGET_WINDOWS
            if (windowsMessageReceived) {
                // 9.处理消息
                __CFRunLoopProcessWindowsMessage(rl, rlm);
            } else {
                __CFRunLoopDoSource1(rl, rlm, rls, msg, livePort);
            }
#endif
        }
        
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 返回条件:如果存在以下条件,返回相应的状态值
        // 1. runloop是否被显式唤醒退出
        // 2. 是否执行到超时
        // 3. 是否有source0事件需要处理
        // 4. 是否有source1事件需要处理
        // 5. 是否被显式唤醒退出
        // 6. 是否被模式退出
        // 7. 是否被timer退出
        if (__CFRunLoopIsStopped(rl)) {
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        } else if (rlm->_stopped) {
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            retVal = kCFRunLoopRunFinished;
        } else if (sourceHandledThisLoop && stopAfterHandle) {
            retVal = kCFRunLoopRunHandledSource;
        }
    } while (0 == retVal);
    
    if (timeout_timer) {
        dispatch_source_cancel(timeout_timer);
        dispatch_release(timeout_timer);
    }
    free(timeout_context);
    return retVal;
}

该方法内部就是一个 do-while 循环,当调用该方法时,线程就会一直留在这个循环里面,直到超时或者手动被停止,该方法才会返回。在这里循环里面,线程在空闲的时候处于休眠状态,在有事件需要处理的时候,处理事件。该方法是整个RunLoop运行的核心方法。

  1. 通知观察者RunLoop已经启动:当RunLoop启动时,通知所有的观察者,RunLoop已经开始运行。可以在此时进行一些初始化操作。
  2. 通知观察者定时器即将触发:在RunLoop即将处理定时器事件时,通知观察者。观察者可以在此时进行一些操作,如更新UI或记录日志。
  3. 通知观察者任何不基于端口的输入源都将触发:在RunLoop即将处理非基于端口的输入源(Source0)事件时,通知观察者。这些输入源通常是应用程序内部事件。
  4. 触发所有准备触发的非基于端口的输入源:处理所有已经准备好的Source0事件,执行相应的回调函数。
  5. 如果基于端口的输入源已准备好并等待启动,立即处理事件;并进入步骤9:检查是否有基于端口的输入源(Source1)已经有事件需要处理。如果有,则处理这些事件,并跳到步骤9继续处理其他事件。
  6. 通知观察者线程进入休眠状态:在RunLoop即将进入休眠状态之前,通知所有的观察者,让他们知道RunLoop即将进入等待状态。可以在此时进行一些准备工作,如记录日志。
  7. 使线程进入睡眠状态,直到发生以下事件之一
    • 某一事件到达基于端口的源:如果有基于端口的事件到达(如文件描述符可读,网络套接字有数据等),唤醒RunLoop。
    • 定时器触发:如果定时器到期,唤醒RunLoop。
    • RunLoop设置的时间已经超时:如果等待超时,唤醒RunLoop。
    • RunLoop被唤醒:如果RunLoop被显式唤醒(如调用CFRunLoopWakeUp),唤醒RunLoop。
  8. 通知观察者线程即将被唤醒:在RunLoop从睡眠状态被唤醒之前,通知所有的观察者。观察者可以在此时进行一些操作,如更新UI或记录日志。
  9. 处理未处理的事件:唤醒后,处理所有未处理的事件:
    • 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2:如果定时器到期,处理定时器事件并重新进入RunLoop循环。
    • 如果输入源启动,传递相应的消息:处理输入源的事件,传递相应的消息。
    • 如果RunLoop被唤醒而且时间还没超时,重启RunLoop。进入步骤2:如果RunLoop被显式唤醒且时间未超时,重新进入RunLoop循环。
  10. 通知观察者RunLoop结束:RunLoop即将结束时,通知所有的观察者。观察者可以在此时进行一些收尾工作,如释放资源或记录日志。

在这里插入图片描述

非基于端口的输入源(Source0):这些源通常不依赖于内核端口机制,直接调用某个函数来处理事件。例如,手动添加的事件处理函数。
基于端口的输入源(Source1):这些源依赖于内核端口机制,监听来自内核的消息。例如,文件描述符、网络套接字、Mach端口等。

__CFRunLoopServiceMachPort

在__CFRunLoopRun内部有一个内置的循环,这个循环会让线程进入休眠状态,直到收到新消息才跳出该循环,继续执行RunLoop。这些消息是基于mach port来进行进程之间的通讯的。

static Boolean __CFRunLoopServiceMachPort(mach_port_name_t port, mach_msg_header_t **buffer, size_t buffer_size, mach_port_t *livePort, mach_msg_timeout_t timeout) {
    Boolean originalBuffer = true; // 标记缓冲区是否是原始缓冲区
    kern_return_t ret = KERN_SUCCESS; // 初始化返回值
    for (;;) { // 无限循环,直到接收到消息或发生错误
        mach_msg_header_t *msg = (mach_msg_header_t *)*buffer; // 将缓冲区指针转换为消息头指针
        msg->msgh_bits = 0; // 重置消息头的标志位
        msg->msgh_local_port = port; // 设置本地端口为指定的端口
        msg->msgh_remote_port = MACH_PORT_NULL; // 设置远程端口为空
        msg->msgh_size = buffer_size; // 设置消息大小为缓冲区大小
        msg->msgh_id = 0; // 重置消息ID
        
        // 根据是否无限超时,调用相应的休眠或轮询函数
        if (TIMEOUT_INFINITY == timeout) {
            CFRUNLOOP_SLEEP(); // 如果是无限超时,调用休眠函数
        } else {
            CFRUNLOOP_POLL(); // 如果有超时时间,调用轮询函数
        }
        
        // 调用mach_msg函数接收消息
        ret = mach_msg(msg,
                       MACH_RCV_MSG | MACH_RCV_LARGE | ((TIMEOUT_INFINITY != timeout) ? MACH_RCV_TIMEOUT : 0) | MACH_RCV_TRAILER_TYPE(MACH_MSG_TRAILER_FORMAT_0) | MACH_RCV_TRAILER_ELEMENTS(MACH_RCV_TRAILER_AV),
                       0, // 不发送消息
                       msg->msgh_size, // 接收消息的大小
                       port, // 使用的端口
                       timeout, // 超时时间
                       MACH_PORT_NULL); // 没有通知端口
        CFRUNLOOP_WAKEUP(ret); // 唤醒RunLoop
        
        // 如果接收消息成功
        if (MACH_MSG_SUCCESS == ret) {
            *livePort = msg ? msg->msgh_local_port : MACH_PORT_NULL; // 设置livePort为本地端口
            return true; // 返回true表示成功
        }
        
        // 如果超时未接收到消息
        if (MACH_RCV_TIMED_OUT == ret) {
            if (!originalBuffer) free(msg); // 如果不是原始缓冲区,则释放内存
            *buffer = NULL; // 将缓冲区指针设为NULL
            *livePort = MACH_PORT_NULL; // 将livePort设为NULL
            return false; // 返回false表示失败
        }
        
        // 如果缓冲区太小,接收到过大的消息
        if (MACH_RCV_TOO_LARGE != ret) break; // 如果不是缓冲区太小的错误,跳出循环
        
        // 重新分配更大的缓冲区
        buffer_size = round_msg(msg->msgh_size + MAX_TRAILER_SIZE); // 计算新的缓冲区大小
        if (originalBuffer) *buffer = NULL; // 如果是原始缓冲区,将其设为NULL
        originalBuffer = false; // 标记缓冲区不再是原始缓冲区
        *buffer = realloc(*buffer, buffer_size); // 重新分配内存
    }
    HALT; // 发生严重错误,终止程序
    return false; // 返回false表示失败
}

休眠:
当没有指定超时时间(即 timeout 为 TIMEOUT_INFINITY)时,线程进入休眠状态。
线程在休眠状态下不会主动消耗 CPU 资源,直到有事件发生唤醒线程。
适用于需要长时间等待事件的场景。

轮询:
当指定了超时时间时,线程不会一直挂起,而是会定期进行检查。
线程在轮询期间会消耗一定的 CPU 资源,但可以更及时地响应事件。
适用于需要在有限时间内频繁检查事件的场景。

该方法接收指定内核端口的消息,并将消息缓存在缓存区,供外界获取。该方法的核心是mach_msg方法,该方法实现消息的发送和接收。RunLoop调用这个函数去接收消息,如果没有接收到port的消息,内核会将线程置于等待状态。

RunLoop事件处理

处理事件主要涉及到如下几个方法:

  • __CFRunLoopDoObservers:处理通知事件。
  • __CFRunLoopDoBlocks:处理block事件。
  • __CFRunLoopDoSources0:处理source0事件。
  • __CFRunLoopDoSource1:处理source1事件。
  • __CFRunLoopDoTimers:处理定时器。
  • CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE:GCD主队列

上述方法回调上层的方法对应如下图所示:
在这里插入图片描述

小结

RunLoop的运行必定要指定一种mode,并且该mode必须注册任务事件。
RunLoop是在默认mode下运行的,当然也可以指定一种mode运行,但是只能在一种mode下运行。
RunLoop内部实际上是维护了一个do-while循环,线程就会一直留在这个循环里面,直到超时或者手动被停止。
RunLoop 的核心就是一个 mach_msg() ,RunLoop 调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态,否则线程处理事件。

RunLoop应用

NSTimer

我们创建timer时,之所以timer能运行,是因为创建timer时,一般情况下,是在主线程中创建,这时会默认将timer以defaultRunloopModel的类型加入主线程,而主线程的runloop对象默认是打开的,从而timer可以运行。
系统会将NSTimer自动加入NSDefaultRunLoopMode模式中,所以以下两段代码含义相同:

[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

NSTimer在滑动时停止工作的问题

当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode
当我们进行拖拽时,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这个模式下没有添加该NSTimer以及其事件,所以我们的NSTimer就不工作了
当我们松开鼠标时候,RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了。

如何解决NSTimer在滑动时停止工作

根据前面学的,NSRunLoopCommonModesNSDefaultRunLoopModeNSEventTrackingRunLoopMode的集合,因此如果想要让timer时刻都不会停止工作,可以使用NSRunLoopCommonModes,即直接让NSTimer在两种mode下都能工作。

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

或者:

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

NSTimer不准确的问题

  • 在RunLoop循环过程中,被NSTimer触发事件阻塞了,导致循环不能及时进行下去,延误之后NSTimer触发时间。
  • 在RunLoop循环过程中,在某一时刻主线程发生了阻塞情况,导致循环不能及时进行下去,厌恶NSTimer触发时间。
  • 在RunLoop循环过程中,发生了模式的转换,(比如UIScrollView的滑动) 导致原有模式下的NSTimer不会正常触发。(虽然可以指定NSTimer所处模式为NSRunLoopCommonModes,但是这种解决方法并不能改变run loops在特定模式下不能处理其余模式事件的本质。)
    以上情况都是由于NSTimer所依赖的run loops会被多种原因干扰正常循环,所以要想解决NSTimer精度问题,就要避免所依赖的run loops被外界干扰

解决办法:

  • 尽量避免将NSTimer放入容易受到影响的主线程runloops中。
  • 尽量避免将耗时操作放入NSTimer依赖的线程中。
  • 尽量避免在NSTimer触发事件中进行耗时操作,如果不能避免,将耗时操作移至其余线程进行。

GCD计时器

不过GCD定时器不同,GCD的线程管理是通过系统来直接管理的。GCD Timer是通过dispatch port给 RunLoop发送消息,来使RunLoop执行相应的block,如果所在线程没有RunLoop,那么GCD 会临时创建一个线程去执行block,执行完之后再销毁掉,因此GCD的Timer是不依赖RunLoop的。

RunLoop 响应用户操作的大致流程:

  1. 当用户进行某个操作(例如点击按钮)时,操作系统会将该事件发送给应用程序。
  2. 应用程序主线程会收到这个事件,并将其添加当前线程的 RunLoop 中。
  3. RunLoop 开始运行,并进入一个循环状态,不断地检查是否事件需要处理。
  4. 如果有事件需要处理,RunLoop 会将事件从队列中取出,并将其分发给相应的处理器(例如事件响应方法)。
  5. 处理器执行相应的代码处理事件,可能包括更新用户界面、执行业务逻辑等操作。
  6. 处理完事件后,RunLoop 继续循环,等下一个事件的到来。
  7. RunLoop 的关键是在循环中不断检查事件,并及时分发给处理器这样可以保证用户操作的响应速度,并且免阻塞主线程。

AutoReleasePool与RunLoop

App启动后,苹果会给RunLoop注册很多个observers,其中有两个是跟自动释放池相关的,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()

  • 第一个observer监听的是activities=0x1(kCFRunLoopEntry),也就是在即将进入loop时,其回调会调用_objc_autoreleasePoolPush() 创建自动释放池;

  • 第二个observer监听的是activities = 0xa0(kCFRunLoopBeforeWaiting | kCFRunLoopExit),即监听的是准备进入睡眠和即将退出loop两个事件。在准备进入睡眠之前,因为睡眠可能时间很长,所以为了不占用资源先调用_objc_autoreleasePoolPop()释放旧的释放池,并调用_objc_autoreleasePoolPush() 创建新建一个新的,用来装载被唤醒后要处理的事件对象;在最后即将退出loop时则会调用_objc_autoreleasePoolPop()释放池子。

    再即将进入loop时,会调用_objc_autoreleasePoolPush() 创建自动释放池;
    在准备睡眠时,先调用_objc_autoreleasePoolPop()释放旧的释放池,后调用_objc_autoreleasePoolPush() 创建新建一个新的,用来装载被唤醒后要处理的事件对象;
    在即将退出loop时,会调用_objc_autoreleasePoolPop()释放池子。

卡顿检测

可以通过RunLoop的不同状态来做页面刷新的卡顿检测。

线程常驻与线程保活

线程常驻

有的时候我们需要创建一个线程在后台一直做一些任务(比如比如后台播放音乐、下载文件等等,我们希望这条线程永远常驻内存),但是常规的线程在任务完成后就会立即销毁,因此我们需要一个常驻线程来让线程一直都存在。

- (void)viewDidLoad {
    [super viewDidLoad];
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
    [self.thread start];
}

- (void)run {
    NSRunLoop *currentRl = [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    //上面这段代码和下面的代码实现的效果相同,让runloop无限期运行下去
//    [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
//    [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
    [currentRl run];
}

- (void)run2
{
    NSLog(@"常驻线程");
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}

上面的代码就是一个常驻线程。把线程thread添加在到RunLoop中,通常RunLoop启动前必须要设置一个mode,并且为mode至少设置一个Source/Timer/Observer,在这里是添加了一个port,虽然消息可以通过port发送到RunLoop内,但是这里并没有发送任何的消息,所以这样便可以保持RunLoop不退出,s实现线程常驻。

NSRunLoop 需要至少一个输入源(如 NSPort)才能保持运行状态。如果 NSRunLoop 没有任何输入源,它会立即退出。通过添加一个 NSPort,可以确保 NSRunLoop 不会退出,从而使线程保持活跃状态。

线程保活

定义:线程保活是指一个线程在空闲时不立即销毁,而是保留一段时间以备后续任务使用。如果在保留时间内有新任务到来,线程可以重新被利用;如果超时未被利用,线程将被销毁。
我们不应该使用 run 方法来启动 RunLoop,因为它创建的是一个不会退出的循环,使用这个方法的子线程自然无法被销毁。我们可以像run 一样利用runMode:beforeDate: 方法来创建一个符合我们条件的子线程:

[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

把它放到一个 while 循环中,利用一个是否停止 RunLoop 的全局标记来辅助处理线程的生命周期问题

__weak typeof(self) weakSelf = self;
    self.thread = [[LSThread alloc] initWithBlock:^{
        NSLog(@"func -- %s   thread -- %@", __func__, [NSThread currentThread]);
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
//        [[NSRunLoop currentRunLoop] run];
        while (!weakSelf.isStopedThread) {
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        NSLog(@"--- 结束 ---");
    }];

停止 RunLoop

- (void)stop {
    self.isStopedThread = YES;
    CFRunLoopStop(CFRunLoopGetCurrent());
}

停止操作一定要在我们的目标线程执行,比如我们直接调用 stop 方法并不能达到我们预期的效果,这是因为stop 默认在主线程执行,没有拿到目标线程,停止无效。

两者的区别
生命周期管理:
线程常驻:线程一旦创建后会一直保持活动状态,不会退出,直到明确调用退出方法。适用于需要长期运行的后台任务。
线程保活:线程在空闲时会保留一段时间,等待新任务。在超时后,线程会被销毁。适用于短期任务较多但不需要长期保持线程的场景。

资源消耗:
线程常驻:会持续占用系统资源,即使在没有任务执行时也是如此。因此需要确保此类线程确实有长期任务以避免浪费资源。
线程保活:在空闲时资源占用较低,只有在有任务执行时才会占用系统资源。

实现复杂度:
线程常驻:相对简单,通过 NSRunLoop 或者循环机制可以轻松实现。
线程保活:需要更多的逻辑来管理线程的生命周期,如线程池、超时机制等。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2109189.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【转载】golang内存分配

Go 的分配采用了类似 tcmalloc 的结构.特点: 使用一小块一小块的连续内存页, 进行分配某个范围大小的内存需求. 比如某个连续 8KB 专门用于分配 17-24 字节,以此减少内存碎片. 线程拥有一定的 cache, 可用于无锁分配. 同时 Go 对于 GC 后回收的内存页, 并不是马上归还给操作系…

Android13 Hotseat客制化--Hotseat修改布局、支持滑动、去掉开机弹动效果、禁止创建文件夹

需求如题&#xff0c;实现效果如下 &#xff1a; 固定Hotseat的padding位置、固定高度 step1 在FeatureFlags.java中添加flag,以兼容原生态代码 public static final boolean STATIC_HOTSEAT_PADDING true;//hotseat area fixed step2:在dimens.xml中添加padding值和高度值…

信息系统安全保障

关注这个证书的其他相关笔记&#xff1a;NISP 一级 —— 考证笔记合集-CSDN博客 0x01&#xff1a;信息系统 信息系统是具有集成性的系统&#xff0c;每一个组织中信息流动的综合构成一个信息系统。信息系统是根据一定的需要进行输入、系统控制、数据处理、数据存储与输出等活动…

职场关系课:职场上的基本原则(安全原则、进步原则、收益原则、逃生舱原则)

文章目录 引言安全原则进步原则收益原则逃生舱原则引言 职场上的王者,身体里都应该有三个灵魂: 一个文臣,谨小慎微,考虑风险; 一个武将,积极努力,谋求胜利; 一个商人,精打细算,心中有数。 安全原则 工作安全:保住自己的工作和位置信用安全:保住个人的信用,如果领…

《征服数据结构》差分数组

摘要&#xff1a; 1&#xff0c;差分数组的介绍 2&#xff0c;二维差分数组的介绍 1&#xff0c;差分数组的介绍 差分数组主要是操作区间的&#xff0c;关于区间操作的数据结构比较多&#xff0c;除了前面讲的《稀疏表》&#xff0c;还有树状数组&#xff0c;线段树&#xff0c…

高德地图SDK Android版开发 10 InfoWindow

高德地图SDK Android版开发 10 InfoWindow 前言相关类和方法默认样式Marker类AMap类AMap.OnInfoWindowClickListener 接口 自定义样式(视图)AMap 类AMap.ImageInfoWindowAdapter 接口 自定义样式(Image)AMap.ImageInfoWindowAdapter 接口 示例界面布局MapInfoWindow类常量成员变…

215篇【大模型医疗】论文合集(附PDF)

ChatGPT的横空出世引发了新一轮生成式大模型热潮&#xff0c;作为最新技术的"试验场"&#xff0c;医疗也成为众多大模型的热门首选。 我整理了215篇医疗和大模型的论文&#xff0c;供大家学习和参考。 领215篇医疗和大模型论文

欧拉下搭建第三方软件仓库—docker

1.创建新的文件内容 切换目录到etc底下的yum.repos.d目录&#xff0c;创建docker-ce.repo文件 [rootlocalhost yum.repos.d]# cd /etc/yum.repos.d/ [rootlocalhost yum.repos.d]# vim docker-ce.repo 编辑文件,使用阿里源镜像源&#xff0c;镜像源在编辑中需要单独复制 h…

vue3的学习(2)

属性绑定 1.将一个容器中的class和id使用vue用法赋上具体的值&#xff0c;这样就可以动态的给容器添加上自己想要给其添加的class或者id或者title。 2.关键语法&#xff0c;在容器中的class或者id或者title前面加上 "v-bind:"&#xff0c;当加上"v-bind关键语…

计算机网络基础 - 应用层(2)

计算机网络基础 应用层FTP 与 EMail文件传输协议 FTP电子邮件 EMail主要组成部分SMTP概述SMTP 与 HTTP1.1 邮件报文格式报文格式多媒体扩展 MIME 邮件访问协议概述POP3IMAP DNS概述域名结构工作机理集中式设计分布式、层次数据库根 DNS 服务器顶级域 DNS 服务器权威 DNS 服务器…

公司的企业画册如何制作?

企业画册是公司形象和产品服务展示的重要载体&#xff0c;一个制作精良的企业画册不仅能展示公司的实力&#xff0c;也能提升客户对公司专业度的认可。以下是制作企业画册的步骤和要点&#xff0c;帮助你的公司画册既美观又实用。 1.要制作电子杂志,首先需要选择一款适合自己的…

OpenCV结构分析与形状描述符(7)计算轮廓的面积的函数contourArea()的使用

操作系统&#xff1a;ubuntu22.04 OpenCV版本&#xff1a;OpenCV4.9 IDE:Visual Studio Code 编程语言&#xff1a;C11 算法描述 计算轮廓的面积。 该函数计算轮廓的面积。与 moments 类似&#xff0c;面积是使用格林公式计算的。因此&#xff0c;返回的面积与你使用 drawCo…

Mysql中的隐式COMMIT以及Savepoints的作用以及MySQL的Innodb分空间存储、设计优化、索引等几个小知识点整理

一、Mysql中的隐式COMMIT以及Savepoints的作用 Mysql默认是自动提交的&#xff0c;如果要开启使用事务&#xff0c;首先要关闭自动提交后START TRANSACTION 或者 BEGIN 来开始一个事务&#xff0c;使用ROLLBACK/COMMIT来结束一个事务。但即使如此&#xff0c;也并不是所有的操作…

零知识证明在BSV网络上的应用

​​发表时间&#xff1a;2023年6月15日 2024年7月19日&#xff0c;BSV区块链主网上成功通过使用零知识证明验证了一笔交易。 零知识证明是一种技术&#xff0c;它允许一方&#xff08;证明者&#xff09;在不透露任何秘密的情况下&#xff0c;向另一方&#xff08;验证者&…

TP-link-路由器上网设置(已有路由器再连接新的网线)

一、192.168.0.1进入管理界面&#xff0c;比如密码&#xff1a;D804D804。 二、这是设置连接账户和密码&#xff08;比如账户&#xff1a;TP-LINK_F56C 比如密码&#xff1a;D804D804&#xff09;登录后台管理、移动设备连接。比较固定。 三、 有的网络是分&#xff1a;&#…

JS面试真题 part1

JS面试真题 part1 1、说说JavaScript中的数据类型&#xff0c;储存上的差别2、说说你了解的js数据结构3、DOM常见的操作有哪些4、说说你对BOM的理解&#xff0c;常见的BOM对象你了解哪些5、 和 区别&#xff0c;分别在什么情况使用 1、说说JavaScript中的数据类型&#xff0c;…

K8s的Pv和Pvc就是为了pod数据持久化

一、 1.pv&#xff08;persistent volume&#xff09;&#xff1a;是k8s虚拟化的存储资源&#xff0c;实际上就是存储&#xff0c;列如本地的硬盘、网络文件系统&#xff08;Nfs&#xff09;、lvm、RAID、云存储。 2.pvc&#xff1a;pod对存储资源的请求&#xff0c;定义了需…

MyBatis:解决数据库字段和Java对象字段不匹配问题及占位符问题

MyBatis&#xff1a;解决数据库字段和Java对象字段不匹配问题及占位符问题 文章目录 MyBatis&#xff1a;解决数据库字段和Java对象字段不匹配问题及占位符问题一、数据库字段和Java对象字段不匹配问题1、问题描述2、解决方案2.1、方案12.2、方案22.3、方案3 二、占位符问题1、…

ELK在Linux上部署教程

Docker Compose搭建ELK Elasticsearch默认使用mmapfs目录来存储索引。操作系统默认的mmap计数太低可能导致内存不足&#xff0c;我们可以使用下面这条命令来增加内存 sysctl -w vm.max_map_count262144创建Elasticsearch数据挂载路径 mkdir -p /echola/elasticsearch/data对…

Day 3 - 5 :线性表 — 单链表

存储结构 将线性表中的各元素分布在存储器的不同存储块&#xff0c;称为结点。 结点的data域存放数据元素ai&#xff0c;而next域是一个指针&#xff0c;指向ai的直接后继ai1所在的结点。 如果要删除a1&#xff0c;只要修改a1前手元素指针的指向即可。 例如&#xff1a;需要找到…