RunLoop

****简介****
RunLoop在OS X/ iOS中一项比较基础的知识点,虽然基础,但是十分重要。它与线程息息相关,是用于处理到来事件的循环处理机制,可以理解为消息队列,当有事件需要处理的时候,RunLoop会使得线程在持续运行状态,没有任务的时候会让线程进入休眠状态。每一条线程都有一条与之对应的RunLoop(但是不仅限于一条,可以在RunLoop内嵌套另一个RunLoop)。

本文尝试讲解RunLoop的一些原理方面的知识,同时会介绍几个RunLoop的具体应用案例,基于RunLoop在cocoa层的API(NSRunLoop)网上有许多参考资料,翻阅官方reference也能中得到,在这里就不花篇幅介绍了。

****RunLoop结构****
RunLoop的实现可以用伪代码表示为:

int retVal = 1
fun __CFRunLoopRun(CFRonLoopRef rl) {
      var msg
      do {
             msg = get_next_msg
            if (msg) {
                rl.wakeUp()
                handle(msg)
            }else {
                rl.sleep()
            }
            retVaule =  msg==quit || msg==timeout 0 : 1
      }while (retVal == 1)
}

这个模型被称为Event Loop,也可以理解成消息队列。RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

在OS X和iOS中,苹果提供了两种类型的API:

  • cocoa层 : NSRunLoop

  • CoreFoundation : CFRunLoopRef

显而易见,NSRunLoop是对CFRunLoopRef在OC上面向对象的封装,但是NSRunLoop不是线程安全的,而CFRunLoopRef是线程安全的。
整个CoreFoundation是开源的,可以在这里下载源码。

这个CFRunLoop有五大类:

  • CFRunLoopRef :

一个runLoop对象

  • CFRunLoopMod

一个RunLoop包含若干个Mode,Mode中又包含若干个Source/Timer/Observer,如果切换Mode,只能退出当前的RunLoop,主要是为了分隔开不同组的Source/Timer/Observer。

  • CFRunLoopObserverRef

RunLoop状态的观察者,每一个观察者都包含一个回调(指针函数),当RunLoop的状态发生变化时,观察者就能通过回调接收这个变化。
可以观察到的RunLoop的状态时间点有:

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),         //即将退出
};
  • CFRunLoopSourceRef
    事件的产生的地方。

有两个版本的Source:

  • *Source0 *:只包含一个回调函数指针,使用时需要将事件标记为待处理:CFRunLoopSourceSignal(source)
    ,再调用CFRunLoopWakeUP(runloop)
    来唤醒RunLoop,使其处理整个事件

  • *Source1 *:包含一个用于内核和其他线程发送消息的方法:mach_port,这种source能主动唤醒RunLoop,具体原因看底下会讲到的mach_msg()

  • CFRunLoopTimerRef
    基于时间的触发器,与NSTimer可以混用,包含一个回调,当其加入RunLoop时,RunLoop会注册时间点,等到时间点到了RunLoop会被唤醒处理回调时间。

Source/Timer/Observer被统称为mode item,一个item可以加入不同的RunLoop,但是重复添加到同一个Runloop是不会起作用的;如果一个RunLoop不包含Source/Timer(只包含Observer也不行),这个RunLoop是不会进入循环的。

RunLoop代码
CFRunLoopMode和CFRunLoop的关系大致为:

struct __CFRunLoopMode {
    ...
    CFStringRef _name;//mode的名字:kCFRunLoopDefaultMode
    CFMutableSetRef _source0;
    CFMutableSetRef _source1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    ...
}

struct __CFRunLoop {
    ...
    CFMutableSets _commonModes;  //存放具有"common"标示的mode
    CFMutableSets _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes
    ...
}

__commonModesItems里面存放observer/timer/source,当RunLoop的状态发生变化时,RunLoop会将这个集合里面的所有mode item同步到具有"common"标示的mode中。
应用场景:我们知道,一个timer在NSDefaultMode
下被触发,如果这个时候拖动scrollview的话,这个timer就失效了,因为拖动scrollview,RunLoop的mode切换为UITrackingRunLoopMode
。如果想要让一个定时器在两个模式下都有效有两种方法:1、将它加入到两个mode中;2、将timer加入到顶层的RunLoop的commonModeItems集合中,RunLoop会自动将这个集合中的所有item同步到具有"common"标示的mode

****RunLoop与线程****
线程与RunLoop是一一对应关系,其关系保存在一个全局字典中,但是不是说一条线程中只能有一个RunLoop,你可以通过在RunLoop中嵌套另一个RunLoop达到一个线程中多个RunLoop的目的。

