RunLoop相关

431 阅读5分钟

概念

  • Runloop是通过内部维护的事件循环来对事件/消息进行管理的一个对象。

  • 事件循环

    • 就是没有消息需要处理时,休眠以避免资源占用。用户态切换到内核态

    • 有消息需要处理时,立刻被唤醒。内核态切换到用户态

img

基本作用

  • 保持程序的持续运行
  • 处理App中的各种事件(比如触摸事件、定时器事件等)
  • 节省CPU资源,提高程序性能:该做事时做事,该休息时休息

Q:main函数为什么可以保证不退出?

  • UIApplicationMain启动主线程的runloop

Runloop 对象

  • Foundation:NSRunLoop

  • Core Foundation:CFRunLoopRef

CFRunLoopRef

image.png

Mode

CFRunLoopModeRef代表RunLoop的运行模式一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer,RunLoop启动时只能选择其中一个Mode,作为currentMode。如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入不同组的Source0/Source1/Timer/Observer能分隔开来,互不影响如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。

当在mode1下运行时,只能接收和处理mode1下的sourcesobserverstimers,是无法接收mode2mode3下的事件回调的。

img

img

常见的2种Mode

  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
  • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

Q:如何将一个Timer同时加入到两个Mode中?

img

  • commonMode不是实际存在的一种Mode
  • 是同步Source/Timer/Observer到多个Mode中的一种技术方案。

ModeItem

  • Source0
    • 当添加事件到Source0,它并不会主动唤醒线程,需要手动唤醒线程。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1
    • 具备唤醒线程的能力。包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。
  • CFRunLoopTimer
    • 基于事件的定时器,和NSTimer是可以转换的。
  • CFRunLoopObserver
    • 观测时间点
      • kCFRunLoopEntry即将进入Loop
      • kCFRunLoopBeforeTimers即将处理 Timer
      • kCFRunLoopBeforeSources即将处理 Source
      • kCFRunLoopBeforeWaiting(用户态切内核态)
      • kCFRunLoopAfterWaiting(内核态切用户态)
      • kCFRunLoopExit即将退出

Q:唤醒休眠的Runloop?

  • Source1
  • Timer事件
  • 外部手动唤醒

事件循环机制

RunLoop事件循环机制

img

RunLoop内部是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回。RunLoop t通过调用mach_msg函数进入休眠等待唤醒状态。

runloop 和线程关系

  • 每条线程都有唯一的一个与之对应的RunLoop对象
  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
  • RunLoop会在线程结束时销毁
  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop

线程安全

  • CFRunLoop系列函数是线程安全的
  • NSRunLoop系列函数不是线程安全的

启动RunLoop

通过CFRunLoopRun系列函数启动RunLoop,启动时可以指定超时时间。RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,你可以添加一个一次性timer到RunLoop然后再调用CFRunLoopRun。

退出RunLoop

  • 启动RunLoop时制定超时时间
  • 通过 CFRunLoopStop主动退出

Q:怎样实现一个常驻线程?

  • 为当前线程开启一个RunLoop
  • 向该RunLoop中添加一个Port/Source等维持RunLoop的事件循环。
  • 启动该RunLoop

img

Q:怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作?

  • 子线程抛回给主线程更新UI的逻辑,包装起来,提交到主线程的kCFRunLoopDefaultMode下。
  • 当前用户滑动tableView处于UITrackingRunLoopMode模式下就不会执行该逻辑。
  • 当用户停止滑动之后,当前线程mode切换到kCFRunLoopDefaultMode下,就会处理更新UI的逻辑。

RunLoop应用

苹果用RunLoop实现的功能

AutoreleasePool、事件响应、手势识别、界面更新、定时器、PerformSelecter、GCD、网络请求底层等都用到了RunLoop

解决NSTimer事件在列表滚动时不执行问题

  • 在滑动时,currentMode会从kCFRunLoopDefaultMode切换到UITrackingRunLoopMode
  • 可以通过CFRunLoopAddTimer(runLoop,timer,commonMode)函数,将timer添加到commonMode当中。
  • commonMode的作用就是将某一个事件源同步到多个mode当中。

Runloop中自动释放池创建和释放时机

-系统在Runloop开始处理一个事件时创建一个autoreleaspool。

  • 系统会在处理完一个事件后释放 autoreleaspool 。
  • 我们手动创建的 autoreleasepool 会在 block 执行完成之后进行 drain 操作。需要注意的是:当 block 以异常结束时,pool 不会被 drain Pool 的 drain 操作会把所有标记为 autorelease 的对象的引用计数减一,但是并不意味着这个对象一定会被释放掉,我们可以在 autorelease pool 中手动 retain 对象,以延长它的生命周期(在 MRC 中)。 通过_objc_autoreleasePoolPush和_objc_autoreleasePoolPop来创建和释放自动释放池,底层是通过AutoreleasePoolPage来实现的。
  • 自动释放池是由 AutoreleasePoolPage 以双向链表的方式实现的
  • 当对象调用 autorelease 方法时,会将对象加入 AutoreleasePoolPage 的栈中
  • 调用 AutoreleasePoolPage::pop 方法会向栈中的对象发送 release 消息 关于自动释放池的原理,可以参考这篇文章自动释放池的前世今生

监控卡顿

可以通过监控runloop的 kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting的事件间隔来监控卡顿。

创建子线程执行任务

你可以创建子线程,然后在别的线程通过performSelector:onThread:withObject:waitUntilDone:路由到该子线程进行处理。

AsyncDisplayKit

AsyncDisplayKit( 现在更名为Texture),是Facebook开源的用来异步绘制UI的框架。ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。