java runloop_RunLoop从理解到忘记(示例代码)

Runloop概述

关于runloop的概念,我觉得苹果文档已经阐述的相当到位了。原文翻译过来即为:runloop是与线程相关的基础架构的一部分。 runloop是一个事件处理循环,用于调度工作并协调传入事件的接收。 runloop的目的是在有工作时保持线程忙,并在没有事件处理时让线程进入休眠状态。

在iOS系统中,关于runloop的代码有2处,一处是CoreFoundation 框架内的CFRunLoopRef,提供了纯C函数的API且是线程安全的。另一处是Foundation框架内的NSRunLoop,其核心是对CFRunLoopRef在OC中的封装并无提供额外的其他功能,提供面向对象的API,线程不安全。

Runloop事件循环序列

根据苹果文档的描述,每次运行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的相关概念,感兴趣的同学可以去看CFRunLoopActivity状态),然后系统通过mach_msg内核函数对runloop进行唤醒或休眠操作,避免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)

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的说明。其分别为:

NSDefaultRunLoopMode(Cocoa),kCFRunLoopDefaultMode(Core Foundation): iOS系统默认的RunloopMode。大多数情况下,应使用此mode启动runloop并配置输入源。

NSConnectionReplyMode(Cocoa):Cocoa将此mode与NSConnection对象结合使用以监视回复。很少用到

NSModalPanelRunLoopMode(Cocoa):Cocoa使用此mode来识别用于modal panels的事件。(移动设备开发用不到)

UITrackingRunLoopMode(iOS),NSEventTrackingRunLoopMode(Cocoa):此mode限制鼠标拖动循环和其他种类的用户界面跟踪循环期间的传入事件。

NSRunLoopCommonModes(Cocoa),kCFRunLoopCommomModes(Core Foundation):这是一组可配置的常用模式,其为一组runloop mode的集合。对于Cocoa应用程序,此集合默认包括默认(NSDefaultRunLoopMode),模态(NSTaskDeathCheckMode)和事件(UITrackingRunLoopMode)跟踪模式。 Core Foundation最初只包含默认模式。 您可以使用CFRunLoopAddCommonMode函数将自定义模式添加到集合中。

这样来看__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

Source0:非基于内核Port的,用于用户主动触发的事件。只包含了一个回调(函数指针),需要手动唤醒线程让其处理事件。

Source1:基于内核Port的,包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。具备唤醒线程的能力。Source1在处理的时候会分发一些操作给Source0去处理,例如button点击事件。

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的创建和释放就是监听上述状态的改变来实现的。

Runloop的应用

未完待续.......

!--

p.p1>!--

p.p1>!--

p.p1>!--

p.p1>!--

p.p1>!--

p.p1>!--

p.p1>

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值