前言
Runloop在iOS中是一个很重要的组成部分,关于任何单线程的UI模型都必须运用EvenLoop才能够接连处理不同的事情,而RunLoop便是EvenLoop模型在iOS中的完成。在前面的几篇文章中,我已经介绍了Runloop的底层原理等,这篇文章主要是从实践开发的角度,探讨一下实践上在哪些场景下,咱们能够去运用RunLoop
线程保活
在实践开发中,咱们通常会遇到常驻线程的创建,比方说发送心跳包,这就能够在一个常驻线程来发送心跳包,而不干扰主线程的行为,再比方音频处理,这也能够在一个常驻线程中来处理。以前在Objective-C中运用的AFNetworking 1.0就运用了RunLoop来进行线程的保活。
值得注意的是RunLoop的mode中至少需求一个port/timer/observer,不然RunLoop只会履行一次就退出了
中止Runloop
脱离RunLoop一共有两种办法:其一是给RunLoop配置一个超时的时刻,其二是主动通知RunLoop脱离。Apple在文档中是引荐第一种办法的,假如能直接定量的办理,这种办法当然是最好的
设置超时时刻
可是实践中咱们无法准确的去设置超时的时刻,比方在线程保活的比如中,咱们需求保证线程的RunLoop一向坚持运转中,所以结束的时刻是一个变量,而不是常量,要到达这个目标咱们能够结合一下RunLoop供给的API,在开端的时候,设置RunLoop超时时刻为无限,可是在结束时,设置RunLoop超时时刻为当时,这样变相经过控制timeout的时刻中止了RunLoop,具体代码如下
直接中止
CoreFoundation供给了API:**CFRunLoopStop()
** 可是这个办法只会中止当时这次循环的RunLoop,并不会完全中止RunLoop。那么有没有其它的战略呢?咱们知道RunLoop的Mode中必须要至少有一个port/timer/observer才会作业,不然就会退出,而CF供给的API中正好有:
CF_EXPORT void CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFRunLoopMode mode);CF_EXPORT void CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);CF_EXPORT void CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFRunLoopMode mode)
所以很天然的联想到假如移除source/timer/observer, 那么这个计划可不能够中止RunLoop呢?
**答案是否定的,这一点在Apple的官方文档中有比较具体的描绘:
**
❝Although removing a run loop’s input sources and timers may also cause the run loop to exit, this is not a reliable way to stop a run loop. Some system routines add input sources to a run loop to handle needed events. Because your code might not be aware of these input sources, it would be unable to remove them, which would prevent the run loop from exiting.
简而言之,便是你无法保证你移除的便是全部的source/timer/observer,由于体系或许会增加一些必要的source来处理事情,而这些source你是无法确保移除的.
延迟加载图片
这是一个很常见的运用办法,由于咱们在滑动scrollView/tableView/collectionView的进程,总会给cell设置图片,可是直接给cell的imageView设置图片的进程中,会涉及到图片的解码操作,这个就会占用CPU的核算资源,或许导致主线程发生卡顿,所以这儿能够将这个操作,不放在trackingMode,而是放在defaultMode中,经过一种取巧的办法来解决或许的功能问题
卡顿监测
现在来说,一共有三种卡顿监测的计划,可是根本上每一种卡顿监测的计划都和RunLoop是有相关的
CADisplayLink(FPS)
YYFPSLabel选用的便是这个计划,FPS(Frames Per Second)代表每秒烘托的帧数,一般来说,假如App的FPS坚持50~60之间,用户的体会便是比较流通的,可是Apple自从iPhone支撑120HZ的高刷之后,它发明晰一种ProMotion的动态屏幕改写率的技能,这种办法根本就不能运用了,可是这儿仍旧供给已作参阅。
这儿值得注意的技能细节是运用了NSObject来做办法的转发,在OC中能够运用NSProxy来做音讯的转发,效率更高。
// 笼统的超类,用来充任其它对象的一个替身
// Timer/CADisplayLink能够运用NSProxy做音讯转发,能够防止循环引用
// swift中咱们是没发运用NSInvocation的,所以咱们直接运用NSobject来做音讯转发
class WeakProxy: NSObject {
private weak var target: NSObjectProtocol?
init(target: NSObjectProtocol) {
self.target = target
super.init()
}
override func responds(to aSelector: Selector!) -> Bool {
return (target?.responds(to: aSelector) ?? false) || super.responds(to: aSelector)
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
return target
}
}
class FPSLabel: UILabel {
var link: CADisplayLink!
var count: Int = 0
var lastTime: TimeInterval = 0.0
fileprivate let defaultSize = CGSize.init(width: 80, height: 20)
override init(frame: CGRect) {
super.init(frame: frame)
if frame.size.width == 0 || frame.size.height == 0 {
self.frame.size = defaultSize
}
layer.cornerRadius = 5.0
clipsToBounds = true
textAlignment = .center
isUserInteractionEnabled = false
backgroundColor = UIColor.white.withAlphaComponent(0.7)
link = CADisplayLink.init(target: WeakProxy.init(target: self), selector: #selector(FPSLabel.tick(link:)))
link.add(to: RunLoop.main, forMode: .common)
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
deinit {
link.invalidate()
}
@objc func tick(link: CADisplayLink) {
guard lastTime != 0 else {
lastTime = link.timestamp
return
}
count += 1
let timeDuration = link.timestamp - lastTime
// 1、设置改写的时刻: 这儿是设置为1秒(即每秒改写)
guard timeDuration >= 1.0 else { return }
// 2、核算当时的FPS
let fps = Double(count)/timeDuration
count = 0
lastTime = link.timestamp
// 3、开端设置FPS了
let progress = fps/60.0
let color = UIColor(hue: CGFloat(0.27 * (progress - 0.2)), saturation: 1, brightness: 0.9, alpha: 1)
self.text = "\(Int(round(fps))) FPS"
self.textColor = color
}
}
子线程Ping
这种办法是创建了一个子线程,经过GCD给主线程增加异步使命:修正是否超时的参数,然后让子线程休眠一段时刻,假如休眠的时刻结束之后,超时参数未修正,那阐明给主线程的使命并没有履行,那么这就阐明主线程的上一个使命还没有做完,那就阐明卡顿了,这种办法其实和RunLoop没有太多的相关,它不依靠RunLoop的状况。在ANREye(github.com/zixun/ANREy…)中是选用子线程Ping的办法来监测卡顿的
class PingMonitor {
static let timeoutInterval: TimeInterval = 0.2
static let queueIdentifier: String = "com.queue.PingMonitor"
private var queue: DispatchQueue = DispatchQueue(label: queueIdentifier)
private var isMonitor: Bool = false
private var semphore: DispatchSemaphore = DispatchSemaphore(value: 0)
func startMonitor() {
guard isMonitor == false else { return }
isMonitor = true
queue.async {
while self.isMonitor {
var timeout = true
DispatchQueue.main.async {
timeout = false
self.semphore.signal()
}
Thread.sleep(forTimeInterval:PingMonitor.timeoutInterval)
// 阐明等了timeoutInterval之后,主线程依然没有履行派发的使命,这儿就以为它是处于卡顿的
if timeout == true {
//TODO: 这儿需求取出溃散办法栈中的符号来判别为什么呈现了卡顿
// 能够运用微软的结构:PLCrashReporter
}
self.semphore.wait()
}
}
}
}
这个办法在正常状况下会每隔一段时刻让主线程履行GCD派发的使命,会形成部分资源的浪费,而且它是一种主动的去Ping主线程,并不能很及时的发现卡顿问题,所以这种办法会有一些缺陷
实时监控
而咱们知道,主线程中使命都是经过RunLoop来办理履行的,所以咱们能够经过监听RunLoop的状况来知道是否会呈现卡顿的状况,一般来说,咱们会监测两种状况:第一种是kCFRunLoopAfterWaiting
的状况,第二种是kCFRunLoopBeforeSource
的状况。为什么是两种状况呢?
首先看第一种状况kCFRunLoopAfterWaiting
,它会在RunLoop被唤醒之后回调这种状况,然后根据被唤醒的端口来处理不同的使命,假如处理使命的进程中耗时过长,那么下一次检查的时候,它依然是这个状况,这个时候就能够阐明它卡在了这个状况了,然后能够经过一些战略来提取出办法栈,来判别卡顿的代码。同理,第二种状况也是相同的,阐明一向处于kCFRunLoopBeforeSource
状况,而没有进入下一状况(即休眠),也发生了卡顿
class RunLoopMonitor {
private init() {}
static let shared: RunLoopMonitor = RunLoopMonitor()
var timeoutCount = 0
var runloopObserver: CFRunLoopObserver?
var runLoopActivity: CFRunLoopActivity?
var dispatchSemaphore: DispatchSemaphore?
// 原理:进入睡觉前办法的履行时刻过长导致无法进入睡觉,或者线程唤醒之后,一向没进入下一步
func beginMonitor() {
let uptr = Unmanaged.passRetained(self).toOpaque()
let vptr = UnsafeMutableRawPointer(uptr)
var context = CFRunLoopObserverContext.init(version: 0, info: vptr, retain: nil, release: nil, copyDescription: nil)
runloopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
CFRunLoopActivity.allActivities.rawValue,
true,
0,
observerCallBack(),
&context)
CFRunLoopAddObserver(CFRunLoopGetMain(), runloopObserver, .commonModes)
// 初始化的信号量为0
dispatchSemaphore = DispatchSemaphore(value: 0)
DispatchQueue.global().async {
while true {
// 计划一:能够经过设置单次超时时刻来判别 比方250毫秒
// 计划二:能够经过设置接连屡次超时便是卡顿 戴铭在GCDFetchFeed中以为接连三次超时80秒便是卡顿
let st = self.dispatchSemaphore?.wait(timeout: .now() + .milliseconds(80))
if st == .timedOut {
guard !self.runloopObserver else {
self.dispatchSemaphore = nil
self.runLoopActivity = nil
self.timeoutCount = 0
return
}
if self.runLoopActivity == .afterWaiting || self.runLoopActivity == .beforeSources {
self.timeoutCount += 1
if self.timeoutCount < 3 { continue }
DispatchQueue.global().async {
let config = PLCrashReporterConfig(signalHandlerType: .BSD, symbolicationStrategy: .all)
guard let crashReporter = PLCrashReporter(configuration: config) else { return }
let data = crashReporter.generateLiveReport()
do {
let reporter = try PLCrashReport(data: data)
let report = PLCrashReportTextFormatter.stringValue(for: reporter, with: PLCrashReportTextFormatiOS) ?? ""
NSLog("------------卡登时办法栈:\n \(report)\n")
} catch _ {
NSLog("解析crash data过错")
}
}
}
}
}
}
}
func end() {
guard let _ = runloopObserver else { return }
CFRunLoopRemoveObserver(CFRunLoopGetMain(), runloopObserver, .commonModes)
runloopObserver = nil
}
private func observerCallBack() -> CFRunLoopObserverCallBack {
return { (observer, activity, context) in
let weakself = Unmanaged<RunLoopMonitor>.fromOpaque(context!).takeUnretainedValue()
weakself.runLoopActivity = activity
weakself.dispatchSemaphore?.signal()
}
}
}
Crash防护
Crash防护是一个很有意思的点,处于应用层的APP,在履行了某些不被操作体系答应的操作之后会触发操作体系抛出反常信号,可是由于没有处理这些反常从而被系操作体系杀掉的线程,比方常见的闪退。这儿不对Crash做具体的描绘,我会在下一个模块来描绘iOS中的反常。要明确的是,有些场景下,是期望能够捕获到体系抛出的反常,然后将App从过错中康复,重新发动,而不是被杀死。而对应在代码中,咱们需求去手动的重启主线程,已到达持续运转App的目的
CFRunLoopRunInMode(mode, 0.001, false)
由于无法确定RunLoop到底是怎样发动的,所以选用了这种办法来发动RunLoop的每一个Mode,也算是一种替代计划了。由于CFRunLoopRunInMode
在运转的时候自身便是一个循环并不会退出,所以while循环不会一向履行,仅仅在mode退出之后,while循环遍历需求履行的mode,直到持续在一个mode中常驻。
这儿仅仅重启RunLoop,其实在Crash防护里最重要的还是要监测到何时发送溃散,捕获体系的exception信息,以及singal信息等等,捕获到之后再对当时线程的办法栈进行剖析,定位为crash的成因
总结
本篇文章我从线程保活开端介绍了RunLoop在实践开发中的运用,然后主要是介绍了卡顿监测和Crash防护中的高阶运用,当然,RunLoop的运用远不止这些,假如有更多更好的运用,期望我们能够留言交流
参阅:
RunLoop详解
RunLoop之线程保活
浅谈RunLoop