序言
之前写了一篇文章介绍如何检测卡顿,iOS实时卡顿检测-RunLoop(附实例)这是借助于信号量Semaphore
来实现的。本介绍第二种方法,采用定时器 NSTimer
实现,原理方面的就不多说了,看之前一篇文章即可,直接上代码。
一 监测的工具类Monitor
- 工具类 Monitor.h
/**
卡顿监控工具类
*/
@interface Monitor : NSObject
/// 单例
+ (instancetype)shareInstance;
/// 开启卡顿监听
- (void)startMonitor;
/// 停止监听
- (void)endMonitor;
/// 打印堆栈信息
- (void)printTraceLog;
@end
- Monitor.m
#import "Monitor.h"
#import
#import
static double _waitStartTime; // 等待启动的时间
@implementation Monitor {
CFRunLoopObserverRef _observer; // runloop observer
double _lastRecordTime; // last record time
NSMutableArray *_backtraces;
}
+ (instancetype)shareInstance {
static dispatch_once_t onceToken;
static id shareInstance;
dispatch_once(&onceToken, ^{
shareInstance = [[self alloc] init];
});
return shareInstance;
}
#pragma mark - start | end
- (void)startMonitor {
[self addMainThreadObserver];
[self addSecondaryThreadAndObserver];
}
- (void)endMonitor {
if (!_observer) {
return;
}
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
CFRelease(_observer);
_observer = NULL;
}
#pragma mark - MainThread runloop observer
/// 添加在主线程的 runloop 监听器
- (void)addMainThreadObserver {
dispatch_async(dispatch_get_main_queue(), ^{
// 建立自动释放池
@autoreleasepool {
// 获得当前线程的 runloop
NSRunLoop *mainRunLoop = [NSRunLoop currentRunLoop];
// 设置runloop observer 的运行环境
/** 第一个参数用于分配observer对象的内存
第二个参数用以设置observer所要关注的事件,详见回调函数myRunLoopObserver中注释
第三个参数用于标识该observer是在第一次进入run loop时执行还是每次进入run loop处理时均执行
第四个参数用于设置该observer的优先级
第五个参数用于设置该observer的回调函数
第六个参数用于设置该observer的运行环境 */
CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
// 创建 runloop observer 对象
self->_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &mainRunLoopObserver, &context);
if (self->_observer) {
// 将 cocoa的 NSRunLoop 类型转换为 Core Foundation 的 CFRunLoopRef 类型
CFRunLoopRef cfRunLoop = [mainRunLoop getCFRunLoop];
// 将新建的 observer 加入到当前 thread 的 runloop 中
CFRunLoopAddObserver(cfRunLoop, self->_observer, kCFRunLoopDefaultMode);
}
}
});
}
void mainRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"YYYY-MM-dd HH:mm:ss:SSS"];
NSString *time = [formatter stringFromDate:[NSDate date]];
switch (activity) {
//The entrance of the run loop, before entering the event processing loop.
//This activity occurs once for each call to CFRunLoopRun and CFRunLoopRunInMode
case kCFRunLoopEntry:
NSLog(@"kCFRunLoopEntry - %@",time);
break;
//Inside the event processing loop before any timers are processed
case kCFRunLoopBeforeTimers:
NSLog(@"kCFRunLoopBeforeTimers - %@",time);
break;
//Inside the event processing loop before any sources are processed
case kCFRunLoopBeforeSources:
NSLog(@"kCFRunLoopBeforeSources - %@",time);
break;
//Inside the event processing loop before the run loop sleeps, waiting for a source or timer to fire.
//This activity does not occur if CFRunLoopRunInMode is called with a timeout of 0 seconds.
//It also does not occur in a particular iteration of the event processing loop if a version 0 source fires
case kCFRunLoopBeforeWaiting:{ // 即将进入休眠-这个时候处理 UI 操作
_waitStartTime = 0;
NSLog(@"kCFRunLoopBeforeWaiting - %@",time);
break;
}
//Inside the event processing loop after the run loop wakes up, but before processing the event that woke it up.
//This activity occurs only if the run loop did in fact go to sleep during the current loop
case kCFRunLoopAfterWaiting:{ // 从休眠中醒来开始做事情了
_waitStartTime = [[NSDate date] timeIntervalSince1970];
NSLog(@"kCFRunLoopAfterWaiting - %@",time);
break;
}
//The exit of the run loop, after exiting the event processing loop.
//This activity occurs once for each call to CFRunLoopRun and CFRunLoopRunInMode
case kCFRunLoopExit:
NSLog(@"kCFRunLoopExit - %@",time);
break;
/*
A combination of all the preceding stages
case kCFRunLoopAllActivities:
break;
*/
default:
break;
}
}
#pragma mark - second thread observer
- (void)addSecondaryThreadAndObserver {
NSThread *thread = [self secondaryThread];
[self performSelector:@selector(addSecondaryTimer) onThread:thread withObject:nil waitUntilDone:YES];
}
#pragma mark - timer
- (void)addSecondaryTimer {
__weak typeof(self)weakSelf = self;
NSTimer *timer = [NSTimer timerWithTimeInterval:0.5 repeats:YES block:^(NSTimer *timer) {
[weakSelf timerFired];
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}
- (void)timerFired {
if (_waitStartTime < 1) { // 因为刚刚经历了kCFRunLoopBeforeWaiting状态,_waitStartTime=0,直接 pass
NSLog(@"timerFired return curTime:%@, waitStartTime:%@",[self getCurTimeStamp], [self getTimeStamp:_waitStartTime]);
return;
}
double currentTime = [[NSDate date] timeIntervalSince1970];
double timeDiff = currentTime - _waitStartTime;
NSLog(@"timerFired curTime:%@, waitStartTime:%@, timeDiff:%f, _lastRecordTime:%f",[self getCurTimeStamp], [self getTimeStamp:_waitStartTime], timeDiff, _lastRecordTime);
// 如果 timeDiff 时间间隔超过 2S,表示 runloop 处于kCFRunLoopAfterWaiting跟kCFRunLoopBeforeWaiting之间状态很长时间
// 即长时间处于kCFRunLoopBeforeTimers和kCFRunLoopBeforeSources状态,就是进行 UI 操作了
if (timeDiff > 2.0) {
NSLog(@"last lastRecordTime:%f waitStartTime:%f",_lastRecordTime,_waitStartTime);
if (_lastRecordTime - _waitStartTime < 0.001 && _lastRecordTime != 0) { // 距离上一次记录堆栈信息时间过短的话,就直接 pass,避免短时间内多次记录堆栈信息
NSLog(@"last return timeDiff:%f waitStartTime:%@ lastRecordTime:%@ difference:%f",timeDiff, [self getTimeStamp:_waitStartTime], [self getTimeStamp:_lastRecordTime], _lastRecordTime - _waitStartTime);
return;
}
NSLog(@"记录崩溃堆栈信息");
[self logStack];
_lastRecordTime = _waitStartTime;
}
}
#pragma mark - stack
- (void)printTraceLog {
}
- (void)logStack {
// 收集Crash信息也可用于实时获取各线程的调用堆栈
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);
}
#pragma mark - private
/// 返回一个子线程
- (NSThread *)secondaryThread {
static NSThread *_secondaryThread = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
_secondaryThread = [[NSThread alloc] initWithTarget:self
selector:@selector(networkRequestThreadEntryPoint:)
object:nil];
[_secondaryThread start];
});
return _secondaryThread;
}
- (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"MonitorThread"];
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
[runloop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
[runloop run];
}
}
- (NSString *)getCurTimeStamp {
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"YYYY-MM-dd HH:mm:ss:SSS"];
return [formatter stringFromDate:[NSDate date]];
}
- (NSString *)getTimeStamp:(double)time {
NSDate *date = [NSDate dateWithTimeIntervalSince1970:time];
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
[formatter setDateFormat:@"YYYY-MM-dd HH:mm:ss:SSS"];
return [formatter stringFromDate:date];
}
@end
二 核心代码解析
2.1 实现思路
首先在主线程注册了runloop observer
的回调mainRunLoopObserver
,每次当runloop
的状态发生变更的时候,该方法都会回调一次。
拿一个变量_waitStartTime
记录runloop
从休眠中唤醒时的时间,即当runloop
处于kCFRunLoopAfterWaiting
状态时,记录runloop
唤醒时的时间。当runloop
即将进入休眠时,即处于kCFRunLoopBeforeWaiting
状态时,将该变量置为零。
另外开一个子线程
并且开启runloop
(模仿AFNetworking
的方式),然后每隔0.5
秒去检测,即对比一下时间,如果发现当前时间与_waitStartTime
差距大于 2S,则可知道 runloop
处于kCFRunLoopAfterWaiting
跟kCFRunLoopBeforeWaiting
之间状态很长时间,则认为有卡顿情况,记录当前堆栈信息。
本文借助 PLCrashReporter来获取所有线程对线信息。
接下来对较难理解的代码进行详细讲解
2.2 _waitStartTime
变量讲解
case kCFRunLoopBeforeWaiting:{ // 即将进入休眠-这个时候处理 UI 操作
_waitStartTime = 0;
NSLog(@"kCFRunLoopBeforeWaiting - %@",time);
break;
}
//Inside the event processing loop after the run loop wakes up, but before processing the event that woke it up.
//This activity occurs only if the run loop did in fact go to sleep during the current loop
case kCFRunLoopAfterWaiting:{ // 从休眠中醒来开始做事情了
_waitStartTime = [[NSDate date] timeIntervalSince1970];
NSLog(@"kCFRunLoopAfterWaiting - %@",time);
break;
}
因为我们知道UI
的操作是在runloop
处于kCFRunLoopBeforeWaiting
之前,即处于kCFRunLoopBeforeTimers
或者kCFRunLoopBeforeSources
状态。所以当runloop
处于kCFRunLoopAfterWaiting
的时候,记录唤醒的时间,当处于kCFRunLoopBeforeWaiting
时,将时间清零。
运行结果如下
2019-06-29 15:57:26.962881+0800 MonitorTimer[98651:1479432] kCFRunLoopBeforeWaiting - 2019-06-29 15:57:26:963
2019-06-29 15:57:27.090253+0800 MonitorTimer[98651:1479561] timerFired return curTime:2019-06-29 15:57:27:090, waitStartTime:1970-01-01 08:00:00:000
...
2019-06-29 15:57:27.377416+0800 MonitorTimer[98651:1479432] kCFRunLoopBeforeWaiting - 2019-06-29 15:57:27:377
2019-06-29 15:57:27.591108+0800 MonitorTimer[98651:1479561] timerFired return curTime:2019-06-29 15:57:27:591, waitStartTime:1970-01-01 08:00:00:000
...
2019-06-29 15:57:39.524085+0800 MonitorTimer[98651:1479432] kCFRunLoopBeforeWaiting - 2019-06-29 15:57:39:524
2019-06-29 15:57:39.590825+0800 MonitorTimer[98651:1479561] timerFired return curTime:2019-06-29 15:57:39:590, waitStartTime:1970-01-01 08:00:00:000
...
2019-06-29 15:57:39.823567+0800 MonitorTimer[98651:1479432] kCFRunLoopBeforeWaiting - 2019-06-29 15:57:39:823
2019-06-29 15:57:40.093783+0800 MonitorTimer[98651:1479561] timerFired return curTime:2019-06-29 15:57:40:093, waitStartTime:1970-01-01 08:00:00:000
2.3 timerFired
方法讲解(也是最重要最难理解的方法)
- (void)timerFired {
if (_waitStartTime < 1) { // 因为刚刚经历了kCFRunLoopBeforeWaiting状态,_waitStartTime=0,直接 pass
NSLog(@"timerFired return curTime:%@, waitStartTime:%@",[self getCurTimeStamp], [self getTimeStamp:_waitStartTime]);
return;
}
double currentTime = [[NSDate date] timeIntervalSince1970];
double timeDiff = currentTime - _waitStartTime;
NSLog(@"timerFired curTime:%@, waitStartTime:%@, timeDiff:%f, _lastRecordTime:%f",[self getCurTimeStamp], [self getTimeStamp:_waitStartTime], timeDiff, _lastRecordTime);
// 如果 timeDiff 时间间隔超过 2S,表示 runloop 处于kCFRunLoopAfterWaiting跟kCFRunLoopBeforeWaiting之间状态很长时间
// 即长时间处于kCFRunLoopBeforeTimers和kCFRunLoopBeforeSources状态,就是进行 UI 操作了
if (timeDiff > 2.0) {
NSLog(@"last lastRecordTime:%f waitStartTime:%f",_lastRecordTime,_waitStartTime);
if (_lastRecordTime - _waitStartTime < 0.001 && _lastRecordTime != 0) { // 距离上一次记录堆栈信息时间过短的话,就直接 pass,避免短时间内多次记录堆栈信息
NSLog(@"last return timeDiff:%f waitStartTime:%@ lastRecordTime:%@ difference:%f",timeDiff, [self getTimeStamp:_waitStartTime], [self getTimeStamp:_lastRecordTime], _lastRecordTime - _waitStartTime);
return;
}
NSLog(@"记录崩溃堆栈信息");
[self logStack];
_lastRecordTime = _waitStartTime;
}
}
2.3.1 _waitStartTime < 1
直接return
if (_waitStartTime < 1) { // 因为刚刚经历了kCFRunLoopBeforeWaiting状态,_waitStartTime=0,直接 pass
NSLog(@"timerFired return curTime:%@, waitStartTime:%@",[self getCurTimeStamp], [self getTimeStamp:_waitStartTime]);
return;
}
前面说了,当runloop
处于kCFRunLoopAfterWaiting
时,记录当前唤醒的时间,当处于kCFRunLoopBeforeWaiting
时,将_waitStartTime
清零。也就是说当runloop
处于kCFRunLoopAfterWaiting
和kCFRunLoopBeforeWaiting
之间时,直接 pass,不需要做事情。
运行结果如下
2019-06-29 15:57:26.962881+0800 MonitorTimer[98651:1479432] kCFRunLoopBeforeWaiting - 2019-06-29 15:57:26:963
2019-06-29 15:57:27.090253+0800 MonitorTimer[98651:1479561] timerFired return curTime:2019-06-29 15:57:27:090, waitStartTime:1970-01-01 08:00:00:000
...
2019-06-29 15:57:27.377416+0800 MonitorTimer[98651:1479432] kCFRunLoopBeforeWaiting - 2019-06-29 15:57:27:377
2019-06-29 15:57:27.591108+0800 MonitorTimer[98651:1479561] timerFired return curTime:2019-06-29 15:57:27:591, waitStartTime:1970-01-01 08:00:00:000
...
2019-06-29 15:57:39.524085+0800 MonitorTimer[98651:1479432] kCFRunLoopBeforeWaiting - 2019-06-29 15:57:39:524
2019-06-29 15:57:39.590825+0800 MonitorTimer[98651:1479561] timerFired return curTime:2019-06-29 15:57:39:590, waitStartTime:1970-01-01 08:00:00:000
...
2019-06-29 15:57:39.823567+0800 MonitorTimer[98651:1479432] kCFRunLoopBeforeWaiting - 2019-06-29 15:57:39:823
2019-06-29 15:57:40.093783+0800 MonitorTimer[98651:1479561] timerFired return curTime:2019-06-29 15:57:40:093, waitStartTime:1970-01-01 08:00:00:000
2.3.2 如何判断出现卡顿
double currentTime = [[NSDate date] timeIntervalSince1970];
double timeDiff = currentTime - _waitStartTime;
if (timeDiff > 2.0) {
// 出现了卡顿
}
当runloop
处于kCFRunLoopAfterWaiting
时,记录当前唤醒的时间,当处于kCFRunLoopBeforeWaiting
时,将_waitStartTime
清零。如果 runloop
唤醒后,长时间处于kCFRunLoopAfterWaiting
之前的状态,即处于kCFRunLoopBeforeTimers
或者kCFRunLoopBeforeSources
状态,表示正在做很多事情,即出现了卡顿。导致_waitStartTime
的值一直不变,然后timeDiff
的值越来越大,当达到临界值2S
时,即可判断出现了卡顿现象。
运行结果如下
019-06-29 15:57:28.089722+0800 MonitorTimer[98651:1479561] timerFired curTime:2019-06-29 15:57:28:089, waitStartTime:2019-06-29 15:57:27:743, timeDiff:0.345903, _lastRecordTime:0.000000
2019-06-29 15:57:28.591188+0800 MonitorTimer[98651:1479561] timerFired curTime:2019-06-29 15:57:28:591, waitStartTime:2019-06-29 15:57:27:743, timeDiff:0.847366, _lastRecordTime:0.000000
2019-06-29 15:57:29.089330+0800 MonitorTimer[98651:1479561] timerFired curTime:2019-06-29 15:57:29:089, waitStartTime:2019-06-29 15:57:27:743, timeDiff:1.345805, _lastRecordTime:0.000000
2019-06-29 15:57:29.589632+0800 MonitorTimer[98651:1479561] timerFired curTime:2019-06-29 15:57:29:588, waitStartTime:2019-06-29 15:57:27:743, timeDiff:1.845115, _lastRecordTime:0.000000
2019-06-29 15:57:30.089099+0800 MonitorTimer[98651:1479561] timerFired curTime:2019-06-29 15:57:30:089, waitStartTime:2019-06-29 15:57:27:743, timeDiff:2.345361, _lastRecordTime:0.000000
2019-06-29 15:57:30.089665+0800 MonitorTimer[98651:1479561] last lastRecordTime:0.000000 waitStartTime:1561795047.743299
2019-06-29 15:57:30.089864+0800 MonitorTimer[98651:1479561] 记录崩溃堆栈信息
2.3.3 如何防止重复多次记录卡顿信息
if (_lastRecordTime - _waitStartTime < 0.001 && _lastRecordTime != 0) {
// 距离上一次记录堆栈信息时间过短的话,就直接 pass,避免短时间内多次记录堆栈信息
return;
}
NSLog(@"记录崩溃堆栈信息");
[self logStack];
_lastRecordTime = _waitStartTime;
因为如果runloop
长时间处于kCFRunLoopBeforeTimers
或者kCFRunLoopBeforeSources
状态,表示正在做很多事情,即出现了卡顿。然后_lastRecordTime
的值和_waitStartTime
值是相同的。所以这样就会直接return
,不会记录卡顿堆栈信息了。
只有当上一次记录堆栈信息时,_waitStartTime
刚好为 0,导致_lastRecordTime
也为零。或者_waitStartTime
的值为零,则满足记录卡顿对线信息的条件。
运行结果如下
...
2019-06-29 15:57:14.088766+0800 MonitorTimer[98651:1479561] last lastRecordTime:0.000000 waitStartTime:1561795031.917216
2019-06-29 15:57:14.089345+0800 MonitorTimer[98651:1479561] 记录崩溃堆栈信息
...
2019-06-29 15:57:24.600888+0800 MonitorTimer[98651:1479561] last lastRecordTime:1561795031.917216 waitStartTime:0.000000
2019-06-29 15:57:24.601743+0800 MonitorTimer[98651:1479561] 记录崩溃堆栈信息
...
2019-06-29 15:57:30.089665+0800 MonitorTimer[98651:1479561] last lastRecordTime:0.000000 waitStartTime:1561795047.743299
2019-06-29 15:57:30.089864+0800 MonitorTimer[98651:1479561] 记录崩溃堆栈信息
...
2.3.4 如何记录堆栈信息
- (void)logStack {
// 收集Crash信息也可用于实时获取各线程的调用堆栈
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);
}
借助 PLCrashReporter来获取所有线程对线信息。
运行结果如下
2019-06-29 15:57:14.089345+0800 MonitorTimer[98651:1479561] 记录崩溃堆栈信息
2019-06-29 15:57:15.066679+0800 MonitorTimer[98651:1479561] ---------卡顿信息
Incident Identifier: 0A37B895-4801-44EB-831D-3DF6E5D9E675
CrashReporter Key: TODO
Hardware Model: x86_64
Process: MonitorTimer [98651]
Path: /Users/cs/Library/Developer/CoreSimulator/Devices/2BAC277B-4BE9-4769-B3E0-12B8177803F9/data/Containers/Bundle/Application/B7DA98D5-D8DF-4006-88C3-2114BC6D0652/MonitorTimer.app/MonitorTimer
Identifier: cs.MonitorTimer
Version: 1.0 (1)
Code Type: X86-64
Parent Process: debugserver [98652]
Date/Time: 2019-06-29 07:57:14 +0000
OS Version: Mac OS X 12.2 (18F132)
Report Version: 104
Exception Type: SIGTRAP
Exception Codes: TRAP_TRACE at 0x10b478ccf
Crashed Thread: 9
Thread 0:
0 libsystem_kernel.dylib 0x000000010e3e822a mach_msg_trap + 10
1 CoreFoundation 0x000000010c752684 __CFRunLoopServiceMachPort + 212
2 CoreFoundation 0x000000010c74ccc9 __CFRunLoopRun + 1657
3 CoreFoundation 0x000000010c74c302 CFRunLoopRun
不过貌似没有记录到有用的堆栈信息,之前的文章是有记录到的 iOS实时卡顿检测-RunLoop(附实例),具体原因,要再研究一下,有结果后会再次更新文章。
2.3.5 测试代码
#import "ViewController.h"
#import "Monitor.h"
@interface ViewController () {
UITableView *_tableView;
}
@end
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self drawTableView];
// 开始卡顿监听
[[Monitor shareInstance] startMonitor];
}
/// 绘制TableView视图
- (void)drawTableView {
_tableView = [[UITableView alloc] initWithFrame:self.view.bounds];
_tableView.dataSource = self;
[self.view addSubview:_tableView];
}
#pragma mark - UITableViewDataSource
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
return 100;
}
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"cell"];
if (!cell) {
cell = [[UITableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"cell"];
}
NSString *text = nil;
if (indexPath.row % 10 == 0) { // 每10行休眠0.2S
usleep(500 * 1000); // 1 * 1000 * 1000 == 1秒
text = @"我在做复杂的事情,需要一些时间";
} else {
text = [NSString stringWithFormat:@"cell - %ld",indexPath.row];
}
cell.textLabel.text = text;
return cell;
}
本文参考 简单监测iOS卡顿的demo,非常感谢该作者。
相关文章参考
- iOS实时卡顿检测-RunLoop(附实例)
- iOS实时卡顿监控
- 简单监测iOS卡顿的demo
- RunLoop总结:RunLoop的应用场景(四)App卡顿监测
- 质量监控-卡顿检测
- 关于dispatch_semaphore的使用
项目链接地址 - MonitorTimer