苹果不允许直接创建RunLoop,只能通过提供的两个方法获取当前线程的RunLoop和主线程的RunLoop:CFRunLoopGetCurrent() 和CFRunLoopGetMain() 。当你获取RunLoop的时候,如果没有这个RunLoop,那么系统会创建一个RunLoop返回给你,线程刚创建时是没有RunLoop的,RunLoop的创建发生在第一次获取的时候。

获取线程RunLoop的代码大致如下:

CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) 
            CFRunLoopRef newLoop = __CFRunLoopCreate(t);
            loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    if (!loop) 
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
    return loop;
}

****RunLoop流程:****

//1、通知observer即将进入RunLoop:
__CFRunLoopDoObserver(rl, currentMode, KCFRunLoopEntry);

__CFRunLoopRun(CFRunLoopRel rl, sourchHandleThisLoop) {
    int retval = 1;
    do {

        //2、通知observer:即将处理Timer
        __CFRunLoopDoObserver(rl, currentMode, kCFRunLoopBeforeTimers);

        //3、通知observer:即将处理Source0
        __CFRunLoopDoSource(rl, currentMode, kCFRunLoopBeforeSources);

        //4、RunLoop触发source0
        __CFRunLoopDoObserver(rl, currentMode, stopAfterHandler);

        //5、如果有source1且是处理ready状态,跳转到 9 ,直接处理source1然后跳转去处理消息
        if (__Source0DidDispatchPortLastTime) {
            Boolean hasMag = __CFRunLoopServiceMachPort(dispatchPort, &msg);
            if (hasMas) goto handleMsg
        }

        //6、如果没有待处理的消息,通知observer,即将进入休眠
        if (!__soure0DidDispatchPortLastTime) {
            __CFRunLoopDoObserver(rl, currentMode, kCFRunLoopBeforeWaiting);
        }

        //7、RunLoop进入休眠,除非有以下情况才会从休眠中被唤醒:
        1)timer设定的时间点到了
        2)基于port的source事件
        3)分发到主队列RunLoop的任务

        //8、通知observer线程刚刚被唤醒
        __CFRunLoopDoObserver(rl, currentMode, kCFRunLoopAfterWaiting);

        //9、RunLoop被唤醒,处理消息:
        if (msg_is_timer) {
            CFRunLoopDoTimer(rl, currentMode, mach_absoluteTime());
        }else if (msg_is_dispatch) {
            __CFRUNLOOP_IS_SERVICING_MAIN_DISAPTCH_QUEUE(msg)
        }else {
            CFRunLoopSourceRef   source1 = __CFRunLoopFindSourceForMachPort(rl, currentMode, livePort);
            sourceDoHandleThisPort = __CFRunLoopDoSource1(rl, currentMode, source1);
            if (surceDoHnadleThisPort) {
                mach_msg(reply, MACH_SEND_MSG, reply);
        }   

        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;
            } 
        } while (retVal == 1)
}

//10、通知observer,即将推出RunLoop
__CFRunLoopDoObserver(rl, currentMode, kCFRunLoopExit);

****RunLoop底层实现****
RunLoop的核心是基于mach_port,进入休眠期间调用的是mach_msg()方法,
关于mach:

mach是OS X/iOS 系统架构XNU内核,作为它作为一个微内核,仅提供处理器的调度,IPC等非常少量的服务。
在mach中的对象不能直接调用,只能通过消息传递的方式实现对象间的通信。
有关mach的通信如何使用参照[这里](https://segmentfault.com/a/1190000002400329)。
消息在mach中是最基础的概念,在两个端口直接传递,行程了mach 的IPC核心。
从mach 的源码信息中可以看出mach其实就是一个二进制包数据,其头部包含了当前端口local_port
和目标端口remote_port
。
苹果提供了在cocoa层的对mach端口操作API-NSMachport。

在APP静止的时候点击暂停可以从调用栈信息里面看到:

1638754-89db6f6d46061ee7.png.jpeg

系统在用户态时会调用mach_msg_trap()会触发陷阱机制,切换到内核态,在内核态下实际上是调用了mach_msg()来实现完成实际工作。
大体过程是这样的:

1638754-1eec76243bcab529.png.jpeg

****RunLoop的五个Mode****
RunLoop包含五个Mode,分别为

kCFRunLoopDefaultMode 默认的mode,主线程在此mode下实现
UITrackingRunLoopMode 追踪scrollview的触摸滑动
UIInitializationRunLoopMode 刚启动APP时的第一个mode,只在刚启动时有效,之后后切换为default mode
GSEventReceiveRunLoopMode 系统事件的内部mode
kCFRunLoopCommonMode 占位mode,无实际作用

****RunLoop的调用函数****
RunLoop在执行中,是通过一长串的函数进行回调的,例如:
即将出发timer的回调:
CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION(kCFRunLoopBeforeTimers),
即将出发source回调:
CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION(source0);
......
这些函数都能在调用栈信息中找到。

在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.1 如果是被Timer唤醒的,回调Timer
        __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

        /// 9.2 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

        /// 9.3 如果如果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);
}

