一、先来几个问题
1.在viewDidLoad
中添加3个循环的timer
,这3个timer
都会访问并且改变一个可变数组,请问会有什么问题吗?
2.如何获取子线程runLoop,如何让一个runLoop跑起来,runLoop和线程之间的关系
3.如何结束一个runLoop
4.如何用runLoop来做点事,让你的程序高效起来
二、为什么要有RunLoop
1.能够让程序一直处于运行状态,随时能够和用户交互,并且在没有交互的时候休眠,以节省性能
2.调用解耦(Message Queue) 把用户对界面的操作消息放到消息队列中,然后通过运行循环到消息队列中把消息取出来通知对应需要处理的界面操作或者逻辑等。
三、RunLoop in Cocoa
Foundation
NSRunLoop [不是线程安全]
Core Foundation (跨平台)
CFRunLoop [线程安全]
NSRunLoop
纯粹是对CFRunLoop
的一层面向对象的封装,在使用的时候需要考虑线程安全问题
在我们的日常开发中都有哪些是用到了RunLoop
的呢,例如:
NSTimer
UIEvent
Autorelease
NSObject
(NSDelayedPerfroming)
CADisplayLink
CATransition
CAAnimation
dispatch_get_main_queue()
等等...
或者以前的AFNetworking
中的NSURLConnection
的代理的回调就是在一个置顶线程的RunLoop中处理的
四、RunLoop机制
runLoop.png1.Souce
是RunLoop的数据源抽象类(Protocol
),RunLoop定义了两个版本的Souce:
-
Souce0
:处理App内部事件,App自己负责管理(触发),如UIEvent、CFSocket【Source0只包含了一个回调(函数指针),它并不能主动触发事件。使用时你需要先调用CFRunLoopSourceSignal(source),将这个source标记为待处理,然后手动调用CFRunLoopWeakUp(runloop)来唤醒RunLoop,让其处理事件】 -
Source1
:由RunLoop和内核管理,Mach port驱动,如CFMachPort、CFMessagePort 【Source1包含了一个mach_port和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种Source能主动唤醒RunLoop的线程】
2.AutoreleasePool
对对象的释放也是在RunLoop的两次Sleep之间进行Pop和Push将这次循环中产生的Autorelease对象释放
3.RunLoop接收来自两种不同类型源的事件:
- 输入源:提供异步事件,通常是来自另一个线程或是不同应用程序的消息
- 定时源:同步事件,发生在预定时间或者是重复间隔
4.RunLoop在同一时间只能并且必须在一种特定的Mode下跑,如果需要更换Mode则需要停止当前的loop然后在指定的Mode下开启loop。Mode是iOS滑动顺畅的关键,有以下几种:
-
NSDefaultRunLoopMode
默认状态,空闲状态 -
UITrackingRunLoopMode
滑动ScrollView时 -
UIInitializationRunLoopMode
私有的,App启动时 -
GSEventReceiveRunLoopMode
接收系统内部事件的Mode,通常用不到 -
NSRunLoopCommonModes
占位Mode,没有实际作用【但我们老用】
更多苹果内部:Mode:http://iphonedevwiki.net/index.php/CFRunLoop
平常我们把timer添加到RunLoop的defaultMode中发现滚动scrollView的时候timer就不工作了就是因为这个时候的mode切换成UITrackingRunLoopMode
了。如果想要继续工作的话,只需要把timer添加到commonModes中即可,这个时候不论应用程序在什么mode的情况下都会执行timer。从这里我们也可以看出,如果想要保持用户滑动的流畅性,除了把一些耗时操作放到子线程去做还可以把一些必须在主线程执行的操作放到defaultMode中去执行。例如:
private func test() {
let img = UIImage(named: "xxx.png")
perform(#selector(setImg(_:)), with: img) // 默认就是defaultMode
}
@objc private func setImg(_ img: UIImage) {
imageView.image = img
}
5.事件运行循环顺序
1.通知观察者已经输入了运行循环。
2.通知观察者,任何准备好的计时器即将开火。
3.通知观察者,任何不是基于端口的输入源都将被触发。
4.触发任何可以触发的非基于端口的输入源。
5.如果基于端口的输入源已准备就绪并正在等待触发,请立即处理该事件。转到第9步。
6.通知观察者线程即将睡眠。
7.使线程进入睡眠状态,直到发生以下事件之一:
[7.1]一个事件到达一个基于端口的输入源。
[7.2]计时器启动。
[7.3]为运行循环设置的超时值已过期。
[7.4]运行循环明确地被唤醒。
8.通知观察者线程刚刚醒来。
9.处理未决事件。
[9.1]如果用户定义的定时器触发,则处理定时器事件并重新启动循环。转到第2步。
[9.2]如果输入源被触发,则交付事件。
[9.3]如果运行循环被明确唤醒但尚未超时,请重新启动循环。转到第2步。
10.通知观察者运行循环已经退出。
基于这些指定的顺序苹果给出了特定的api可以检测到runloop的所处的运行状态
/* Run Loop Observer Activities */
public struct CFRunLoopActivity : OptionSet {
public init(rawValue: CFOptionFlags)
// 1.进入runloop
public static var entry: CFRunLoopActivity { get }
// 2.即将处理定时源
public static var beforeTimers: CFRunLoopActivity { get }
// 3.即将处理输入源
public static var beforeSources: CFRunLoopActivity { get }
// 4.即将进入睡眠状态
public static var beforeWaiting: CFRunLoopActivity { get }
// 5.被唤醒
public static var afterWaiting: CFRunLoopActivity { get }
// 6.退出runloop
public static var exit: CFRunLoopActivity { get }
// 7.以上所有状态集合
public static var allActivities: CFRunLoopActivity { get }
}
五、Coding
1.主线程的RunLoop是默认开启的,如果是我们自己创建的子线程,默认是没有开启RunLoop的,只有我们去获取的时候才会以懒加载的形式创建。注意RunLoop需要在线程内部获取,一旦开启线程将不会被释放。这里可能有人会问为什么下面要用while循环的方式不停的调用run方法,这是苹果推荐的一种RunLoop开启方式,是为了方便我们停止RunLoop。如果只是调runloop.run()
其实内部也是一个while循环在不停的调用runloop.run(mode: .defaultRunLoopMode, before: Date.distantFuture)
let thread = Thread {
let runloop = RunLoop.current
runloop.add(Port(), forMode: .defaultRunLoopMode)
self.rl = CFRunLoopGetCurrent()
while (self.isRun) {
runloop.run(mode: .defaultRunLoopMode, before: Date.distantFuture)
}
runloop.run()
}
thread.start()
结束一个正在运行的RunLoop
CFRunLoopStop(rl)
thread?.cancel()
isRun = false
2.给RunLoop添加观察者
let rl = CFRunLoopGetMain()
let ob = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0) { (observer, activity) in
if activity == .allActivities {
print("allActivities")
} else if activity == .beforeTimers {
print("beforeTimers")
} else if activity == .beforeSources {
print("beforeSources")
} else if activity == .beforeWaiting {
print("beforeWaiting")
} else if activity == .afterWaiting {
print("afterWaiting")
} else if activity == .entry {
print("entry")
} else if activity == .exit {
print("exit")
}
}
CFRunLoopAddObserver(rl, ob, .defaultMode)
observer = ob
一般来说我们只需要观察beforeWaiting
在一次RunLoop结束的时候做点事情就可以了
3.移除RunLoop中的observer,这里多说一点,在swift
中Core Foundating
中的内容也不用我们手动释放了,系统帮我们处理了,所以我们不用主动释放observer
CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, .defaultMode)
完整代码:
import UIKit
class ViewController: UIViewController {
private var observer: CFRunLoopObserver?
private var thread: WYThread?
private var rl: CFRunLoop!
private var isRun = true
private func startRunLoop() {
thread = WYThread(block: { [unowned self] in
let runloop = RunLoop.current
runloop.add(Port(), forMode: .defaultRunLoopMode)
self.rl = CFRunLoopGetCurrent()
while (self.isRun) {
runloop.run(mode: .defaultRunLoopMode, before: Date.distantFuture)
}
runloop.run()
})
thread?.name = "wy_test"
thread?.start()
}
private func stopRunLoop() {
CFRunLoopStop(rl)
thread?.cancel()
isRun = false
}
private func addRunLoopObserver() {
let rl = CFRunLoopGetMain()
let ob = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, CFRunLoopActivity.allActivities.rawValue, true, 0) { (observer, activity) in
if activity == .allActivities {
print("allActivities")
} else if activity == .beforeTimers {
print("beforeTimers")
} else if activity == .beforeSources {
print("beforeSources")
} else if activity == .beforeWaiting {
print("beforeWaiting")
} else if activity == .afterWaiting {
print("afterWaiting")
} else if activity == .entry {
print("entry")
} else if activity == .exit {
print("exit")
}
}
CFRunLoopAddObserver(rl, ob, .defaultMode)
observer = ob
}
private func removeRunLoopObserver() {
CFRunLoopRemoveObserver(CFRunLoopGetMain(), observer, .defaultMode)
}
}
网友评论