【iOS】——RunLoop学习

文章目录

  • 一、RunLoop简介
    • 1.RunLoop介绍
    • 2.RunLoop功能
    • 3.RunLoop使用场景
    • 4.Run Loop 与线程
    • 5.RunLoop源代码和模型图
  • 二、RunLoop Mode
    • 1.CFRunLoopModeRef
    • 2.RunLoop Mode的五种模式
    • 3.RunLoop Mode使用
  • 三、RunLoop Source
    • 1.CFRunLoopSourceRef
      • sourc0:
      • source1:
    • 2.CFRunLoopTimerRef
    • 3.CFRunLoopObserverRef
  • 四、RunLoop实现
    • 1.获取RunLoop对象
    • 2.添加Mode
    • 3.添加Run Loop Source(ModeItem)
  • 五、RunLoop实现逻辑


一、RunLoop简介

1.RunLoop介绍

一个线程一次只能执行一个任务,执行完毕后就会退出。如果需要一个机制,让线程能随时处理事件,处理完毕后并不退出,代码逻辑是这样的:

do {//获取消息//处理消息
} while (消息 != 退出)

这种模型通常被称为Event Loop,Event loop 在很多系统和框架都有实现。例如:Node.js 的事件处理,Windows 程序的消息循环,而在macOS、iOS中就是Run Loop。
简单的说RunLoop其实就是一种循环,通过这种循环得以让应用程序持续运行,让线程能随时处理事件,并在线程需要进行事件处理时忙起来,在不需要进行事件处理时闲下来。

2.RunLoop功能

  • 保持程序的持续运行
  • 处理app中各种事件
  • 节省CPU资源,提高程序性能:该做事时做事,该休眠时休眠。并且休眠时不占用CPU

3.RunLoop使用场景

只有在创建辅助线程时,才需要显式启动RunLoop。app 启动过程中主线程会自动启动RunLoop。即使用 Xcode 提供的模版创建 app,无需为主线程显式启动RunLoop。

RunLoop用于需要与线程交互的情况。对于辅助线程,根据需要配置、启动 RunLoop,但并非所有线程都需要使用 RunLoop。例如,执行一些长时间运行的固定任务,则无需启动RunLoop。如有以下需求,则需要启动RunLoop:

  1. 使用端口或自定义 input source 与其他线程通讯。
  2. 在线程上使用计时器。
  3. 使用任一 performSelector 方法。
  4. 线程需要定期执行任务。

RunLoop 的配置和启动也很简单,但需要在某些条件下退出 RunLoop。最好清理所有工作后退出RunLoop,而非强制终止线程。

4.Run Loop 与线程

下图为RunLoop与线程的关系:
在这里插入图片描述
Runloop 在线程中的作用主要是从 input source 和 timer source 接受事件,然后在线程中处理事件。

输入源(Input source):异步传递事件,通常是从其他线程、app 发送的消息。
计时器源(Timer source):同步传递事件,这些事件在计划的时间、间隔触发。
Input source 传递异步事件到对应处理程序,并退出runUntilDate:方法。Timer source 传递事件到对应处理程序,但不会退出 run loop。

runUntilDate:方法在指定时间到达前会保持RunLoop 处于运行状态,RunLoop运行时会处理 input source 的各种事件。

Runloop 和线程是绑定在一起的。每个线程(包括主线程)都有一个对应的 Runloop 对象。我们并不能自己创建 Runloop 对象,但是可以获取到系统提供的 Runloop 对象。
主线程的 Runloop 会在应用启动的时候完成启动,其他线程的 Runloop 默认并不会启动,需要我们手动启动。

除处理 input source 传递的任务,RunLoop还会发送其当前状态的通知。注册观察RunLoop状态变化,可以在状态变化时进行额外工作。使用 Core Foundation 的CFRunLoopObserverCreate()CFRunLoopObserverCreateWithHandler()方法创建观察者,CFRunLoopAddObserver()方法添加观察者,最后使用CFRelease()释放observer。

5.RunLoop源代码和模型图

在这里插入图片描述

struct __CFRunLoop {CFRuntimeBase _base;pthread_mutex_t _lock;  /* locked for accessing mode list */__CFPort _wakeUpPort;   // used for CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloopBoolean _unused;volatile _per_run_data *_perRunData; // reset for runs of the run looppthread_t _pthread;             //RunLoop对应的线程uint32_t _winthread;CFMutableSetRef _commonModes;    //存储的是字符串,记录所有标记为common的modeCFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)CFRunLoopModeRef _currentMode;   //当前运行的modeCFMutableSetRef _modes;          //存储的是CFRunLoopModeRefstruct _block_item *_blocks_head;//doblocks的时候用到struct _block_item *_blocks_tail;CFTypeRef _counterpart;
};

