标签:大致 div 必须 程序 步骤 stopped err win 拖动
现在都9102年了谈这个runloop实在是多余,但是最近发现自己以前理解的知识点有的都忘了,如题。所以写这个文章记录一下,以后记不清了就返回来看看。如有不当之处,还请大家指正。本文是基于苹果官方文档的解读,大多数内容在文档中都可以找到,其相关文档为:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html#//apple_ref/doc/uid/10000057i-CH16-SW1
与Runloop相关的CFRunLoopRef源码也是公开的,地址为:https://opensource.apple.com/source/CF/CF-635.19/CFRunLoop.c.auto.html 喜欢研究源码的同学可以看下。
关于runloop的概念,我觉得苹果文档已经阐述的相当到位了。原文翻译过来即为:runloop是与线程相关的基础架构的一部分。 runloop是一个事件处理循环,用于调度工作并协调传入事件的接收。 runloop的目的是在有工作时保持线程忙,并在没有事件处理时让线程进入休眠状态。
在iOS系统中,关于runloop的代码有2处,一处是CoreFoundation 框架内的CFRunLoopRef,提供了纯C函数的API且是线程安全的。另一处是Foundation框架内的NSRunLoop,其核心是对CFRunLoopRef在OC中的封装并无提供额外的其他功能,提供面向对象的API,线程不安全。
根据苹果文档的描述,每次运行runloop时,runloop都会处理执行若干个task,并为任何外部附加的observers生成通知,即告知runloop当前处于什么状态。
runloop执行此操作的顺序为:
1.通知观察者已经进入了runloop
2.通知观察者任何准备好的Timer即将被触发
3.通知观察者任何非基于端口的输入源即将被触发(即Source0)
4.触发任何准备触发的基于非端口的输入源,即处理Source0回调
5.如果基于端口的输入源准备就绪并等待触发(即Source1),请立即处理该事件。 转到第9步
6.通知观察者thread即将睡眠
7.将线程置于睡眠状态,直到发生以下事件之一:
·事件到达基于端口的输入源,即Source1触发
·Timer触发
·为runloop设置的timeout到期
·runloop被明确唤醒
8.通知观察者thread刚刚醒来
9.处理待处理事件。
·如果触发了用户定义的Timer,则处理Timer事件并重新启动runloop。 转到第2步
·如果输入源被触发,则传递该事件
·如果runloop被明确唤醒但尚未超时,请重新启动循环。 转到第2步
10.通知观察者runloop已退出
其事件循环序列对应的runloop源码即为:
首先调用
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry); result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);//1
进入runloop的主函数__CFRunLoopRun
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);//2 if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);//3 __CFRunLoopDoBlocks(rl, rlm);//执行被加入的block
在这里“执行被加入的block”应该怎么理解呢?runloop在运行时会处理若干个task,而其中有一种方式是可以被开发者使用的,方法为:
/**即通过CFRunLoopPerformBlock函数将一个block插入目标队列*/ void CFRunLoopPerformBlock(CFRunLoopRef rl, CFTypeRef mode, void (^block)(void));
调用上面的函数方法,runloop在运行时,会通过__CFRunLoopDoBlocks(rl, rlm)执行队列里的所有block. 方法中有个CFTypeRef mode参数,即调用时定义的为哪种mode,那么在执行时也只执行与此mode相关的block. 关于mode的解释,后面会详细说明。
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle); if (sourceHandledThisLoop) { __CFRunLoopDoBlocks(rl, rlm); } //4 Boolean poll = sourceHandledThisLoop || (0LL == timeout_context->termTSR); if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) { msg = (mach_msg_header_t *)msg_buffer; if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), 0)) { goto handle_msg; //5 } } if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting); __CFRunLoopSetSleeping(rl); //6
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), poll ? 0 : TIMEOUT_INFINITY);//7 __CFRunLoopUnsetSleeping(rl);
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);//8
之后进入步骤9,即如果有计时器(Timer)时间到了,将触发这个Timer的回调,即调用__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())。或者如果开发者调用GCD的API将任务放入主队列中,那么runloop会调用_dispatch_main_queue_callback_4CF(msg)来执行这个block。再或者runloop通过__CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort)判断Source1是否被触发,如果触发则调用__CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply)处理此事件。
在循环的最后,runloop会判断当前mode返回的原因,即CFRunLoopRunResult. 这是个枚举类型,分别为:
/* Reasons for CFRunLoopRunInMode() to Return */ typedef CF_ENUM(SInt32, CFRunLoopRunResult) { kCFRunLoopRunFinished = 1, //runloop没有要处理的source和timer kCFRunLoopRunStopped = 2, //CFRunLoopStop在runloop中被调用 kCFRunLoopRunTimedOut = 3, //超出传入的标记时间 kCFRunLoopRunHandledSource = 4 //runloop被告知处理完此source就返回 };
当runloop循环标识位不为0时,循环停止。
最后调用
__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit); //10
从CFRunLoopRef的源码中去看,runloop主体就是一个do while循环,这点和runloop概念中提到的“事件处理循环”相吻合。依靠mach_msg系统调用,发送和接收Mach消息。mach_msg可以理解为系统级别的多进程之间的通信机制,使用相同的缓冲区来发送和接收消息(关于mach_msg的相关概念,感兴趣的同学可以去看文档)。依靠苹果内核Kernel对其进行管理。runloop运行时会向外部报告runloop当前状态的更改(即CFRunLoopActivity状态),然后系统通过mach_msg内核函数对runloop进行唤醒或休眠操作,避免runloop在无事件处理时占用系统资源。
runloop的数据结构分为:CFRunLoopRef,CFRunLoopModeRef,CFRunLoopSourceRef,CFRunLoopTimerRef,CFRunLoopObserverRef
CFRunloopRef其实就是__CFRunloop这个结构体指针,其核心方法即是我们上面讲的__CFRunLoopRun函数。对应的结构体源码为:
struct __CFRunLoop { CFRuntimeBase _base; pthread_mutex_t _lock; /* locked for accessing mode list */ __CFPort _wakeUpPort; // used for CFRunLoopWakeUp Boolean _ignoreWakeUps; volatile uint32_t *_stopped; pthread_t _pthread; //代表线程,且runloop与线程是一一对应的 uint32_t _winthread; CFMutableSetRef _commonModes; CFMutableSetRef _commonModeItems; CFRunLoopModeRef _currentMode; //每次调用Runloop的主函数__CFRunLoopRun()时必须指定一种Mode,这个Mode称为 _currentMode CFMutableSetRef _modes; //包含多个mode的数据集合,其中mode的数据结构为CFRunLoopMode(NSMutableSet<CFRunLoopMode*>) struct _block_item *_blocks_head; struct _block_item *_blocks_tail; CFTypeRef _counterpart; };
_commonModes和_commonModeItems在接下来讲述mode的数据结构时再详细说明。
CFRunLoopModeRef对应的源码为:
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; mach_port_t _timerPort; };
从mode的源码中可以看到,mode就是为了实现runloop的逻辑行为而设计的。依据文档,mode是要监视的输入源(即Source0和Source1)和计时器的集合,以及要通知的RunLoopObserver的集合。并且runloop在运行时,在同一时段只能且必须(显式或隐式)在一种特定的mode下运行。在指定mode以后,仅监视与该mode关联的Source源并允许其传递其事件。那这个指定的mode就是在__CFRunLoop结构体中的_currentMode.
在Cocoa和Core Foundation框架中定义了标准mode以及何时使用该mode的说明。其分别为:
这样来看__CFRunLoop结构体中的_commonModes和_commonModeItems就好理解了,一个 Mode 可以将自己标记为”common”属性(通过将其 ModeName 添加到runloop的 “commonModes” 中)。每当runloop的内容发生变化时,runloop都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “common” 标记的所有Mode里。
那么在runloop中runloop mode是怎么切换的呢?在源码中可以看到,为了保证线程安全,每回在mode切换时,必会有:
__CFRunLoopLock(rl);
..............
__CFRunLoopModeLock(rlm);
..............
__CFRunLoopModeUnLock(rlm);
..............
__CFRunLoopUnlock(rl);
所以大致可以认为mode切换有二种方式:一种是在runloop中途切换,一种是按顺序在当前mode结束之后切换。
CFRunLoopSourceRef是事件产生的地方,即事件源。Source分为两种:Source0和Source1
CFRunLoopTimerRef基于事件的触发器,和NSTimer是toll-free bridge的(免费桥转换)。包含一个时间长度和一个回调(函数指针)。我们可以设置Timer的触发方式once或者repeat. 一个repeating timer重复触发依赖的时间是基于上一次的fire time的,并非实际的时间(有可能中间 runloopmode 有改变)。runloop为了节省资源,并不会在非常准确的时间点回调这个Timer。Timer有个属性叫做Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。
CFRunLoopObserverRef对应的源码为:
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 */ };
CFRunLoopObserver为runloop中的一个监听器,随时通知外部当前RunLoop的运行状态。其状态对应的枚举类型为:
/* Run Loop Observer Activities */ typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), //进入runloop kCFRunLoopBeforeTimers = (1UL << 1), //即将处理Timer事件 kCFRunLoopBeforeSources = (1UL << 2),//即将处理Sources事件 kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠,将要进行用户态到内核态的切换 kCFRunLoopAfterWaiting = (1UL << 6), //即将进入唤醒,内核态到用户态的切换后不久 kCFRunLoopExit = (1UL << 7), //退出runloop kCFRunLoopAllActivities = 0x0FFFFFFFU };
AutoreleasePool的创建和释放就是监听上述状态的改变来实现的。
未完待续.......
标签:大致 div 必须 程序 步骤 stopped err win 拖动
原文地址:https://www.cnblogs.com/code4appstyle/p/10497903.html