introduction
Runloop is a very important component in iOS. For any single-threaded UI model, EvenLoop must be used to handle different events continuously. RunLoop is the implementation of the EvenLoop model in iOS. In the previous articles, I have introduced the underlying principles of Runloop, etc. This article mainly discusses what scenarios we can actually use RunLoop from the perspective of actual development.
Thread keeps alive
In actual development, we usually encounter the creation of resident threads, such as sending heartbeat packets, which can send heartbeat packets in a resident thread without interfering with the behavior of the main thread. For example, audio processing, which can also be handled in a resident thread. AFNetworking 1.0, which was used in Objective-C, used RunLoop to keep threads alive.
var thread: Thread! func createLiveThread() { thread = (block: { let port = () (port, forMode: .default) () }) () }
It is worth noting that at least one port/timer/observer is needed in the mode of RunLoop, otherwise RunLoop will only be executed once and exit.
Stop Runloop
There are two ways to leave RunLoop: one is to configure a timeout time for RunLoop, and the other is to actively notify RunLoop to leave. Apple recommends the first method in the document. If it can be managed directly and quantitatively, this method is of course the best.
Set timeout
However, in reality, we cannot accurately set the timeout moment. For example, in the example of keeping thread alive, we need to ensure that the thread's RunLoop remains running, so the end time is a variable, not a constant. To achieve this goal, we can combine the API provided by RunLoop. At the beginning, the RunLoop timeout time is set to infinite, but at the end, the RunLoop timeout time is set to the current time, so that RunLoop is stopped by controlling the timeout time in disguise. The specific code is as follows:
var thread: Thread? var isStopped: Bool = false func createLiveThread() { thread = (block: { [weak self] in guard let self = self else { return } let port = () (port, forMode: .default) while ! { (mode: .default, before: ) } }) thread?.start() } func stop() { (#selector(), on: thread!, with: nil, waitUntilDone: false) } @objc func stopThread() { = true (mode: .default, before: ()) = nil }
Stop it directly
CoreFoundation provides an API:CFRunLoopStop()
However, this method will only stop the RunLoop of the current loop and will not completely stop the RunLoop. So are there any other strategies? We know that RunLoop's Mode must have at least one port/timer/observer to work, otherwise it will exit, and the API provided by CF happens to have:
**public func CFRunLoopRemoveSource(_ rl: CFRunLoop!, _ source: CFRunLoopSource!, _ mode: CFRunLoopMode!) public func CFRunLoopRemoveObserver(_ rl: CFRunLoop!, _ observer: CFRunLoopObserver!, _ mode: CFRunLoopMode!) public func CFRunLoopRemoveTimer(_ rl: CFRunLoop!, _ timer: CFRunLoopTimer!, _ mode: CFRunLoopMode!)**
So it is natural to think of if source/timer/observer is removed,So can this solution stop RunLoop?
The answer is no, and this is described in more detail in Apple's official documentation:
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.
In short, you cannot guarantee that you will remove all the source/timer/observer, because the system may add some necessary sources to handle events, and you cannot guarantee that you will remove these sources.
Delay loading pictures
This is a very common way of using it, because when we slide scrollView/tableView/collectionView, we always set pictures for the cell, but when setting pictures for the cell's imageView, the image decoding operation will be involved, which will occupy the CPU's computing resources and may cause the main thread to stutter. Therefore, we can place this operation here instead of trackingMode, but in the defaultMode, and solve possible performance problems through a trick.
func setupImageView() { (onMainThread: #selector(), with: nil, waitUntilDone: false, modes: []) } @objc func setupImage() { () }
Stop monitoring
At present, there are three types of lag monitoring solutions, but basically each lag monitoring solution is related to RunLoop.
CADisplayLink(FPS)
YYFPSLabelThis is the solution used. FPS (Frames Per Second) represents the number of frames rendered per second. Generally speaking, if the App's FPS remains between 50 and 60, the user's experience will be relatively smooth. However, since Apple supported high refresh rate of 120HZ from iPhone, it invented a technology of ProMotion's dynamic screen refresh rate. This method is basically unusable, but it is still provided for reference here.
The technical details worth noting here are that NSObject is used to forward methods. In OC, NSProxy can be used to forward messages, which is more efficient.
// Abstract superclass, used to act as a substitute for other objects// Timer/CADisplayLink can use NSProxy to forward messages, which can avoid circular references// We did not send NSInvocation in swift, so we directly use NSobject to forward messagesclass WeakProxy: NSObject { private weak var target: NSObjectProtocol? init(target: NSObjectProtocol) { = target () } override func responds(to aSelector: Selector!) -> Bool { return (target?.responds(to: aSelector) ?? false) || (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 = (width: 80, height: 20) override init(frame: CGRect) { (frame: frame) if == 0 || == 0 { = defaultSize } = 5.0 clipsToBounds = true textAlignment = .center isUserInteractionEnabled = false backgroundColor = (0.7) link = (target: (target: self), selector: #selector((link:))) (to: , forMode: .common) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } deinit { () } @objc func tick(link: CADisplayLink) { guard lastTime != 0 else { lastTime = return } count += 1 let timeDuration = - lastTime // 1. Set the refresh time: Here is set to 1 second (that is, refresh every second) guard timeDuration >= 1.0 else { return } // 2. Calculate the current FPS let fps = Double(count)/timeDuration count = 0 lastTime = // 3. Start setting up FPS let progress = fps/60.0 let color = UIColor(hue: CGFloat(0.27 * (progress - 0.2)), saturation: 1, brightness: 0.9, alpha: 1) = "\(Int(round(fps))) FPS" = color } }
Child thread ping
This method is to create a child thread and add asynchronous tasks to the main thread through GCD: modify the parameter of whether to time out, and then let the child thread sleep for a period of time. If the timeout parameter is not modified after the sleep time is over, it means that the task to the main thread has not been executed. This means that the previous task of the main thread has not been completed, which means that it is stuck. This method actually has no much connection with RunLoop, and it does not depend on the state of RunLoop. existANREyeIn this paper, child thread ping is used to monitor lags.
At the same time, in order to make these operations synchronous, semaphores are used here.
class PingMonitor { static let timeoutInterval: TimeInterval = 0.2 static let queueIdentifier: String = "" private var queue: DispatchQueue = (label: queueIdentifier) private var isMonitor: Bool = false private var semphore: DispatchSemaphore = (value: 0) func startMonitor() { guard isMonitor == false else { return } isMonitor = true { while { var timeout = true { timeout = false () } (forTimeInterval:) // It means that after waiting for timeoutInterval, the main thread still did not execute the dispatched task, so here it is considered that it is in a stuttering state. if timeout == true { //TODO: Here you need to remove the symbols in the crash method stack to determine why there is a stutter // You can use Microsoft's framework: PLCrashReporter } () } } } }
Under normal circumstances, this method will allow the main thread to execute GCD-distributed tasks every once in a while, which will cause some resources to be wasted. Moreover, it is an active ping of the main thread and cannot detect lag problems in a timely manner, so this method will have some disadvantages.
Real-time monitoring
We know that tasks in the main thread are managed and executed through RunLoop, so we can know whether there will be lag by listening to the status of RunLoop. Generally speaking, we will monitor two states: the first iskCFRunLoopAfterWaiting
The second state iskCFRunLoopBeforeSource
status. Why are there two states?
First look at the first statekCFRunLoopAfterWaiting
, it will call back to this state after RunLoop is awakened, and then handle different tasks according to the awakened port. If the process of processing the task takes too long, then the next time it checks, it will still be in this state. At this time, it can be said that it is stuck in this state. Then some strategies can be used to extract the method stack to judge the stuttering code. Similarly, the second state is the same, which means that it has been inkCFRunLoopBeforeSource
The state, without entering the next state (i.e., sleep), also lag.
class RunLoopMonitor { private init() {} static let shared: RunLoopMonitor = () var timeoutCount = 0 var runloopObserver: CFRunLoopObserver? var runLoopActivity: CFRunLoopActivity? var dispatchSemaphore: DispatchSemaphore? // Principle: The execution time before sleep is too long, causing it to be unable to enter sleep, or after the thread wakes up, it has not entered the next step func beginMonitor() { let uptr = (self).toOpaque() let vptr = UnsafeMutableRawPointer(uptr) var context = (version: 0, info: vptr, retain: nil, release: nil, copyDescription: nil) runloopObserver = CFRunLoopObserverCreate(kCFAllocatorDefault, , true, 0, observerCallBack(), &context) CFRunLoopAddObserver(CFRunLoopGetMain(), runloopObserver, .commonModes) // The initialized semaphore is 0 dispatchSemaphore = (value: 0) ().async { while true { // Scheme 1: You can determine by setting a single timeout time, such as 250 milliseconds // Scheme 2: You can set multiple times of timeouts to be stuck. Dai Ming believes that three times of timeouts to be delayed for 80 seconds in GCDFetchFeed will be delayed. let st = ?.wait(timeout: () + .milliseconds(80)) if st == .timedOut { guard != nil else { = nil = nil = 0 return } if == .afterWaiting || == .beforeSources { += 1 if < 3 { continue } ().async { let config = (signalHandlerType: .BSD, symbolicationStrategy: .all) guard let crashReporter = (configuration: config) else { return } let data = () do { let reporter = try (data: data) let report = (for: reporter, with: PLCrashReportTextFormatiOS) ?? "" NSLog("------------Stuck method stack:\n \(report)\n") } catch _ { NSLog("Parse crash data error") } } } } } } } 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() = activity ?.signal() } } }
Crash Protection
Crash protection is a very interesting point. APPs at the application layer will trigger the operating system to throw an exception signal after performing certain operations that are not allowed by the operating system. However, threads that are killed by the operating system because these exceptions are not processed, such as common crashes. I won't describe Crash in detail here, I will describe exceptions in iOS in the next module. It should be clear that in some scenarios, it is hoped that the exception thrown by the system can be caught, and then the app will be restored from the error and restarted, rather than being killed. Correspondingly in the code, we need to manually restart the main thread, and we have achieved the purpose of continuing to run the App.
let runloop = CFRunLoopGetCurrent() guard let allModes = CFRunLoopCopyAllModes(runloop) as? [CFRunLoopMode] else { return } while true { for mode in allModes { CFRunLoopRunInMode(mode, 0.001, false) } }
CFRunLoopRunInMode(mode, 0.001, false)
Because it is impossible to determine how RunLoop is started, this method is used to start each mode of RunLoop, which is also an alternative. becauseCFRunLoopRunInMode
When running, it is a loop itself and will not exit, so the while loop will not be executed all the time. It is just that after the mode exits, the while loop traverses the mode that needs to be executed until it continues to reside in a mode.
This is just restarting RunLoop. In fact, the most important thing in Crash protection is to monitor when the crash is sent, capture the system's exception information, and singal information, etc. After the capture is made, the method stack of the current thread is analyzed and positioned as the cause of crash.
Matrix Framework
Next, let’s take a look at the application of RunLoop in the Matrix framework. Matrix is a performance monitoring framework open source by Tencent. There is a plug-in in this framework**WCFPSMonitorPlugin
:**This is an FPS monitoring tool that records the call stack of the main thread when the user slides the interface. Its source code is as follows:CADisplayLink
The principle of monitoring lag is the same:
- (void)startDisplayLink:(NSString *)scene { FPSInfo(@"startDisplayLink"); m_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(onFrameCallback:)]; [m_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes]; ... } - (void)onFrameCallback:(id)sender { // Current time: unit is seconds double nowTime = CFAbsoluteTimeGetCurrent(); // Convert units into milliseconds double diff = (nowTime - m_lastTime) * 1000; // 1. If the time interval exceeds the maximum frame interval: then the screen refresh method timed out if (diff > ) { m_currRecorder.dumpTimeTotal += diff; m_dropTime += * pow(diff / , ); // Total timeout exceeds the threshold: Display timeout information if (m_currRecorder.dumpTimeTotal > * ) { FPSInfo(@"diff %lf exceed, begin: %lf, end: %lf, scene: %@, you can see more detail in record id: %d", m_currRecorder.dumpTimeTotal, m_currRecorder.dumpTimeBegin, m_currRecorder.dumpTimeBegin + m_currRecorder.dumpTimeTotal / 1000.0, m_scene, m_currRecorder.recordID); ...... } // 2. If the time interval does not have the maximum frame interval: then the screen refresh method does not time out } else { // Total timeout exceeds the threshold: Display timeout information if (m_currRecorder.dumpTimeTotal > ) { FPSInfo(@"diff %lf exceed, begin: %lf, end: %lf, scene: %@, you can see more detail in record id: %d", m_currRecorder.dumpTimeTotal, m_currRecorder.dumpTimeBegin, m_currRecorder.dumpTimeBegin + m_currRecorder.dumpTimeTotal / 1000.0, m_scene, m_currRecorder.recordID); .... // The total timeout does not exceed the threshold: recount the time to 0 } else { m_currRecorder.dumpTimeTotal = 0; m_currRecorder.dumpTimeBegin = nowTime + 0.0001; } } m_lastTime = nowTime; }
It passes the number of times and the allowable time interval between times as the threshold. If it exceeds the threshold, it will be recorded. If it does not exceed the threshold, it will be counted again. Of course, this framework is not only used as a simple lag monitoring, but also has many performance monitoring functions for use during normal development: including analysis of method stack during crashes, etc.
Summarize
In this article, I introduced the use of RunLoop in actual development from thread keep-alive, and then mainly introduced the advanced use of lag monitoring and Crash protection. Of course, the use of RunLoop is far more than this. If there are more and better uses, I hope everyone can leave a message to communicate.
The above is the detailed content of the RunLoop application example of EvenLoop model in iOS. For more information about the RunLoop model of ios EvenLoop, please follow my other related articles!