RunLoop从理解到忘记

 

现在都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是一个事件处理循环,用于调度工作并协调传入事件的接收。 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的相关概念,感兴趣的同学可以去看文档)。依靠苹果内核Kernel对其进行管理。runloop运行时会向外部报告runloop当前状态的更改(即CFRunLoopActivity状态),然后系统通过mach_msg内核函数对runloop进行唤醒或休眠操作,避免runloop在无事件处理时占用系统资源。

 Runloop相关数据结构

runloop的数据结构分为:

CFRunLoopRef:runloop对象

CFRunLoopModeRef:runloop的运行模式

CFRunLoopSourceRef:runloop模型图中提到的Source0(需要手动唤醒线程)和Source1(具备唤醒线程的能力)

CFRunLoopTimerRef:runloop模型图中提到的定时源

CFRunLoopObserverRef:通过注册Observer来监听runloop运行状态的改变

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

  • 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与线程的关系:

runloop与线程是一一对应的。且不允许手动创建,只能通过方法获取(CFRunLoopGetMain() 和 CFRunLoopGetCurrent())。主线程的runloop的是在程序启动时,默认开启的。如果自定义runloop需要自己创建,且在线程结束后销毁。runloop与线程的对应关系保存在一个全局字典中。 

 Runloop的应用

AutoreleasePool

在app启动后,在控制台打印 po [NSRunLoop currentRunLoop],可以看到如下信息:

<CFRunLoopObserver 0x6000018d0780 [0x10792db68]>{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10c2c81b1), context = <CFArray 0x6000027986f0 [0x10792db68]>{type = mutable-small, count = 1, values = (<0x7fb96e800058>)}}

<CFRunLoopObserver 0x6000018d0640 [0x10792db68]>{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10c2c81b1), context = <CFArray 0x6000027986f0 [0x10792db68]>{type = mutable-small, count = 1, values = (<0x7fb96e800058>)}}

 可以看到在主线程的runloop中,有两个注册的Observer其回调都是 _wrapRunLoopWithAutoreleasePoolHandler(),从名称上不难看出这两个是和AutoreleasePool相关的两个监听。

第一个Observer监听的是RunLoopEntry,即runloop的进入。在其回调内会调用objc_autoreleasePoolPush()向当前的AutoreleasePoolPage增加一个哨兵对象(值为0,即nil)标志创建自动释放池,order是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。(AutoreleasePoolPage双向链表数据结构,此处为源码AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组成。AutoreleasePoolPage每个对象会开辟4096字节内存,也就是虚拟内存一页的大小,除了内部实例变量所占的空间,剩下的空间全部用来储存autorelease对象的地址)

第二个Observer监听的是进入休眠(BeforeWaiting)和即将退出RunLoop(Exit)两种状态。准备进入休眠时会调用objc_autoreleasePoolPop() 和 objc_autoreleasePoolPush() 根据情况从最新加入的对象一直往前清理直到遇到哨兵对象。而在即将退出RunLoop时会调用objc_autoreleasePoolPop() 释放自动自动释放池内对象。这个Observer的order是2147483647,优先级最低,确保发生在所有回调操作之后。

简单的说UIKit通过RunLoopObserver在runloop两次sleep间对AutoreleasePool进行了Pop和Push,并将这次runloop中产生的Autorelease对象释放。

注:AutoreleasePoolPage释放对象的过程为根据传入的哨兵对象地址找到哨兵对象所在的AutoreleasePoolPage,向在哨兵对象之后加入的所有对象发送release消息,然后移动next指针到下一个位置。其次对于多层AutoreleasePool嵌套就是对哨兵对象每次pop的时候释放上次push哨兵对象的位置。

触摸事件响应

当触摸事件发生时,通过IOKit.framework将相应的输入封装成IOHIDEvent对象,然后UIKit通过IOHIDEvent的类型,判断出相应事件应该由SpringBoard.app处理,直接通过mach_port转发给SpringBoard.app。SpringBoard.app主线程Runloop收到IOKit.framework转发来的消息苏醒,并触发对应Mach_Port的Source1回调__IOHIDEventSystemClientQueueCallback()。Source1回调内部触发Source0回调__UIApplicationHandleEventQueue(),Soucre0回调内部,封装IOHIDEvent为UIEvent,Soucre0回调内部调用UIApplication的sendEvent:方法,将UIEvent传给UIWindow。

定时器

Timer即是NSTimer,其实就是 CFRunLoopTimerRef,他们之间是免费桥接的(toll-free bridged)。NSTimer定时器的触发正是基于runloop运行的,所以使用NSTimer之前必须注册到runloop,但是runloop为了节省资源并不会在非常准确的时间点调用定时器。Timer有个属性叫做Tolerance(宽容度),标示了当时间点到后,容许有多少最大误差。如果一个任务执行时间较长,那么当错过一个时间点后只能等到下一个时间点执行。

实现定时器大约有PerformSelecter、NSTimer、CADisplayLink、GCD这几种。前三种有其局限性且精度也不高,管理不善还会造成内存泄漏。所以项目中如果要实现定时器功能,强烈建议用GCD实现。

注意:因为NSTimer会对Target进行强引用直到任务结束或exit之后才会释放。在控制器中引用NSTimer对象并调用其方法后,无论是重复执行的定时器还是一次性的定时器,都要在控制器对象释放之前调用invalidate方法(只是一次性的定时器执行完操作后会自动调用invalidate方法)。这样就可以避免控制器无法释放的情况发生了。

posted @ 2019-03-21 00:04  Alex.xue  阅读(184)  评论(0编辑  收藏  举报