note:autoreleasepool的创建和释放是在RunLoop的休眠和下一次启动之间进的。
****RunLoop在cocoa中的应用****
事件传递与手势识别
对于硬件事件(触摸、锁屏、摇晃)的处理,苹果注册了一个基于port的source1,它的回调函数是__IOHIDEventSystemClientQueueCallback()
,事件发生后,系统将事件包装成IOHIDEvent
对象,并由mach port分配到对应的APP进程中,随后触发source1的回调,并调用_UIApplicationHandleEventQueueCallback()
进行内部分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等,接下来发生的 事件传递响应链 了。
对于手势识别:当_UIApplicationHandleQueueCallback()
接收到手势的时候,会将TouchBegin等事件的回调打断,随后会将这个手势标记为待处理状态,同时注册一个observer,检测BeforeWaiting状态,当RunLoop即将进入休眠时,其内部会获取到刚才所有标记为待处理的手势,执行_UIGestureRecognizerUpdateQueue()

****Autorealease****
iOS中autorelease变量什么时候释放,应该分为两种情况:

  • 手动释放@autoreleasepool { }中的自动释放变量在当前大括号作用域结束时释放;
  • 系统释放:在当前RunLoop本次Loop结束后释放;

autorelease原理:

主线程注册了两个observer1,observer2,这两个observer的回调函数都是__wrapRunLoopWithAutoReleasePoolHandler()。

observer1监测Entry状态,当进入RunLoop时,调用_obj_autoreleasepool_push()方法创建一个新的autoreleasepool,这个observer的优先级最高,确保autoreleasepool的创建在所有的回调之前;

observer2监测BeforeWaiting状态,当RunLoop即将进入休眠时,回调中先调用_objc_autoreleasepool_pop()方法将autoreleasepool里面的自动释放类型的变量释放,然后再调用_objc_autoreleasepool_push()方法创建一个新的autoreleasepool。同时observer2还会检测Exit状态,当退出RunLoop时调用_objc_autoreleasepool_pop()。这个observer的优先级最低,确保autoreleasepool的释放在所有的回调之后。

Autorelease的深层原理请参考:黑幕背后的Autorelease

****页面刷新****
当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行:
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。在这个函数之后就在屏幕上看到UI的变化

****Timer****
可以说没有RunLoop就不可能实现定时器的功能。定时器的大致原理:设定一个时间点,将定时器加入RunLoop中,等到达设定的时间点的时候回唤醒线程处理回调。

****PerfromSeletor:afterDelay:****
如果当前线程中没有RunLoop这个方法是不会有效的,本质上是在当前线程的RunLoop中添加一个定时器,当时间点到了会唤醒RunLoop执行回调。

****dispatch_main_queue****
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

有一种说法,说RunLoop的timer和GCD中的timer是一个东西,其实不是的,但是GCD的timer和RunLoop是怎么协调工作的,具体还不太清楚。

****RunLoop的实际使用案例****
AFNetWorking
在AFN中的:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

在上面的代码中,AFN创建了一个RunLoop,并且加入了一个mach port,但是这个port什么事情也没做,主要作用是为了让RunLoop处于常驻状态,否则这个RunLoop马上就会退出了,所以,这也是创建一个常驻服务线程的方法。

****滚动视图中延迟加载图片****
在tableview或者collection view中,如果要在停止拖拽的时候再加载图片,最直接的想法是,通过tableview和colletionview的isDragging等属性来进行判断,但是使用RunLoop来实现这一功能就不需要那么麻烦:

[imgView performSelector:@selector(loadImage:)
              withObject:image
                 inModeS:@[NSDefaultRunLoopMode]];
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,980评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,178评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,868评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,498评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,492评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,521评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,910评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,569评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,793评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,559评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,639评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,342评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,931评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,904评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,144评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,833评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,350评论 2 342

推荐阅读更多精彩内容