RunLoop学习笔记

作者: 箪食豆羹 | 来源:发表于2017-02-18 20:34 被阅读255次

    参考

    深入理解RunLoop

    深入研究 Runloop 与线程保活

    RunLoop分享by孙源

    RunLoop的概念

    RunLoop是一个机制,让线程能随时处理事件但并不退出。这种机制实现的关键点在于:如何管理事件/消息,如何让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。

    iOS提供了两个这样的对象:NSRunLoopCFRunLoopRef

    • CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
    • NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

    Runloop和线程之间的关系

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

    RunLoop的组成

    • Timer
      • 理解的Timer
    • SourceRunLoop数据源的抽象类protocol)
      • Source0:处理App内部时间,App自己负责触发(UIEventCFSocket
      • Source1:由RunLoopmach内核管理,由mach-port驱动
    • Observer
      • 许多机制都由Observer来触发
        • 例如CAAnimation,在afterwaiting收集完所有animation后才执行动画

    RunLoop的Mode

    • NSDefaultRunLoopModekCFRunLoopDefaultMode):App的默认 Mode,通常主线程是在这个 Mode 下运行的
    • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
    • UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用(iOS不公开提供)
    • NSRunLoopCommonModeskCFRunLoopCommonModes):Mode集合(iOS不公开提供)
    • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到

    RunLoop只能运行在一个mode下,如果要换mode,当前的loop也需要停下重启成新的。

    例如:ScrollView滚动过程中NSDefaultRunLoopModekCFRunLoopDefaultMode)的mode会切换到UITrackingRunLoopMode来保证ScrollView的流畅滑动,如果我们把一个NSTimer对象以NSDefaultRunLoopModekCFRunLoopDefaultMode)添加到主运行循环中的时候, ScrollView滚动过程中会因为mode的切换,而导致NSTimer将不再被调度,解决方案是将timer添加到NSRunLoopCommonModeskCFRunLoopCommonModes)中或者另起线程避免mode切换来解决。

    RunLoop内部逻辑

    RunLoop 内部的大致逻辑

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

    RunLoop的底层实现

    RunLoop 的核心是基于 mach port 的

    iOS的内核是Mach,在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为"对象"。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。"消息"是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 MachIPC (进程间通信) 的核心。

    为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),陷阱这个概念在 Mach 中等同于系统调用。当你在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg()函数会完成实际的工作。

    RunLoop调用这个函数去接收消息,如果没有别人发送 port 消息过来,内核会将线程置于等待状态。例如你在模拟器里跑起一个 iOS 的 App,然后在 App 静止时点击暂停,你会看到主线程调用栈是停留在mach_msg_trap()这个地方。

    iOS利用RunLoop实现的功能

    • AutoreleasePool

      App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()

      第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

      第二个 Observer 监视了两个事件:BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observerorder 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

      在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

    • 事件响应

      苹果注册了一个 Source1 (基于 mach port的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()

      当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent事件并由 SpringBoard接收。这个过程的详细情况可以参考这里SpringBoard只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种Event,随后用 mach port转发给需要的App进程。随后苹果注册的那个Source1 就会触发回调,并调用_UIApplicationHandleEventQueue()进行应用内部的分发。

      _UIApplicationHandleEventQueue()会把IOHIDEvent处理并包装成UIEvent进行处理或分发,其中包括识别 UIGesture、处理屏幕旋转发送给UIWindow等。通常事件比如UIButton点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

    • 手势识别

      当上面的 _UIApplicationHandleEventQueue()识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer标记为待处理。

      苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的GestureRecognizer,并执行GestureRecognizer的回调。

      当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

    • 界面更新

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

      苹果注册了一个Observer 监听BeforeWaiting(即将进入休眠) 和Exit (即将退出Loop) 事件,回调去执行一个很长的函数:

      _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

    • 定时器

      NSTimer其实就是CFRunLoopTimerRef,他们之间是 toll-free bridged 的。一个NSTimer注册到 RunLoop 后,RunLoop 会为其重复的时间点注册好事件。例如 10:00, 10:10, 10:20 这几个时间点。RunLoop为了节省资源,并不会在非常准确的时间点回调这个TimerTimer 有个属性叫做Tolerance (宽容度),标示了当时间点到后,容许有多少最大误差。

      如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

      CADisplayLink是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的AsyncDisplayLink就是为了解决界面卡顿的问题,其内部也用到了 RunLoop

    • PerformSelecter

      当调用NSObjectperformSelecter:afterDelay:后,实际上其内部会创建一个Timer并添加到当前线程的RunLoop中。所以如果当前线程没有 RunLoop,则这个方法会失效。

      当调用 performSelector:onThread:时,实际上其会创建一个Timer加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

    RunLoop常见应用

    • 使用RunLoopModetableview滑动优化
      • 通过不同的mode的切换,实现滑动时暂停加载图片等,停止滑动时加载
    • NSTimer计时任务
    • autorelease pool
      • RunLoop维护
    • 卡顿检测
      • 利用Observer记录主线程RunLoop休眠的时间
      • 利用Observer记录主线程RunLoop唤醒的时间
      • 计算这个(唤醒时间 - 休眠时间)的值,将其与正常的时间比较,判断当前是否会掉帧
    • Crash的程序回光返照
      • 接收到CrashSignal后手动重启RunLoop
    • 异步Test Case
      • sleep前验证

    用到的框架

    • AFNetworking用于维护线程

      AFNetworking是基于NSURLConnection构建的,为了在后台也能接受回调,会创建一个线程,线程中添加一个RunLoop。由于没有调用RunLoop的停止方法,所以RunLoop不会退出。

    • AsyncDisplayKit

      ASDK 创建了一个名为 ASDisplayNode的对象,并在内部封装了UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 framebackgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。
      并在主线程的RunLoop中添加一个Observer,监听RunLoop进入休眠和退出的回调事件,收到回调后,遍历执行队列中的任务。

    • YYAsyncLayer

      • 实现原理如下:
        • 正常情况下:假设一次RunLoop需要处理50张图片
        • 使用YYAsyncLayer的情况:一次RunLoop处理1张图片,利用50个RunLoop去处理50张图片
          • 注意:在不计算休眠时间的情况下,50个RunLoop处理时间 = 1次RunLoop处理50张图片的时间

    有关RunLoop的问题

    • RunLoop与线程的关系
      • 一对一,一个线程可以有一个RunLoop,也可以没有
      • 主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建
      • RunLoop在第一次获取时创建,在线程结束时销毁
    • RunLoop只是个死循环吗?
      • 不是,RunLoop是个有时间限制的循环
    • 使用while(true)RunLoop哪个好?
      • RunLoop,因为RunLoop可以在不需要使用的时候休眠,节省CPU资源,而while(true)则一直处于CPU活跃状态
    • 为什么我们主线程需要有RunLoop
      • 保持线程存活,接受事件
      • 为了管理AutoreleasePool
    • [NSRunLoop currentRunLoop]实际上做了什么
      • [NSRunLoop currentRunLoop]实则为一个懒加载的方法。它会遍历一张全局静态的数据表,该数据表以线程PID为Key,以与该线程绑定的RunLoopValue。该表创建的时候会首先对当前线程(主线程)的PID放入一个RunLoop
    • RunLoopautorelease pool的关系
      • 对于每一个Runloop, 系统会隐式创建一个Autorelease pool,这样所有的release pool会构成一个象CallStack一样的一个栈式结构,对象会自动被放入栈顶的AutoreleasePool中,在每一个Runloop结束时,当前栈顶的Autorelease pool会被销毁,这样这个pool里的每个Object会被release
      • 两次pop两次push,均利用Observer实现
        • 进入后push
        • 睡眠前pop
        • 睡眠后push
        • 离开前pop
    • GCDdispatch_get_main()是如何实现的
      • 当调用dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch会向主线程的RunLoop发送消息,RunLoop会被唤醒,并从消息中取得这个block,并在回调\__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__里执行这个block。但这个逻辑仅限于 利用GCDblock分发到主线程,分发到其他线程仍然是由libDispatch处理的。
      • GCD有自己的线程池,当需要使用到线程的时候随机找一个线程来跑,但是主线程是唯一的,使用RunLoop的主线程
    • 如何切换Mode?为什么要这样做?
      • 先离开,重新进入后切换Mode
      • 这样是为了保证Mode里面的TimerSourcesObserver互不影响
      • 延伸:在主线程Mode切换的时候,RunLoop这一次离开与下一次进入之前有一段间隔,这段间隔会对我们的应用有影响吗(比如会丢事件吗)?
        • 不会有影响,因为我们会把在这期间收到的事件都放在一个队列中,等待下一次RunLoop进入的时候,RunLoop根据该队列进行处理
    • 使用Timer要注意什么
      • 注意使用内存管理[timer invalidate];及设nil
      • 使用addCommonMode / addUITrackingMode保证精准度
    • CommonModes本质是什么
      • CommonModes是一个标识,CFRunLoopAddCommonMode等于给某个Mode打标识。
      • 这里有个概念叫CommonModes:一个Mode可以将自己标记为Common属性(通过将其 ModeName添加到 RunLoopcommonModes 中)。每当RunLoop 的内容发生变化时,RunLoop都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有Common标记的所有Mode里。
    • NSThread在没有RunLoop的情况下,执行完入口函数,会被立刻关闭吗?
      • 不会立刻关闭,会在执行完后,过段时间被清理
      • 延伸:既然如此,为什么把主线程的RunLoop关闭后,应用会崩溃?
        • 应用保证了主线程一定要有RunLoop,没有RunLoop则崩,与上面问题没有关系

    相关文章

      网友评论

      本文标题:RunLoop学习笔记

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