Recently, I have been writing something related to APM, so I have sorted out the schemes of the lag monitoring in iOS. If you do not know the principle of the lag, you can read this article about the skills of iOS to keep the interface smooth, which is well written.
FPS
FPS (Frames Per Second) is the definition in the image field, which refers to the number of Frames rendered Per Second and is usually used to measure the smoothness of the picture. The more Frames Per Second, the smoother the picture will be. 60fps is the best.
There are several ways to monitor FPS, but here is only the most common one, which I first saw in YYFPSLabel. The implementation principle is to add a commonModes CADisplayLink to the main thread RunLoop, each time the screen is refreshed, the CADisplayLink method will be executed, so we can count the number of screen refreshes within 1s, that is, FPS. Here is the code I implemented with Swift:
class WeakProxy: NSObject {
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? {
returntarget } } class FPSLabel: UILabel { var link:CADisplayLink! Var count: Int = 0 var count: Int = 0 var lastTime: link.timestamp -_lasttime: TimeInterval = 0 var _font: UIFont! var _subFont: UIFont! fileprivatelet defaultSize = CGSize(width: 55,height: 20)
override init(frame: CGRect) {
super.init(frame: frame)
if frame.size.width == 0 && frame.size.height == 0 {
self.frame.size = defaultSize
}
self.layer.cornerRadius = 5
self.clipsToBounds = true
self.textAlignment = NSTextAlignment.center
self.isUserInteractionEnabled = falseThe self. The backgroundColor = UIColor. White. WithAlphaComponent (0.7) _font = UIFont (name:"Menlo", size: 14)
if_font ! = nil { _subFont = UIFont(name:"Menlo", size: 4)
}else{
_font = UIFont(name: "Courier", size: 14)
_subFont = UIFont(name: "Courier", size: 4)
}
link = CADisplayLink(target: WeakProxy.init(target: self), selector: #selector(FPSLabel.tick(link:)))
link.add(to: RunLoop.main, for@objc func tick(link: CADisplayLink) {guard lastTime! = 0else {
lastTime = link.timestamp
return
}
count += 1
letTimePassed = link. Timestamp - lastTime // The value is calculated once if the time is greater than or equal to 1 second, that is, the interval at which FPSLabel is refreshed. Do not refresh guard timePassed >= 1 too frequentlyelse {
return
}
lastTime = link.timestamp
let fps = Double(count) / timePassed
count = 0
letProgress = FPS / 60.0letColor = UIColor(Hue: CGFloat(0.27 * (progress-0.2)), saturation: 1, Brightness: 0.9, alpha: 1)let text = NSMutableAttributedString(string: "\(Int(round(fps))) FPS") text.addAttribute(NSAttributedStringKey.foregroundColor, value: color, range: NSRange(location: 0, length: text.length - 3)) text.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.white, range: NSRange(location: text.length - 3, length: 3)) text.addAttribute(NSAttributedStringKey.font, value: _font, range: NSRange(location: 0, length: text.length)) text.addAttribute(NSAttributedStringKey.font, value: _subFont, range: NSRange(location: text.length - 4, length: 1)) self.attributedText = text} // Remove displaylin from Runloop modes deinit {link.invalidate()} required init? (coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented")}}Copy the code
RunLoop
FPS CADisplayLink is also based on RunLoop, and both rely on main RunLoop. Let’s see
Let’s take a look at the code for RunLoop in its abbreviated form
__CFRunLoopRun(runloop, currentMode, seconds,returnAfterSourceHandled) // 2.RunLoop is about to trigger the Timer callback. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers); RunLoop is about to trigger the Source0 (non-port) callback. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources); RunLoop triggers the Source0 (non-port) callback.sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle) // 5. Execute the added block __CFRunLoopDoBlocks(runloop, currentMode); // 6. The RunLoop thread is about to go to sleep. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting); // 7. Call mach_msg and wait for the message to accept mach_port. The thread will sleep until it is awakened by one of the following events. __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(MSg_buffer), &livePort) // Went to sleep // 8. The thread of RunLoop was just woken up. __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting // 9. If a Timer runs out of time, the Timer's callback __CFRunLoopDoTimers(runloop, currentMode, mach_Absolute_time ()) // 10 is triggered. If there are blocks dispatched to main_queue, bloc __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(MSG); __CFRunLoopDoSource1(runloop, currentMode,source1, msg); // 12.RunLoop will exit __CFRunLoopDoObservers(rL, currentMode, kCFRunLoopExit);Copy the code
We can see that the RunLoop calls are mainly between kCFRunLoopBeforeSources and kCFRunLoopAfterWaiting. One might ask that there are also some method calls after kCFRunLoopAfterWaiting. In my understanding, most of the methods that cause lag are between kCFRunLoopBeforeSources and kCFRunLoopAfterWaiting. For example, source0 mainly deals with intra-app events. App itself is responsible for management (start), such as UIEvent(Touch event, GS initiating to RunLoop running and event callback to UI), CFSocketRef. Start a child thread and calculate whether the time between kCFRunLoopBeforeSources and kCFRunLoopAfterWaiting exceeds a certain threshold in real time to determine whether the main thread is running late.
Here the approach is a little different, iOS real-time monitoring is set to 5 times of timeout 50ms is considered to be delayed, Daming in GCDFetchFeed is set to 3 times of timeout 80ms is considered to be delayed code. Here is the code provided in the iOS Real-time Caton monitor:
- (void)start
{
if (observer)
return; // Semaphore = dispatch_semaphore_create(0); CFRunLoopObserverContext Context = {0,(__bridge void*)self,NULL,NULL}; observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context); CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes); Dispatch_async (dispatch_get_global_queue(0, 0), ^{while (YES)
{
long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if(st ! = 0) {if(! observer) { timeoutCount = 0; semaphore = 0; activity = 0;return;
}
if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
{
if (++timeoutCount < 5)
continue;
PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD
symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
NSData *data = [crashReporter generateLiveReport];
PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter
withTextFormat:PLCrashReportTextFormatiOS];
NSLog(@"------------\n%@\n------------", report); } } timeoutCount = 0; }}); }Copy the code
The child thread Ping
However, since the main thread RunLoop is basically in a Before Waiting state when idle, this detection method can always identify the main thread as being stalled even if nothing has occurred. The main thread must be between kCFRunLoopBeforeSources and kCFRunLoopAfterWaiting. Set the flag bit to YES for each detection, and then send the task to the main thread and set the flag bit to NO. Then the child thread sleeps the timeout threshold and determines whether the flag bit is successfully set to NO. If it does not indicate that the main thread has stalled. In ANREye, the child thread Ping is used to detect the lag.
@interface PingThread : NSThread
......
@end
@implementation PingThread
- (void)main {
[self pingMainThread];
}
- (void)pingMainThread {
while(! self.cancelled) { @autoreleasepool { dispatch_async(dispatch_get_main_queue(), ^{ [_lock unlock]; }); CFAbsoluteTime pingTime = CFAbsoluteTimeGetCurrent(); NSArray *callSymbols = [StackBacktrace backtraceMainThread]; [_lock lock];if (CFAbsoluteTimeGetCurrent() - pingTime >= _threshold) {
......
}
[NSThread sleepForTimeInterval: _interval];
}
}
}
@end
Copy the code
Here’s what I did with Swift:
public class CatonMonitor {
enum Constants {
static letTimeOutInterval: TimeInterval = 0.05 staticlet queueTitle = "com.roy.PerformanceMonitor.CatonMonitor"
}
private var queue: DispatchQueue = DispatchQueue(label: Constants.queueTitle)
private var isMonitoring = false
private var semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
public init() {}
public func start() { guard ! isMonitoringelse { return }
isMonitoring = true
queue.async {
while self.isMonitoring {
var timeout = true
DispatchQueue.main.async {
timeout = false
self.semaphore.signal()
}
Thread.sleep(forTimeInterval: Constants.timeOutInterval)
if timeout {
let symbols = RCBacktrace.callstack(.main)
for symbol in symbols {
print(symbol.description)
}
}
self.semaphore.wait()
}
}
}
public func stop() {
guard isMonitoring else { return }
isMonitoring = false}}Copy the code
The CPU exceeds 80%
This is what the Matrix-ios Caton monitor says:
We also believe that too high CPU may also cause application lag, so when the child thread checks the status of the main thread, if it detects that the CPU usage is too high, it will take a snapshot of the current thread and save it in a file. At present, it is considered in wechat application that the single-core CPU usage exceeds 80%, which means that the CPU usage is too high.
This method generally cannot be used alone as caton monitoring, but it can work together with other methods just like wechat Matrix.
Daming also captures the function call stack in GCDFetchFeed if CPU usage exceeds 80%. Here is the code:
#define CPUMONITORRATE 80
+ (void)updateCPU {
thread_act_array_t threads;
mach_msg_type_number_t threadCount = 0;
const task_t thisTask = mach_task_self();
kern_return_t kr = task_threads(thisTask, &threads, &threadCount);
if(kr ! = KERN_SUCCESS) {return;
}
for (int i = 0; i < threadCount; i++) {
thread_info_data_t threadInfo;
thread_basic_info_t threadBaseInfo;
mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;
if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {
threadBaseInfo = (thread_basic_info_t)threadInfo;
if(! (threadBaseInfo->flags & TH_FLAGS_IDLE)) { integer_t cpuUsage = threadBaseInfo->cpu_usage / 10;ifNSString *reStr = smStackOfThread(threads[I]); (cpuUsage > CPUMONITORRATE) {// cpuUsage > CPUMONITORRATE SMCallStackModel *model = [[SMCallStackModel alloc] init]; model.stackStr = reStr; / / records in the database [[[SMLagDB shareInstance] increaseWithStackModel: model] subscribeNext: ^ (id) x {}]. // NSLog(@"CPU useage Overload Thread stack: \n%@",reStr);
}
}
}
}
}
Copy the code
Catton method stack information
When we get the time of the block, we need to get the stack of the block immediately. There are two ways to get the stack frame. One is to walk through the stack frame, and I get the stack of any thread on iOS, which is very detailed, and I open-source the code RCBacktrace. Backtrace-swift = backtrace-swift = backtrace-swift = backtrace-swift = backtrace-swift = backtrace-swift = backtrace-swift = backtrace-swift
Refer to the article
Quality control – caton detection Matrix – iOS caton monitor 13 | how to utilize the RunLoop principle to monitor caton? IOS Real-time Lag Monitoring iOS Development — SUMMARY of APP Performance Detection Schemes (I)