关于 Runloop,这篇文章写的很是棒,深刻理解RunLoop。我写这篇文章在深度上是不如它的,可是为何还想写一下呢?git
Runloop 是一个偏门的东西,在个人工做经历中,几乎没有使用到它的地方,在我当时学习它时,由于自己对 iOS 整个生态了解不够,不少概念让我很是头疼。github
所以这篇文章我但愿能够换一下因果关系,先不要管 Runloop 是什么,让咱们从需求入手,看看 Runloop 能作什么,当你实现过一次以后,回头看这些高屋建瓴的文章,可能会更有启发性。swift
本文涉及的代码托管在:github.com/tianziyao/R…api
首先先记下 Runloop 负责作什么事情:数组
保证程序不退出和监听应该比较容易理解,用伪代码来表示,大体是这样:缓存
// 退出
var exit = false
// 事件
var event: UIEvent? = nil
// 事件队列
var events: [UIEvent] = [UIEvent]()
// 事件分发/响应链
func handle(event: UIEvent) -> Bool {
return true
}
// 主线程 Runloop
repeat {
// 出现新的事件
if event != nil {
// 将事件加入队列
events.append(event!)
}
// 若是队列中有事件
if events.count > 0 {
// 处理队列中第一个事件
let result = handle(event: events.first!)
// 处理完成移除第一个事件
if result {
events.removeFirst()
}
}
// 再次进入发现事件->添加到队列->事件分发->处理事件->移除事件
// 直到 exit=true,主线程退出
} while exit == false复制代码
负责渲染屏幕上全部的 UI,也就是在一次 Runloop 中,事件引发了 UI 的变化,再经过像素点的重绘表现出来。bash
上面讲到的,所有是 Runloop 在系统层面的用处,那么在应用层面,Runloop 能作什么,以及应用在什么地方呢?首先咱们从一个计时器开始。网络
当咱们使用计时器的时候,应该有了解过 timer 的几种构造方法,有的须要加入到 Runloop 中,有的不须要。app
实际上,就算咱们不须要手动将 timer 加入到 Runloop,它也是在 Runloop 中,下面的两种初始化方式是等价的:ide
let timer = Timer(timeInterval: 1,
target: self,
selector: #selector(self.run),
userInfo: nil,
repeats: true)
RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
///////////////////////////////////////////////////////////////////////////
let scheduledTimer = Timer.scheduledTimer(timeInterval: 1,
target: self,
selector: #selector(self.run),
userInfo: nil,
repeats: true)复制代码
如今新建一个项目,添加一个 TextView
,你的 ViewController 文件应该是这样:
class ViewController: UIViewController {
var num = 0
override func viewDidLoad() {
super.viewDidLoad()
let timer = Timer(timeInterval: 1,
target: self,
selector: #selector(self.run),
userInfo: nil,
repeats: true)
RunLoop.current.add(timer, forMode: .defaultRunLoopMode)
}
func run() {
num += 1
print(Thread.current ,num)
}
}复制代码
按照直觉,当 App 运行后,控制台会每秒打印一次,可是当你滚动 TextView
时,会发现打印中止了,TextView
中止滚动时,打印又继续进行。
这是什么缘由呢?在学习线程的时候咱们知道,主线程的优先级是最高的,主线程也叫作 UI 线程,UI 的变化不容许在子线程进行。所以在 iOS 中,UI 事件的优先级是最高的。
Runloop 也有同样的概念,Runloop 分为几种模式:
// App 的默认 Mode,一般主线程是在这个 Mode 下运行
public static let defaultRunLoopMode: RunLoopMode
// 这是一个占位用的Mode,不是一种真正的Mode,用于区分 defaultMode
public static let commonModes: RunLoopMode
// 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其余 Mode 影响
public static let UITrackingRunLoopMode: RunLoopMode复制代码
看到这里你们应该能够明白,咱们的 timer 是在 defaultRunLoopMode
中,而 TextView
的滚动则处于 UITrackingRunLoopMode
中,所以二者不能同时进行。
这个问题会在什么场景下出现呢?好比你使用定时器作了轮播,当下面的列表滚动时,轮播图停住了。
那么如今将 timer 的 Mode
修改成 commonModes
和 UITrackingRunLoopMode
再试一下,看看会发生什么有趣的事情?
commonModes
模式下,run
方法会持续进行,不受 TextView
滚动和静止的影响,UITrackingRunLoopMode
模式下,当 TextView
滚动时,run
方法执行,当 TextView
静止时,run
方法中止执行。
若是看过一些关于 Runloop 的介绍,咱们应该知道,每一个线程都有 Runloop,主线程默认开启,子线程需手动开启,在上面的例子中,当 Mode 是 commonModes
时,定时器和 UI 滚动同时进行,看起来像是在同时进行,但实际上不管 Runloop Mode 如何变化,它始终是在这条线程上循环往复。
你们都知道,在 iOS 开发中有一条铁律,永远不能阻塞主线程。所以,在主线程的任何 Mode 上,也不能进行耗时操做,如今将 run
方法改为下面这样试下:
func run() {
num += 1
print(Thread.current ,num)
Thread.sleep(forTimeInterval: 3)
}复制代码
如今咱们了解了 Runloop 是怎样运行的,以及运行的几种 Mode,下面咱们尝试解决一个实际的问题,TableCell
的内容加载。
在平常的开发中,咱们大体会将 TableView
的加载分为两部分处理:
TableView
,使用模型数据填充 TableCell
;为何咱们大多会这样处理?实际上仍是上面的原则:永远不能阻塞主线程。所以,为了 UI 的流畅,咱们会千方百计将耗时操做从主线程中剥离,才有了上面的方案。
可是有一点,UI 的操做是必须在主线程中完成的,那么,若是使用模型数据填充 TableCell
也是一个耗时操做,该怎么办?
好比像下面这种操做:
let path = Bundle.main.path(forResource: "rose", ofType: "jpg")
let image = UIImage(contentsOfFile: path ?? "") ?? UIImage()
cell.config(image: image)复制代码
在这个例子中,rose.jpg
是一张很大的图片,每一个 TableCell
上有 3 张这样的图片,咱们固然能够将图片在子线程中读取完毕后再更新,不过咱们须要模拟一个耗时的 UI 操做,所以先这样处理。
你们能够下载代码运行一下,滚动 TableView
,FPS 最低会降到 40 如下,这种现象是如何产生的呢?
上面咱们讲到过,Runloop 负责渲染屏幕的 UI 和监听触摸事件,手指滑动时,TableView
随之移动,触发屏幕上的 UI 变化,UI 的变化触发 Cell 的复用和渲染,而 Cell 的渲染是一个耗时操做,致使 Runloop 循环一次的时间变长,所以形成 UI 的卡顿。
那么针对这个过程,咱们怎样改善呢?既然 Cell 的渲染是耗时操做,那么须要把 Cell 的渲染剥离出来,使其不影响 TableView
的滚动,保证 UI 的流畅后,在合适的时机再执行 Cell 的渲染,总结一下,也就是下面这样的过程:
cellForRowAtIndexPath
代理中直接返回 Cell;数组存放代码你们应该能够理解,也就是一个 Block 的数组,可是 Runloop 如何监听呢?
咱们须要知道 Runloop 循环在什么时候开始,在什么时候结束,Demo 以下:
fileprivate func addRunLoopObServer() {
do {
let block = { (ob: CFRunLoopObserver?, ac: CFRunLoopActivity) in
if ac == .entry {
print("进入 Runloop")
}
else if ac == .beforeTimers {
print("即将处理 Timer 事件")
}
else if ac == .beforeSources {
print("即将处理 Source 事件")
}
else if ac == .beforeWaiting {
print("Runloop 即将休眠")
}
else if ac == .afterWaiting {
print("Runloop 被唤醒")
}
else if ac == .exit {
print("退出 Runloop")
}
}
let ob = try createRunloopObserver(block: block)
/// - Parameter rl: 要监听的 Runloop
/// - Parameter observer: Runloop 观察者
/// - Parameter mode: 要监听的 mode
CFRunLoopAddObserver(CFRunLoopGetCurrent(), ob, .defaultMode)
}
catch RunloopError.canNotCreate {
print("runloop 观察者建立失败")
}
catch {}
}
fileprivate func createRunloopObserver(block: @escaping (CFRunLoopObserver?, CFRunLoopActivity) -> Void) throws -> CFRunLoopObserver {
/* * allocator: 分配空间给新的对象。默认状况下使用NULL或者kCFAllocatorDefault。 activities: 设置Runloop的运行阶段的标志,当运行到此阶段时,CFRunLoopObserver会被调用。 public struct CFRunLoopActivity : OptionSet { public init(rawValue: CFOptionFlags) public static var entry //进入工做 public static var beforeTimers //即将处理Timers事件 public static var beforeSources //即将处理Source事件 public static var beforeWaiting //即将休眠 public static var afterWaiting //被唤醒 public static var exit //退出RunLoop public static var allActivities //监听全部事件 } repeats: CFRunLoopObserver是否循环调用 order: CFRunLoopObserver的优先级,正常状况下使用0。 block: 这个block有两个参数:observer:正在运行的run loop observe。activity:runloop当前的运行阶段。返回值:新的CFRunLoopObserver对象。 */
let ob = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0, block)
guard let observer = ob else {
throw RunloopError.canNotCreate
}
return observer
}复制代码
根据上面的 Demo,咱们能够监听到 Runloop 的开始和结束了,如今在控制器中加入一个 TableView
,和一个 Runloop 的观察者,你的控制器如今应该是这样的:
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
addRunloopObserver()
view.addSubview(tableView)
}
fileprivate func addRunloopObserver() {
// 获取当前的 Runloop
let runloop = CFRunLoopGetCurrent()
// 须要监听 Runloop 的哪一个状态
let activities = CFRunLoopActivity.beforeWaiting.rawValue
// 建立 Runloop 观察者
let observer = CFRunLoopObserverCreateWithHandler(nil, activities, true, Int.max - 999, runLoopBeforeWaitingCallBack)
// 注册 Runloop 观察者
CFRunLoopAddObserver(runloop, observer, .defaultMode)
}
fileprivate let runLoopBeforeWaitingCallBack = { (ob: CFRunLoopObserver?, ac: CFRunLoopActivity) in
print("runloop 循环完毕")
}
fileprivate lazy var tableView: UITableView = {
let table = UITableView(frame: self.view.frame)
table.delegate = self
table.dataSource = self
table.register(TableViewCell.self, forCellReuseIdentifier: "tableViewCell")
return table
}()
}复制代码
如今运行起来,打印信息以下:
runloop 循环完毕
runloop 循环完毕
runloop 循环完毕
runloop 循环完毕复制代码
从这里咱们看到,从控制器的 viewDidLoad
开始,通过几回 Runloop,TableView
成功在屏幕出现,而后进入休眠,当咱们滑动屏幕或者触发陀螺仪、耳机等事件发生时,Runloop 进入工做,处理完毕后再次进入休眠。
而咱们的目的是利用 Runloop 的休眠时间,在用户没有产生事件的时候,能够处理 Cell 的渲染任务。本文的开头咱们提到 Runloop 负责的事情,触摸和网络等事件通常是由用户触发,且执行完 Runloop 会再次进入休眠,那么合适的的事件,也就是时钟了。
所以咱们监听了 defaultMode
,并须要在观察者的回调中启动一个时钟事件,让 Runloop 始终保持在活动状态,可是这个时钟也不须要它执行什么事情,因此我开启了一个 CADisplayLink
,用来显示 FPS。不了解 CADisplayLink
的同窗,将它想象为一个大约 1/60 秒执行一次的定时器就能够了,执行的动做是输出一个数字。
首先咱们声明几个变量:
/// 是否使用 Runloop 优化
fileprivate let useRunloop: Bool = false
/// cell 的高度
fileprivate let rowHeight: CGFloat = 120
/// runloop 空闲时执行的代码
fileprivate var runloopBlockArr: [RunloopBlock] = [RunloopBlock]()
/// runloopBlockArr 中的最大任务数
fileprivate var maxQueueLength: Int {
return (Int(UIScreen.main.bounds.height / rowHeight) + 2)
}复制代码
修改 addRunloopObserver
方法:
/// 注册 Runloop 观察者
fileprivate func addRunloopObserver() {
// 获取当前的 Runloop
let runloop = CFRunLoopGetCurrent()
// 须要监听 Runloop 的哪一个状态
let activities = CFRunLoopActivity.beforeWaiting.rawValue
// 建立 Runloop 观察者
let observer = CFRunLoopObserverCreateWithHandler(nil, activities, true, 0) { [weak self] (ob, ac) in
guard let `self` = self else { return }
guard self.runloopBlockArr.count != 0 else { return }
// 是否退出任务组
var quit = false
// 若是不退出且任务组中有任务存在
while quit == false && self.runloopBlockArr.count > 0 {
// 执行任务
guard let block = self.runloopBlockArr.first else { return }
// 是否退出任务组
quit = block()
// 删除已完成的任务
let _ = self.runloopBlockArr.removeFirst()
}
}
// 注册 Runloop 观察者
CFRunLoopAddObserver(runloop, observer, .defaultMode)
}复制代码
建立 addRunloopBlock
方法:
/// 添加代码块到数组,在 Runloop BeforeWaiting 时执行
///
/// - Parameter block: <#block description#>
fileprivate func addRunloopBlock(block: @escaping RunloopBlock) {
runloopBlockArr.append(block)
// 快速滚动时,没有来得及显示的 cell 不会进行渲染,只渲染屏幕中出现的 cell
if runloopBlockArr.count > maxQueueLength {
let _ = runloopBlockArr.removeFirst()
}
}复制代码
最后将渲染 cell 的 Block 丢进 runloopBlockArr
:
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if useRunloop {
return loadCellWithRunloop()
}
else {
return loadCell()
}
}
func loadCellWithRunloop() -> UITableViewCell {
guard let cell = tableView.dequeueReusableCell(withIdentifier: "tableViewCell") as? TableViewCell else {
return UITableViewCell()
}
addRunloopBlock { () -> (Bool) in
let path = Bundle.main.path(forResource: "rose", ofType: "jpg")
let image = UIImage(contentsOfFile: path ?? "") ?? UIImage()
cell.config(image: image)
return false
}
return cell
}复制代码