通过代码不难发现RunLoop其实也是个对象,一个RunLoop对象,主要包含了一个线程,若干个Mode,若干个commonMode,若干个commonModeItems还有一个当前运行的Mode(currentMode)。

二、RunLoop Mode

1.CFRunLoopModeRef

RunLoop的Mode属于CFRunLoopModeRef类,其代码结构如下:

typedef struct __CFRunLoopMode *CFRunLoopModeRef;struct __CFRunLoopMode {CFRuntimeBase _base;pthread_mutex_t _lock;    /* must have the run loop locked before locking this */CFStringRef _name; //mode名称,运行模式是通过名称来识别的Boolean _stopped; //mode是否被终止char _padding[3];//整个结构体最核心的部分
------------------------------------------CFMutableSetRef _sources0; // Sources0CFMutableSetRef _sources1; // Sources1CFMutableArrayRef _observers; // 观察者CFMutableArrayRef _timers; // 定时器
------------------------------------------CFMutableDictionaryRef _portToV1SourceMap;//字典    key是mach_port_t,value是CFRunLoopSourceRef__CFPortSet _portSet;//保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERSdispatch_source_t _timerSource;dispatch_queue_t _queue;Boolean _timerFired; // set to true by the source when a timer has firedBoolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOOmach_port_t _timerPort;Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWSDWORD _msgQMask;void (*_msgPump)(void);
#endifuint64_t _timerSoftDeadline; /* TSR */uint64_t _timerHardDeadline; /* TSR */
};
  • 通过源代码可以看出一个CFRunLoopModeRef对象有一个name,若干source0,source1,timer,observer和port,可以看出来事件都是由mode在管理,而RunLoop管理着Mode。
  • Runloop Mode 实际上是 Source,Timer 和 Observer 的集合,不同的 Mode 把不同组的
    Source,Timer 和 Observer 隔绝开来。Runloop 在某个时刻只能跑在一个 Mode 下(这个 mode 被称作 currentMode),处理这一个 Mode当中的 Source,Timer 和 Observer。如果需要切换 mode,需退出 loop,重新进入要切换的 mode。这样做可以隔离不同组 source、timer、observer,让其互不影响。

正如下图所示:
在这里插入图片描述
Mode 可用于过滤不需要的 source。大部分情况下,使用系统定义的 default mode 即可。UIScrollView滑动时主线程会进入UITrackingRunLoopMode。此时,只有与UITrackingRunLoopMode关联的 mode 可以向主线程传递事件。对于辅助线程,可以使用自定义 mode 来防止低优先级 source 在时间紧迫的操作期间传递事件。

2.RunLoop Mode的五种模式

  • NSDefaultRunLoopMode:App的默认Mode,通常主线程是在这个Mode下运行
  • NSConnectionReplyMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  • NSModalPanelRunLoopMode:当应用程序显示一个模态对话框时使用此模式。在此模式下,RunLoop只处理与模态面板相关的事件,忽略其他非模态事件,确保用户只能与模态面板交互。
  • NSEventTrackingRunLoopMode:接受系统事件的内部 Mode,通常用不到
  • NSRunLoopCommonModes:这不是一个单一的模式,而是一个特殊的集合,包含了多个模式,其中最典型的就是默认模式(NSDefaultRunLoopMode)和事件跟踪模式(NSEventTrackingRunLoopMode)。将输入源(如定时器)添加到NSRunLoopCommonModes,意味着它会在所有包含的模式下有效,即使RunLoop切换模式也不会停止工作。

CommonModes:一个 Mode 可以将自己标记为”Common”属性(通过将其 ModeName 添加到 RunLoop 的
“commonModes” 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的
Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

3.RunLoop Mode使用

CFRunLoop对外暴露的管理 Mode 接口只有下面2个:

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);
  • 只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 但 RunLoop 内部没有对应 mode
    时,RunLoop会自动帮你创建对应的 CFRunLoopModeRef。
  • 对于一个 RunLoop 来说,其内部的 mode 只能增加不能删除。

官方公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode)UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。

公开提供的 Mode 有两个:kCFRunLoopDefaultMode (NSDefaultRunLoopMode)UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。

