RunLoop

564 阅读4分钟

RunLoop简介

系统维护的一个事件循环,负责处理事件/消息的一个对象

RunLoop数据结构

NSRunLoop是对CFRunLoop的一个封装。 Source0(负责App内部事件,由App负责管理触发,例如UITouch事件)和Timer(又叫Timer Source,基于时间的触发器,上层对应NSTimer)是两个不同的Runloop事件源 Source0需要手动触发不同,Source1可以监听系统端口和其他线程相互发送消息,它能够主动唤醒RunLoop(由操作系统内核进行管理,例如CFMessagePort消息)

RunLoop事件循环机制

RunLoop和NSTimer

timer需要使用NSRunLoopCommonModes。由于runloop每种mode是相互隔离的,所以不能互相接收回调。如果屏幕滚动时,runloop会被切换到UITrackingRunLoopMode上,所以timer不使用NSRunLoopCommonModes则会接收不到回调。其中NSRunLoopCommonModes是一种将timer同步到多个mode中的方案,它并不是一种实际的mode

RunLoop和线程

一一对应的关系,GCD启动的线程默认是没有runloop的,需要自定义runloop来保活线程。 常驻线程大致过程

  1. 在当前线程创建一个runloop
  2. runloop中加入port/source事件
  3. 启动runloop

NSAutoreleasePool(自动释放池)在什么时候释放?

  1. App启动后,主线程的runloop注册两个observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()
  2. 第一个observer是刚进入runloop的时候,会调用_objc_autoreleasePoolPush()创建自动释放池。order = -2147483647(即32位整数最小值)表示其优先级最高,可以保证创建释放池发生在其他所有回调之前;
  3. 第二个observerrunloop将要休眠和退出的时候。在休眠的时候会调用_objc_autoreleasePoolPop()释放池,然后哦_objc_autoreleasePoolPush()创建新池。在退出的时候调用_objc_autoreleasePoolPop()释放池,此时order = 2147483647表示优先级最低,保证其他回调都在其之前完成。
  4. 在主线程写的代码诸如timer,事件回调等。会被主线程的runloop环绕着,所以不会内存泄漏。

手动添加自动释放池:

  1. 编写的不是基于UI框架的程序,例如命令行工具;
  2. 过循环方式创建大量临时对象;
  3. 用非Cocoa程序创建的子线程;

RunLoop在休息的时候是一个什么状态?

我们都知道了RunLoop是一个死循环,有执行任务,退出和休息三种状态,前两个不必说,休息是一种什么状态呢?解释这个问题前,我们先要知道一个 概念:mach_port,mach是一个内核,提供cpu调度,进程间通信等一些基础服务,在 Mach 中,所有的东西都是通过自己的对象实现的,进程、线程和虚拟内存都被称为”对 象”。和其他架构不同, Mach 的对象间不能直接调用,只能通过消息传递的方式实现对象间的通信。”消息”是 Mach 中最基础的概念,一条消息中包含当前端口 local_port 和目标端口 remote_port,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。消息的发送和接收使用<mach/message.h>中的mach_msg()函数,而mach_msg()的本质是一个调用mach_msg_trap(),这相当于一个系统调用,会触发内核状态切换。让程序处于休眠状态。而这个状态就是:APP依然在内存中,但不在主动申请cpu资源(此处不要较真,肯定不是完全不用cpu的),然后一直监听某一个端口(port),等待内核向该端口发送消息,监听到消息后,从睡眠状态中恢复(重新启动RunLoop的循环,处理完事件后继续休眠)

利用runloop监控卡顿

参考

利用监控FPS FPS原理和上面大致相同,只是监控状态为kCFRunLoopBeforeWaiting 和 kCFRunLoopAfterWaiting,然后用1000ms减去这两种状态的时间,再除以16.7

利用runloop优化tableview

其实也是利用runloop的运行机制,观察点在kCFRunLoopBeforeWaiting这个位置,这个位置说明任务基本已经处理完成。然后我们把之前要执行的任务,在这个观察点的回调中执行,以减少主线程压力。然后注册观察者,线程保活基本和上面步骤一直

线程保活,哪些情况下需要保活

为什么必须在主线程操作UI

UIKit并不是一个 线程安全 的类,UI操作涉及到渲染访问各种View对象的属性,如果异步操作下会存在读写问题,而为其加锁则会耗费大量资源并拖慢运行速度。另一方面因为整个程序的起点UIApplication是在主线程进行初始化,所有的用户事件都是在主线程上进行传递(如点击、拖动),所以view只能在主线程上才能对事件进行响应。而在渲染方面由于图像的渲染需要以60帧的刷新率在屏幕上 同时 更新,在非主线程异步化的情况下无法确定这个处理过程能够实现同步更新。