OC面试题 四、Runloop

312 阅读4分钟

RunLoop与线程

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

RunLoop相关的类

  1. Core Foundation中关于RunLoop的5个类
  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

CFRunLoopModeRef

  • 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会立马退出

RunLoop的运行逻辑

image.png

RunLoop在实际开中的应用

  • 控制线程生命周期(线程保活)
  • 解决NSTimer在滑动时停止工作的问题
  • 监控应用卡顿
  • 性能优化

app如何接收到触摸事件的

image.png

为什么只有主线程的runloop是开启的

main()函数中调用UIApplicationMain,这里会创建一个主线程,用于UI处理为了让程序可以一直运行并接收事件,所以在主线程中开启一个runloop,让主线程常驻。

为什么只在主线程刷新UI

我们所有用到的UI都是来自于UIKit这个基础库。因为objc不是一门线程安全的语言,所以存在多线程读写不同步的问题,如果使用加锁的方式操作系统开销很大,会耗费大量的系统资源(内存、时间片轮转、cpu处理速度…),加上上面讲到的系统事件的接收处理都在主线程,如果UI异步线程的话 还会存在 同步处理事件的问题,所以多点触摸手势等一些事件要保持和UI在同一个线程相对是最优解。

另一方面是 屏幕的渲染是 60帧(60Hz/秒), 也就是1秒钟回调60次的频率,(iPad Pro 是120Hz/秒),我们的runloop 理想状态下也会按照时钟周期 回调60次(iPad Pro 120次), 这么高频率的调用是为了 屏幕图像显示能够垂直同步 不卡顿.在异步线程的话是很难保证这个处理过程的同步更新. 即便能保证的话 相对主线程而言 系统资源开销 线程调度等等将会占据大部分资源和在同一个线程只专门干一件事有点得不偿失。

performSelector和runloop的关系

当调用NSObject的performSelector:相关的时候,内部会创建一个timer定时器添加到当前线程runloop中,如果当前线程没有启动runloop,则该方法不会被调用。

开发中遇到最多的问题就是这个performSelector:导致对象的延迟释放,这里开发过程中注意一下,可以用单次的NSTimer替代。

iOS上每个线程可以有多个runloop吗?

每个线程只有一个运行循环,每条线程都有唯一的一个与之对应的RunLoop对象

Runloop与Timer的使用

TimerRunloop的一个触发源,用Timer时,Timer默认添加到当前的runloop中,也可以手动添加到自己新建的runloop

<发现问题:>

当拖拽界面时,timer暂停运行,不拖拽时timer又恢复运行。

<解决问题:>

  • 第一种模式:defaultRunLoopMode设置后,只能在不拖拽当前的其他界面时timer会执行,一旦拖拽当前其他界面就立马转换到其他UI界面执行,因为UI级触摸优先级最高。
  • 第二种模式:UITrackingRunLoopMode设置后,只会在当前界面发生拖拽时也就是触摸事件时,timer会跟踪执行,当不拖拽触发时timer就不会执行。
  • 第三种模式:commonModes设置后,是结合上面的两者,当屏幕触发拖拽时和不触发拖拽时,timer都会执行。

这时候就应该使用Runloop的commonModes模式,能够让定时器在Runloop两种模式切换时也不受影响。

/** 创建timer */
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        //需要执行的代码
 }];

/** 把timer手动添加到自己新建的Runloop中,添加模式为通用的占位模式 */
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

如何使线程保活?