同时官方还提供了一个操作 Common 标记的字符串:kCFRunLoopCommonModes (NSRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时注意区分这个字符串和其他 mode name。


三、RunLoop Source

Run Loop Source分为Source、Observer、Timer三种,他们统称为ModeItem。

1.CFRunLoopSourceRef

根据官方描述,CFRunLoopSourceRef是input sources的抽象。
CFRunLoopSource分为Source0Source1两个版本。
它的结构如下:

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

sourc0:

是App内部事件,由App自己管理的UIEvent、CFSocket都是source0。当一个source0事件准备执行的时候,必须要先把它标记为signal状态,以下是source0的结构体:

typedef struct {CFIndex	version;void *	info;const void *(*retain)(const void *info);void	(*release)(const void *info);CFStringRef	(*copyDescription)(const void *info);Boolean	(*equal)(const void *info1, const void *info2);CFHashCode	(*hash)(const void *info);void	(*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);void	(*cancel)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);void	(*perform)(void *info);
} CFRunLoopSourceContext;

source0是非基于Port的。只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。适用于那些不需要底层硬件或系统事件驱动的场景,比如基于条件的任务,当满足某个条件时手动触发。

source1:

source1由RunLoop和内核管理,source1带有mach_port_t,可以接收内核消息并触发回调,以下是source1的结构体:

typedef struct {CFIndex version;void *  info;const void *(*retain)(const void *info);void    (*release)(const void *info);CFStringRef (*copyDescription)(const void *info);Boolean (*equal)(const void *info1, const void *info2);CFHashCode  (*hash)(const void *info);
#if (TARGET_OS_MAC && !(TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)) || (TARGET_OS_EMBEDDED || TARGET_OS_IPHONE)mach_port_t (*getPort)(void *info);void *  (*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#elsevoid *  (*getPort)(void *info);void    (*perform)(void *info);
#endif
} CFRunLoopSourceContext1;

Source1包含了mach_port和一个回调(函数指针),Source1可以监听系统端口,通过内核和其他线程通信,接收、分发系统事件,他能主动唤醒RunLoop(由操作系统内核进行管理)。并且Source1在处理的时候会分发一些操作给Source0去处理。广泛应用于处理系统级事件,如触摸事件、摇晃、锁屏等,以及进程间通信(IPC)。由于它直接与系统内核和其他进程或线程的事件交互,因此可以实现高效的事件传递和响应。

2.CFRunLoopTimerRef

CFRunLoopTimer是基于时间的触发器,其包含一个时间长度、一个回调(函数指针)。当其加入runloop时,runloop会注册对应的时间点,当时间点到时,runloop会被唤醒以执行那个回调。
并且CFRunLoopTimer和NSTimer是toll-free bridged(对象桥接),可以相互转换。其结构如下:

