美文网首页专注iOS开发runloopiOS Runloop
[iOS]runloop - iOS界的EventLoop

[iOS]runloop - iOS界的EventLoop

作者: pingpong_龘 | 来源:发表于2016-03-30 11:22 被阅读891次

    1.闲扯

    一般除了初学者,大部分人了解runloop可能更多的是在面试或者准备面试的时候。 显然这种技术在平时的开发中,使用的场景是非常低的,但是对这个知识的了解程度可以作为衡量一个iOSer的‘闲散’程度(一般太忙,需求太多的人基本没时间研究这个东西😄)

    其实我也不太懂,只是把自己各方偷师学来的整理一下,整理中也希望自己能有个更深入的理解。嗯开始吧:

    2.优秀文档/资料

    列举下已有的比较好的资料
    1> 深入理解RunLoop (by ibireme)

    屏幕快照 2016-03-30 下午5.35.17.png

    polen:
    从cocoachina看到的,基本算是非常非常全的了,很详细的介绍了你所能了解的全部runloop(为什么我这么确定,因为其实关于runloop的文档并不多,这篇是我看过里面个人认为最全面也最细节的一篇,五星推荐!!!)

    2>看过一个视频:
    某度的@sunnyxx的分享

    屏幕快照 2016-03-30 下午5.34.12.png

    3>以及有个可爱的童鞋对这个视频做了简单的整理:
    iOS runloop (作者:小白和小黑)

    polen:
    如果有心的朋友看一下这个视频 ,大概96min,孙源这哥们讲的非常透彻,之前是百度的,在我写文章这几天好像刚从百度离职,下面内容有些截图就是他视频里面的,希望他不会怪我偷他的图片😄

    3.我来整理

    当然也有比较懒的,那就看我的总结吧
    备注:以下大部分信息非本人原创,只是作为只是整理使用,
    原文链接上面已经提及,大家可以直接看

    3.1runloop的定义

    polen:
    首先说明下背景:
    runloop不是线程,不是GCD,在一个APP里面不是唯一的

    runloop就是一个对象,如果把线程比作一条高速公路,我的理解runoop就是这条道路的管理员,没事了就睡觉,有事了把他叫醒(注意,这里叫醒的实现,一般是其他线程(大部分是main线程)的把他叫醒,可以留一下这里,后面会伪代码说到这个问题)。

    形象理解的话,就是下图里面,如果线程是个箭头线,runloop就是那个圈,一圈又一圈...

    屏幕快照 2016-03-30 下午5.36.26.png

    有人会觉得runloop好虚,如何直观的看到runloop,这个很简单,你打开Xcode,run一段cheng程序,然后打个断点或者暂停一下,看一下堆栈信息,马上就可以看到,我们的进程从main函数开始,紧接着马上回唤醒runloop,然后是再调其其他的函数, 应该说除了main函数和几个基本的函数,大部分都是runloop调用起来的,截图如下:

    runloop.png

    所以和runloop有关的都有哪些东西?

    屏幕快照 2016-03-30 下午5.42.19.png

    当然,专业的说,本质是eventlop,这个不只是在iOS,任何系统或者语言里面都有类似的东西

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

    Crayon Syntax Highlighter v2.7.1
    function loop() {
        initialize();
        do {
            var message = get_next_message();
            process_message(message);
        } while (message != quit);
    }
    
    function loop() {
        initialize();
        do {
            var message = get_next_message();
            process_message(message);
        } while (message != quit);
    }
    

    这种模型通常被称作 Event Loop。 Event Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/iOS 里的 RunLoop。实现这种模型的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。
    所以,RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 "接受消息->等待->处理" 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

    OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。
    CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
    NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

    polen:
    这个我有篇内存管理的文章专门提及过,CoreFoundation和Foundation对象在ARC中处理也是不一样的。
    内存优化之ARC和Core Foundation object

    |

    CFRunLoopRef 的代码是开源的,你可以点击这里 下载到整个 CoreFoundation 的源码来查看。
    (Update: Swift 开源后,苹果又维护了一个跨平台的 CoreFoundation 版本,这个版本的源码可能和现有 iOS 系统中的实现略不一样,但更容易编译,而且已经适配了 Linux/Windows。)

    3.2 RunLoop 与线程的关系

    CFRunLoop 是基于 pthread 来管理的

    苹果不允许直接创建 RunLoop,它只提供了两个自动获取的函数:CFRunLoopGetMain() 和 CFRunLoopGetCurrent()

    线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里。线程刚创建时并没有 RunLoop,如果你不主动获取,那它一直都不会有。RunLoop 的创建是发生在第一次获取时,RunLoop 的销毁是发生在线程结束时。你只能在一个线程的内部获取其 RunLoop(主线程除外)

    polen:
    说明下,一个线程只能有唯一对应的runloop;但这个根runloop里可以嵌套子runloops

    屏幕快照 2016-03-30 下午5.44.45.png

    3 详细说说润runloop的内部

    屏幕快照 2016-03-30 下午12.03.40.png

    一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

    CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。
    • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
    • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

    polen:
    简单理解就是app用到的都是source0, 系统级的调用时source1

    【问】:
    就是UIButton点击事件是source0还是source1:
    (打印堆栈看的话是从source0调出的)
    【答】:
    首先是由那个Source1 接收IOHIDEvent,之后在回调 __IOHIDEventSystemClientQueueCallback() 内触发的 Source0,Source0 再触发的 _UIApplicationHandleEventQueue()。所以UIButton事件看到是在 Source0 内的。你可以在 __IOHIDEventSystemClientQueueCallback 处下一个 Symbolic Breakpoint 看一下。

    CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

    polen:
    toll-free bridged :
    Core Foundation 和 Foundation 之间交换使用数据类型的技术就叫 Toll-Free Bridging.
    这里在ARC体现很明显,ARC是不处理Core Foundation,解决方案是使用 __bridge, __bridge_retained, __bridge_transfer 等进行指针转换
    详情可查看:
    内存优化之ARC和Core Foundation object
    Toll-Free Bridging

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

    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
        kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
        kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
        kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
        kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
        kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
    };
    
    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop
        kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer
        kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
        kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
        kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒
        kCFRunLoopExit          = (1UL << 7), // 即将退出Loop
    };
    

    上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

    |

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

    应用场景举例:
    主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为"Common"属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。
    有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 "commonModeItems" 中。"commonModeItems" 被 RunLoop 自动更新到所有具有"Common"属性的 Mode 里去。

    屏幕快照 2016-03-30 下午6.00.59.png

    3.3RunLoop 的内部逻辑

    屏幕快照 2016-03-30 下午6.03.48.png

    可以看到,实际上 RunLoop 就是这样一个函数,其内部是一个 do-w
    hile 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。

    屏幕快照 2016-03-30 下午6.21.59.png

    polen:
    解释一下这个图:

    SetupThisRunLoopRunTimoutTimer(); //这个是设置一个过期时间,防止runloop无止境的跑下去,由CGD的timer实现,用于检测这次runloop跑了多久
    do {}里面:
    首先
    __CFRunLoopOoObservers(...timers);//告诉observer:我要跑timer了(通知观察者任何即将要开始的定时器)
    __CFRunLoopOoObservers(...Sources);//告诉observer:我要跑source了(通知观察者任何即将启动的非基于端口的源)
    
    __CFRunLoopOoBlocks();
    __CFRoopLoopOoSource0(); //遍历source0跑
    
    CheckIfExistMessagesInMainDispatchQweue();//询问GCD是否有主线程的东西,需要我runloop去跑
    
    之后告诉observer我要开始睡,Zzzz...
    
    ...
    
    直到它被唤醒 received mach_msg,wake up 
    
    //唤醒的场景:
    # a>. 某一事件到达基于端口的源
    # b>. 定时器启动
    # c>. Run loop设置的时间已经超时
    # d>. Run loop被显式唤醒
    
    
    __CFRunLoopOoObservers(kCFRunLoopAfterWaiting) //告诉observer我醒了
    
    接着
    if(){
    }else if (){
    }else{
    }...
    根据唤醒的端口来处理事务:
    1.如果用户定义的定时器启动,处理定时器事件并重启run loop。再次进入__CFRunLoopOoObservers(...timers);
    2.如果是被GCD唤醒,则调用GCD的事件
    3.其他场景是由source1 (基于port事件)触发,做对应的事件处理(比如网络等等) 
    
    
    总结下:
    这里就是如果在do里面睡眠,就一直睡;
    如果没有睡眠,同时没有超时(说明被唤醒了),就开始在while里执行各种runloop的东西
    
    

    RunLoop的挂起与唤醒

    1.制定用于唤醒的mach_port端口
    2.调用mach_msg监听唤醒端口,被唤醒前,系统内核将这个线程挂起,停留在mach_msg_trap
    3.由另外一个线程(或另一个进程中的某个线程)向内核发送这个端口的msg后,trap状态被唤醒,RunLoop继续开始干活
    
    屏幕快照 2016-03-30 下午6.20.48.png

    |

    3.4RunLoop 的底层实现

    OSX/iOS 的系统架构和Darwin 这个核心的架构如下:


    屏幕快照 2016-03-30 下午6.06.58.png

    从上面代码可以看到,RunLoop 的核心是基于 mach port 的,其进入休眠时调用的函数是 mach_msg()。为了解释这个逻辑,下面稍微介绍一下 OSX/iOS 的系统架构。

    可以看到,系统默认注册了5个Mode:

    1. kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。
    2. UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。
    3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。
    4: GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
    5: kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。
    

    4. 补充一些杂项

    4.1. autorelease究竟是在什么时候释放

    答:
    UIKit通过RunLoopObserver在RunLoop两次Sleep间对AutoreleasePool进行pop和push,将这次Loop中产生的Autorelease对象释放

    屏幕快照 2016-03-30 下午6.00.00.png runloop + autorelease流程图

    这个图来自一位大牛-微博@iOS程序犭袁
    对这个图的解释点击这里

    4.2 一个TableView延迟加载图片的新思路

    [self.avatarImageView performSelector:@s;elector(serImage:)    
                            withObjetc:downloadedImage
                            afterDelay:0
                            inModes:@[NSDefaultRunLoopMode]];
    

    4.3 考一个问题:

    有这么一个场景,我们要做一个SDK, 这个函数不能使用回调,直接在接口里面return 结果,但是这个函数又必须先弹出一个登录框,让用户输入用户名密码后,SDK再返回结果,请问如何实现:
    polen:
    说明下,sdk的接口一般都是会单独有个线程去做自己的事情,但是弹出登录框,这个行为必然需要在主线程里面去做(main Thread), 但是题目要求直接return结果,言外之意是,放弃block相关的想法,那么如何实现呢?

    知道的同学可以在评论里回复哈.

    相关文章

      网友评论

      • 践行者:1.CFRunLoopRun()
        2.CFRunLoopStop()

      本文标题:[iOS]runloop - iOS界的EventLoop

      本文链接:https://www.haomeiwen.com/subject/einslttx.html