struct __CFRunLoopTimer {CFRuntimeBase _base;uint16_t _bits;  //标记fire状态pthread_mutex_t _lock;CFRunLoopRef _runLoop;        //添加该timer的runloopCFMutableSetRef _rlModes;     //存放所有 包含该timer的 mode的 modeName,意味着一个timer可能会在多个mode中存在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 source 在预设时间将事件同步传递到线程。Timer 是线程通知自己执行任务的一种方式。例如,用户在搜索框键入字符时,使用计时器间隔指定时间搜索一次,以便用户输入更多文字,也可以提高性能。

尽管 Timer 会产生基于时间的通知,但它不是实时的机制。与 input source 一样,timer 也与RunLoop Mode 关联。如果计时器触发时,RunLoop正在执行其他回调,计时器会等待 RunLoop执行其回调;如果RunLoop未运行,timer 永不触发。

计时器可以配置为一次性或重复生成事件。重复计时器会根据计划的触发时间,而非实际触发时间,自动重新计划自身。例如,计时器计划在指定时间触发,并每5秒触发一次,则触发时间永远会落在原始时间的5秒间隔上。如果触发时间被延误很多,导致缺少一个或多个触发时间,计时器将在错过的时间段内仅触发一次。此后,计时器将重新计划下一个计划的触发时间。

对于NSTimer,当我们用scheduledTimerWithTimeInterval方法初始化定时器时

[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

系统会将NSTimer自动加入NSDefaultRunLoopMode模式中,所以所以它就等同于下面代码:

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

3.CFRunLoopObserverRef

CFRunLoopObserverRef是观察者可以观察Runloop的各种状态,每个Observer都包含了一个回调(函数指针),当RunLoop的状态发生变化时,观察者就能通过回调接收到这个变化。

typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver {CFRuntimeBase _base;pthread_mutex_t _lock;CFRunLoopRef _runLoop;//监听的RunLoopCFIndex _rlCount;//添加该Observer的RunLoop对象个数CFOptionFlags _activities;		/* immutable */CFIndex _order;//同时间最多只能监听一个CFRunLoopObserverCallBack _callout;//监听的回调CFRunLoopObserverContext _context;//上下文用于内存管理
};

Run loop 可观察时间点有以下几个:

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {kCFRunLoopEntry = (1UL << 0),           // 即将进入 loopkCFRunLoopBeforeTimers = (1UL << 1),    // 即将处理 TimerkCFRunLoopBeforeSources = (1UL << 2),   // 即将处理 SourcekCFRunLoopBeforeWaiting = (1UL << 5),   // 即将进入休眠kCFRunLoopAfterWaiting = (1UL << 6),    // 刚从休眠中唤醒,但还未执行将其唤醒的任务。kCFRunLoopExit = (1UL << 7),            // 即将退出 loopkCFRunLoopAllActivities = 0x0FFFFFFFU   // 观察所有状态变化
};

Runloop 通过监控 Source 来决定有没有任务要做,除此之外,我们还可以用 Runloop Observer 来监控 Runloop 本身的状态。 Runloop Observer 可以监控上面的 Runloop 事件,具体流程如下图:
在这里插入图片描述
这六种状态都可以被observer观察到,我们也可以利用这一方法写一些特殊事件,创建监听,监听RunLoop的状态变化:

    // 创建 observerCFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {switch (activity) {case kCFRunLoopEntry:NSLog(@"kCFRunLoopEntry -- %@", CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent()));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:NSLog(@"default");break;}});// 将 observer 添加到主线程的 run loop 的 common modesCFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);// 释放 observerCFRelease(observer);

在这里插入图片描述

四、RunLoop实现

1.获取RunLoop对象

需要知道的是RunLoop对象不能手动创建,前面提到每个线程都有一个RunLoop对象所以只能通过线程来获取对应的RunLoop对象。
Runloop对象主要有两种获取方式:

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

NSRunloop类是Fundation框架中Runloop的对象,并且NSRunLoop是基于CFRunLoopRef的封装,提供了面向对象的API,但是这些API不是线程安全的。

// Core Foundation
CFRunLoopRef runloop = CFRunLoopGetCurrent(); // 获得当前RunLoop对象
CFRunLoopRef runloop = CFRunLoopGetMain(); // 获得主线程的RunLoop对象

CFRunLoopRef类是CoreFoundation框架中Runloop的对象,并且其提供了纯C语言函数的API,所有这些API都是线程安全。

CoreFoundation框架中这两个函数的具体实现:

CFRunLoopRef CFRunLoopGetCurrent(void) {CHECK_FOR_FORK();CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);if (rl) return rl;return _CFRunLoopGet0(pthread_self());
}CFRunLoopRef CFRunLoopGetMain(void) {CHECK_FOR_FORK();static CFRunLoopRef __main = NULL; // no retain neededif (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS neededreturn __main;
}

这两个方法里都是调用了_CFRunLoopGet0这个方法实现传入线程的函数,下面看下CFRunLoopGet0的结构:

static CFMutableDictionaryRef __CFRunLoops = NULL;
static CFSpinLock_t loopsLock = CFSpinLockInit;// t==0 is a synonym for "main thread" that always works
//根据线程取RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {if (pthread_equal(t, kNilPthreadT)) {t = pthread_main_thread_np();}__CFSpinLock(&loopsLock);//如果存储RunLoop的字典不存在if (!__CFRunLoops) {__CFSpinUnlock(&loopsLock);//创建一个临时字典dictCFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);//创建主线程的RunLoopCFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());//把主线程的RunLoop保存到dict中,key是线程,value是RunLoopCFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);//此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoopsif (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {//释放dictCFRelease(dict);}//释放mainrunloopCFRelease(mainLoop);__CFSpinLock(&loopsLock);}//以上说明,第一次进来的时候,不管是getMainRunloop还是get子线程的runloop,主线程的runloop总是会被创建//从字典__CFRunLoops中获取传入线程t的runloopCFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));__CFSpinUnlock(&loopsLock);//如果没有获取到if (!loop) {//根据线程t创建一个runloopCFRunLoopRef newLoop = __CFRunLoopCreate(t);__CFSpinLock(&loopsLock);loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));if (!loop) {//把newLoop存入字典__CFRunLoops,key是线程tCFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);loop = newLoop;}// don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it__CFSpinUnlock(&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;
}

这段代码可以得出以下结论:

  • RunLoop和线程的一一对应的,对应的方式是以key-value的方式保存在一个全局字典中
  • 主线程的RunLoop会在初始化全局字典时创建
  • 子线程的RunLoop会在第一次获取的时候创建,如果不获取的话就一直不会被创建
  • RunLoop会在线程销毁时销毁

2.添加Mode

对于Mode苹果官方提供了以下三个接口:

//向当前RunLoop的common modes中添加一个mode。
CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef mode)
//返回当前运行的mode的name
CFRunLoopCopyCurrentMode(CFRunLoopRef rl)
//返回当前RunLoop的所有mode
CFRunLoopCopyAllModes(CFRunLoopRef rl)

我们没有办法直接创建一个CFRunLoopMode对象,但是我们可以调用CFRunLoopAddCommonMode方法传入一个字符串向RunLoop中添加Mode,传入的字符串即为Mode的名字,Mode对象应该是此时在RunLoop内部创建的。
下面是CFRunLoopAddCommonMode源码:

void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {CHECK_FOR_FORK();if (__CFRunLoopIsDeallocating(rl)) return;__CFRunLoopLock(rl);//看rl中是否已经有这个mode,如果有就什么都不做if (!CFSetContainsValue(rl->_commonModes, modeName)) {CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;//把modeName添加到RunLoop的_commonModes中CFSetAddValue(rl->_commonModes, modeName);if (NULL != set) {CFTypeRef context[2] = {rl, modeName};/* add all common-modes items to new mode *///这里调用CFRunLoopAddSource/CFRunLoopAddObserver/CFRunLoopAddTimer的时候会调用//__CFRunLoopFindMode(rl, modeName, true),CFRunLoopMode对象在这个时候被创建CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);CFRelease(set);}} else {}__CFRunLoopUnlock(rl);
}
  • modeName不能重复,modeName是mode的唯一标识符
  • RunLoop的_commonModes数组存放所有被标记为common的mode的名称(name)
  • 添加commonMode会把commonModeItems数组中的所有source同步到新添加的mode中
  • CFRunLoopMode对象在CFRunLoopAddItemsToCommonMode函数中调用CFRunLoopFindMode时被创建

3.添加Run Loop Source(ModeItem)

在RunLoop Mode部分已经介绍过管理RunLoop Source的几个接口方法,这里就不再赘述。
CFRunLoopAddSource的代码结构如下:

//添加source事件
void CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef rls, CFStringRef modeName) {    /* DOES CALLOUT */CHECK_FOR_FORK();if (__CFRunLoopIsDeallocating(rl)) return;if (!__CFIsValid(rls)) return;Boolean doVer0Callout = false;__CFRunLoopLock(rl);//如果是kCFRunLoopCommonModesif (modeName == kCFRunLoopCommonModes) {//如果runloop的_commonModes存在,则copy一个新的复制给setCFSetRef set = rl->_commonModes ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModes) : NULL;//如果runl _commonModeItems为空if (NULL == rl->_commonModeItems) {//先初始化rl->_commonModeItems = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);}//把传入的CFRunLoopSourceRef加入_commonModeItemsCFSetAddValue(rl->_commonModeItems, rls);//如果刚才set copy到的数组里有数据if (NULL != set) {CFTypeRef context[2] = {rl, rls};/* add new item to all common-modes *///则把set里的所有mode都执行一遍__CFRunLoopAddItemToCommonModes函数CFSetApplyFunction(set, (__CFRunLoopAddItemToCommonModes), (void *)context);CFRelease(set);}//以上分支的逻辑就是,如果你往kCFRunLoopCommonModes里面添加一个source,那么所有_commonModes里的mode都会添加这个source} else {//根据modeName查找modeCFRunLoopModeRef rlm = __CFRunLoopFindMode(rl, modeName, true);//如果_sources0不存在,则初始化_sources0,_sources0和_portToV1SourceMapif (NULL != rlm && NULL == rlm->_sources0) {rlm->_sources0 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);rlm->_sources1 = CFSetCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeSetCallBacks);rlm->_portToV1SourceMap = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, NULL);}//如果_sources0和_sources1中都不包含传入的sourceif (NULL != rlm && !CFSetContainsValue(rlm->_sources0, rls) && !CFSetContainsValue(rlm->_sources1, rls)) {//如果version是0,则加到_sources0if (0 == rls->_context.version0.version) {CFSetAddValue(rlm->_sources0, rls);//如果version是1,则加到_sources1} else if (1 == rls->_context.version0.version) {CFSetAddValue(rlm->_sources1, rls);__CFPort src_port = rls->_context.version1.getPort(rls->_context.version1.info);if (CFPORT_NULL != src_port) {//此处只有在加到source1的时候才会把souce和一个mach_port_t对应起来//可以理解为,source1可以通过内核向其端口发送消息来主动唤醒runloopCFDictionarySetValue(rlm->_portToV1SourceMap, (const void *)(uintptr_t)src_port, rls);__CFPortSetInsert(src_port, rlm->_portSet);}}__CFRunLoopSourceLock(rls);//把runloop加入到source的_runLoops中if (NULL == rls->_runLoops) {rls->_runLoops = CFBagCreateMutable(kCFAllocatorSystemDefault, 0, &kCFTypeBagCallBacks); // sources retain run loops!}CFBagAddValue(rls->_runLoops, rl);__CFRunLoopSourceUnlock(rls);if (0 == rls->_context.version0.version) {if (NULL != rls->_context.version0.schedule) {doVer0Callout = true;}}}if (NULL != rlm) {__CFRunLoopModeUnlock(rlm);}}__CFRunLoopUnlock(rl);if (doVer0Callout) {// although it looses some protection for the source, we have no choice but// to do this after unlocking the run loop and mode locks, to avoid deadlocks// where the source wants to take a lock which is already held in another// thread which is itself waiting for a run loop/mode lockrls->_context.version0.schedule(rls->_context.version0.info, rl, modeName); /* CALLOUT */}
}

通过添加source的这段代码可以得出如下结论:

  • 如果modeName传入kCFRunLoopCommonModes,则该source会被保存到RunLoop的_commonModeItems中
  • 如果modeName传入kCFRunLoopCommonModes,则该source会被添加到所有commonMode中
  • 如果modeName传入的不是kCFRunLoopCommonModes,则会先查找该Mode,如果没有,会创建一个
    同一个source在一个mode中只能被添加一次

五、RunLoop实现逻辑

//用DefaultMode启动
void CFRunLoopRun(void) {CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}//用指定的Mode启动,允许设置RunLoop的超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}//RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {//首先根据modeName找到对应的modeCFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);//如果该mode中没有source/timer/observer,直接返回if (__CFRunLoopModeIsEmpty(currentMode)) return;//1.通知Observers:RunLoop即将进入loop__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);//调用函数__CFRunLoopRun 进入loop__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {Boolean sourceHandledThisLoop = NO;int retVal = 0;do {//2.通知Observers:RunLoop即将触发Timer回调__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);//3.通知Observers:RunLoop即将触发Source0(非port)回调__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);///执行被加入的block__CFRunLoopDoBlocks(runloop, currentMode);//4.RunLoop触发Source0(非port)回调,处理Source0sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);//执行被加入的Block__CFRunLoopDoBlocks(runloop, currentMode);//5.如果有Source1(基于port)处于ready状态,直接处理这个Source1然后跳转去处理消息if (__Source0DidDispatchPortLastTime) {Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)if (hasMsg) goto handle_msg;}//6.通知Observers:RunLoop的线程即将进入休眠if (!sourceHandledThisLoop) {__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);}//7.调用mach_msg等待接收mach_port的消息。线程将进入休眠,直到被下面某个事件唤醒:// 一个基于port的Source的事件// 一个Timer时间到了// RunLoop自身的超时时间到了// 被其他什么调用者手动唤醒__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg}//8.通知Observers:RunLoop的线程刚刚被唤醒__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);//收到消息,处理消息handle_msg://9.1 如果一个Timer时间到了,触发这个timer的回调if (msg_is_timer) {__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())} //9.2 如果有dispatch到main_queue的block,执行blockelse if (msg_is_dispatch) {__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);} //9.3 如果一个Source1(基于port)发出事件了,处理这个事件else {CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);if (sourceHandledThisLoop) {mach_msg(reply, MACH_SEND_MSG, reply);}}//执行加入到loop的block__CFRunLoopDoBlocks(runloop, currentMode);//设置do-while之后的返回值if (sourceHandledThisLoop && stopAfterHandle) {// 进入loop时参数说处理完事件就返回retVal = kCFRunLoopRunHandledSource;} else if (timeout) {// 超出传入参数标记的超时时间了retVal = kCFRunLoopRunTimedOut;} else if (__CFRunLoopIsStopped(runloop)) {// 被外部调用者强制停止了retVal = kCFRunLoopRunStopped;} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {// source/timer/observer一个都没有了retVal = kCFRunLoopRunFinished;}// 如果没超时,mode里没空,loop也没被停止,那继续loop。} while (retVal == 0);}//10. 通知Observers:RunLoop即将退出__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

实际上RunLoop就是这样的一个函数,其内部是一个do-while循环。当你调用CFRunLoopRun()时,线程就会一直停留在这个循环里,知道超时或者被手动调用,该函数才会返回。

RunLoop回调(流程)

当App启动时,系统会默认注册五个上面说过的5个mode
当RunLoop进行回调时,一般都是通过一个很长的函数调出去(call out),当在代码中加断点调试时,通常能在调用栈上看到这些函数。这就是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启动方法

1.run,无条件
无条件地进入运行循环是最简单的选项,但也是最不理想的选择。无条件地运行runloop将线程放入永久循环,这使您无法控制运行循环本身。停止runloop的唯一方法是杀死它。也没有办法在自定义模式下运行循环。
2.runUntilDate, 设置时间限制
设置了超时时间,超过这个时间runloop结束,优于第一种
3.runMode:beforeDate:,在特定模式下
相对比较好的方式,可以指定runloop以哪种模式运行,但是它是单次调用的,超时时间到达或者一个输入源被处理,则runLoop就会自动退出,上述两种方式都是循环调用的
实际上run方法的实现就是无限调用runMode:beforeDate:方法
runUntilDate:也会重复调用runMode:beforeDate:方法,区别在于它超时就不会再调用
RunLoop关闭方法

1.将运行循环配置为使用超时值运行。
2.手动停止。
这里需要注意,虽然删除runloop的输入源和定时器可能会导致运行循环的退出,但这并不是个可靠的方法,系统可能会添加输入源到runloop中,但在我们的代码中可能并不知道这些输入源,因此无法删除它们,导致无法退出runloop。

我们可以通过上述2、3方法来启动runloop,设置超时时间。但是如果需要对这个线程和它的RunLoop有最精确的控制,而并不是依赖超时机制,这时我们可以通过 CFRunLoopStop()方法来手动结束一个 RunLoop。但是 CFRunLoopStop()方法只会结束当前正在执行的这次runMode:beforeDate:调用,而不会结束后续runloop的调用。

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

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

相关文章

Vue中使用$t(‘xxx‘)实现中英文切换;

&#xff08;原文链接&#xff09; 介绍 {{$t(key)}} &#xff1a;是VueI18n插件提供的函数&#xff0c;主要用于根据当前语言环境返回对应的翻译文本&#xff0c;以便在页面上显示多语言内容。 key&#xff1a;作为参数传递给函数$t()的字符串&#xff0c;用于指定需要翻译的…

基于springboot+vue+Mysql的在线BLOG网

开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8服务器&#xff1a;tomcat7数据库&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/ideaMaven包&#xff1a;…

虾皮选品:Shopee首季盈利2.4亿;TikTok美区电商权限要求降低

2024年5月14号&#xff0c;跨境电商日报&#xff1a; 1.Ozon已成功回款 2.TikTok降低美区达人开通电商权限要求 3.Shopee首季盈利2.4亿 4.6月1日起&#xff0c;亚马逊退货处理费收取标准更新 5.欧盟委员会对从中国台湾地区和越南进口的不锈钢冷轧产品征收反补贴和反倾销税…

在数据库中使用存储过程插入单组/多组数据

存储过程可以插入单组数据&#xff0c;也可以以字符串的形式插入多组数据&#xff0c;将字符串中的信息拆分成插入的数据。 首先建立一个简单的数据库 create database student; use student;选中数据库之后建立一张学生表 create table stu(uid int primary key,uname varc…

wordpress 访问文章内容页 notfound

解决&#xff1a; 程序对应的伪静态规则文件.htaccess是空的 网站根目录下要有 .htaccess 文件&#xff0c;然后将下面的代码复制进去。 <ifmodule mod_rewrite.c>RewriteEngine OnRewriteBase /RewriteRule ^index\.php$ - [L]RewriteCond %{REQUEST_FILENAME} !-fRew…

python模拟QQ聊天的代码

以下是一个简单的Python模拟QQ聊天的代码示例&#xff1a; python # 导入QQ消息包 import tqq # 创建QQ客户端对象 client tqq.TQQClient() # 连接QQ服务器 client.connect("你的QQ号码", "你的QQ密码") # 创建一个QQ会话对象 session client.session() …

c++高级篇(一) —— 初识Linux下的进程控制

linux的信号 信号的概念 在Linux中&#xff0c;信号是一种用于进程间通信和处理异步事件的机制&#xff0c;用于进程之间相互传递消息和通知进程发生了事件&#xff0c;但是&#xff0c;它不能给进程传递任何数据。 信号产生的原因有很多种&#xff0c;在shell中&#xff0c…

每日两题 / 437. 路径总和 III 105. 从前序与中序遍历序列构造二叉树(LeetCode热题100)

437. 路径总和 III - 力扣&#xff08;LeetCode&#xff09; 前序遍历时&#xff0c;维护当前路径&#xff08;根节点开始&#xff09;的路径和&#xff0c;同时记录路径上每个节点的路径和 假设当前路径和为cur&#xff0c;那么ans 路径和(cur - target)的出现次数 /*** D…

fastjson_1.2.24和Shiro(CVE-2016-4437)漏洞复现

文章目录 一、fastjson 1.2.24远程命令执行漏洞复现二、shiro反序列化漏洞(CVE-2016-4437)1、Shiro漏洞原理2、手工验证漏洞3、使用ShiroAttack2 一、fastjson 1.2.24远程命令执行漏洞复现 配置环境&#xff1a;本机java 8环境 kali操作系统&#xff08;java8&#xff09; c…

webapi路由寻址机制

路由匹配的原则 1、启动 Application_Start 文件夹中有个WebApiConfig 会把路由规则写入一个容器 2、客户端请求时&#xff1a; 请求会去容器匹配&#xff0c;先找到控制器&#xff08;找到满足的&#xff0c;就转下一步了&#xff09;&#xff0c;然后找Action&#xff0c;we…

被动防护不如主动出击

自网络的诞生以来&#xff0c;攻击威胁事件不断涌现&#xff0c;网络攻防对抗已然成为信息时代背景下的一场无硝烟的战争。然而&#xff0c;传统的网络防御技术&#xff0c;如防火墙和入侵检测技术&#xff0c;往往局限于一种被动的敌暗我明的防御模式&#xff0c;面对攻击者无…

第四届辽宁省大学生程序设计竞赛

比赛经历&#xff1a;2024.5.14简单vp了一个小时只写出了签到题4个然后跑路了 补题&#xff1a;感觉其他题有点太抽象了主要补了一题&#xff0c;在区间问题中数据结构的使用 比赛链接[点我即可] 目录 A.欢迎来到辽宁省赛 B.胜率 F.隔板与水槽 H.取石子 L.区间与绝对值 …

NMACDR:基于邻居交互增强和多头注意力机制的跨域推荐模型

基于邻居交互增强和多头注意力机制的跨域推荐模型 湖北民族大学学报-孙克雷、汪盈盈-2023 思路 针对基于映射的跨域推荐模型没有充分关注源域中数据稀疏的用户,导致用户偏好的迁移效率降低的问题,提出本文。 首先,利用邻居用户的交互来增强源域中数据稀疏用户的交互序列,…

RS422一主多从MAX3490

RS422一主多从MAX3490 最近项目用到了RS422一主多从&#xff0c;一个主机4个从机。芯片用的MAX3490&#xff0c;几经折腾&#xff0c;最终只能从一拖4改为一拖2。 主机发送端&#xff0c;从机4个接收端都是正常的&#xff0c;没有问题。波形非常完美&#xff0c;没有太大变形 …

uni-segmented-control插件使用

dcloud插件市场 前端/uniapp 1.HBuildX打开目标项目 2.进入dcloud插件市场下载目标插件 3.看到如下提示(已经可以在目标项目中使用插件啦) 4.项目正式使用

自动驾驶占据感知的综述:信息融合视角

24年5月香港理工的论文“A Survey on Occupancy Perception for Autonomous Driving: The Information Fusion Perspective“。 3D 占据感知技术旨在观察和理解自动驾驶车辆的密集 3D 环境。该技术凭借其全面的感知能力&#xff0c;正在成为自动驾驶感知系统的发展趋势&#x…

JSPfilters过滤技术

1.创建动态web项目 2.创建filters的文件 3.创建主页面 4.配置xml项目 总结构 主页面代码 <% page language"java" contentType"text/html; charsetUTF-8"pageEncoding"UTF-8"%><!DOCTYPE html><html><head><meta cha…

idea2023.3.2版本全局设置maven地址

idea每次新建项目都默认使用了一个user目录下的地址&#xff0c;而不是自己安装的maven地址&#xff0c;每次创建项目后&#xff0c;都要重新从settings中设置一下maven地址。 可以全局修改&#xff1a;首先在File-->Close Project回到idea最开始的界面 然后在Customize里点…

C++语法|深入理解 new 、delete

在开发过程中&#xff0c;非常重要的语法就有我们new和delete&#xff0c;周所周知在C中最为强大的能力就是对内存的控制&#xff0c;所以我们再怎么强调new和delete都不为过 文章目录 1.new和delete基本语法new和malloc的区别是什么&#xff1f;(1)开辟单个元素的内存差别(2)开…

火遍全网的“当当狸智能激光雕刻机L1” 让创意梦想分分钟实现

当当狸首款“桌面级”智能激光雕刻机来袭&#xff0c;千万别错过。 龙年伊始&#xff0c;当当狸就迎来了新品首发——智能激光雕刻机L1。 话不多说&#xff0c;赶快来看~~ 当当狸这款智能激光雕刻机造型美观&#xff0c;设计时尚&#xff0c;堪称激光雕刻机界的颜值天花板~~ …