现在对于app的要求越来越高,并不是像以前我们只要把功能需求做出来就行。还要求我们对app做各种优化,写出高质量的代码。所以对于界面的流畅度的监测就非常有必要了。那应该如何监测呢?
FPS的监测
- 原理:CADisplayLink是一个特殊的定时器,它的调用执行和GPU的渲染有关,我们常常说的60刷和120hz刷屏。也就以为这,默认情况下CADisplayLink每秒会调用60次或者120次。我们就可以计算它在一秒内调用了多少次,来看界面的流畅度。正常情况下50<fps 肉眼看不出来卡顿,55以上就很流畅,说明你已经做的很好了。下面我们来看代码,具体是如何实现的。
//初始化创建
CADisplayLink *link=[CADisplayLink displayLinkWithTarget:self selector:@selector(displayAction:)];
//添加到主线程中去
[link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
//通记录回调次数和相隔时间 计算fps
-(void)displayAction:(CADisplayLink *)link{
//记录上次的时间
static NSTimeInterval lastTime = 0;
//记录回调了多少次
static NSInteger frameCount = 0;
if (lastTime==0) {
lastTime=link.timestamp;
return;
}
frameCount++;
NSTimeInterval duration=link.timestamp-lastTime;
//刷新间隔1秒
if (duration>=1) {
NSInteger fps=frameCount/duration;
frameCount=0;
lastTime=link.timestamp;
}
}
- 优缺点:通过上面的代码,可以看到界面帧率。越小说明界面越不流畅。但它的作用也仅仅只是这样了。即使知道了卡顿,我们还要自己去查找,无法快速的定位到底是那个方法引起的卡顿。
RunLoop监听应用程序卡顿
在前面我们讲过runloop那个非常烂大街的流程图,这里就不放了。
KCFRunLoopEntyr开始runloop-->timer--> kCFRunLoopBeforeSources(开始处理事件)-->kCFRunLoopBeforeWaiting(事件处理完了,开始休眠)--> kCFRunLoopAfterWaiting(被叫醒)-->然后开始回到最开的步骤 重新实现流程
原理:创建一个子线程进行循环监测,每次检测时设置为YES,然后派发任务切换到主线程,将标记位置设置为NO,如果没有设置成NO,说明主线程卡顿了。关键代码如下:
#import <Foundation/Foundation.h>
#define SHAREDMONITOR [LXDAppFluecyMonitor sharedMonitor]
/*!
* @brief 监听UI线程卡顿
*/
@interface LXDAppFluecyMonitor : NSObject
+ (instancetype)sharedMonitor;
- (void)startMonitoring;
- (void)stopMonitoring;
@end
#import "LXDAppFluecyMonitor.h"
#define LXD_DEPRECATED_POLLUTE_MAIN_QUEUE
@interface LXDAppFluecyMonitor ()
@property (nonatomic, assign) int timeOut;
@property (nonatomic, assign) BOOL isMonitoring;
@property (nonatomic, assign) CFRunLoopObserverRef observer;
@property (nonatomic, assign) CFRunLoopActivity currentActivity;
@property (nonatomic, strong) dispatch_semaphore_t semphore;
@property (nonatomic, strong) dispatch_semaphore_t eventSemphore;
@end
#define LXD_SEMPHORE_SUCCESS 0
static NSTimeInterval lxd_restore_interval = 5;
static NSTimeInterval lxd_time_out_interval = 1;
static int64_t lxd_wait_interval = 200 * NSEC_PER_MSEC;
/*!
* @brief 监听runloop状态在after waiting和before sources之间
*/
static inline dispatch_queue_t lxd_fluecy_monitor_queue() {
static dispatch_queue_t lxd_fluecy_monitor_queue;
static dispatch_once_t once;
dispatch_once(&once, ^{
lxd_fluecy_monitor_queue = dispatch_queue_create("com.sindrilin.lxd_monitor_queue", NULL);
});
return lxd_fluecy_monitor_queue;
}
#define LOG_RUNLOOP_ACTIVITY 0
static void lxdRunLoopObserverCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void * info) {
SHAREDMONITOR.currentActivity = activity;
dispatch_semaphore_signal(SHAREDMONITOR.semphore);
#if LOG_RUNLOOP_ACTIVITY
switch (activity) {
case kCFRunLoopEntry:
NSLog(@"runloop entry");
break;
case kCFRunLoopExit:
NSLog(@"runloop exit");
break;
case kCFRunLoopAfterWaiting:
NSLog(@"runloop after waiting");
break;
case kCFRunLoopBeforeTimers:
NSLog(@"runloop before timers");
break;
case kCFRunLoopBeforeSources:
NSLog(@"runloop before sources");
break;
case kCFRunLoopBeforeWaiting:
NSLog(@"runloop before waiting");
break;
default:
break;
}
#endif
};
@implementation LXDAppFluecyMonitor
#pragma mark - Singleton override
+ (instancetype)sharedMonitor {
static LXDAppFluecyMonitor * sharedMonitor;
static dispatch_once_t once;
dispatch_once(&once, ^{
sharedMonitor = [[super allocWithZone: NSDefaultMallocZone()] init];
[sharedMonitor commonInit];
});
return sharedMonitor;
}
+ (instancetype)allocWithZone: (struct _NSZone *)zone {
return [self sharedMonitor];
}
- (void)dealloc {
[self stopMonitoring];
}
- (void)commonInit {
self.semphore = dispatch_semaphore_create(0);
}
#pragma mark - Public
- (void)startMonitoring {
if (_isMonitoring) { return; }
_isMonitoring = YES;
CFRunLoopObserverContext context = {
0,
(__bridge void *)self,
NULL,
NULL
};
//创建监听者
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &lxdRunLoopObserverCallback, &context);
//监听主RunLoop
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
dispatch_async(lxd_event_monitor_queue(), ^{
NSLog(@"%@",[NSThread currentThread]);
while (SHAREDMONITOR.isMonitoring) {
//主线程的RunLoop 即将进入休眠
if (SHAREDMONITOR.currentActivity == kCFRunLoopBeforeWaiting) {
//默认超时
__block BOOL timeOut = YES;
NSLog(@"0");
dispatch_async(dispatch_get_main_queue(), ^{
//切换到主线程 执行任务
//若主线程没有出现卡顿 能正常执行任务 将timeOut设置为NO
//若主线程出现卡顿 不能能正常执行任务
timeOut = NO;
//发送信号量 +1
dispatch_semaphore_signal(SHAREDMONITOR.eventSemphore);
NSLog(@"1");
});
NSLog(@"2");
//当前子线程休眠1秒钟
[NSThread sleepForTimeInterval: lxd_time_out_interval];
NSLog(@"3");
//超时打印函数调用栈
if (timeOut) {
NSLog(@"4");
[LXDBacktraceLogger lxd_logMain];
}
NSLog(@"5");
//释放信号量 -1 此时的信号量为-1<0 下面的逻辑不会执行 循环依然执行
dispatch_wait(SHAREDMONITOR.eventSemphore, DISPATCH_TIME_FOREVER);
NSLog(@"6");
}
}
});
}
- (void)stopMonitoring {
if (!_isMonitoring) { return; }
_isMonitoring = NO;
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
CFRelease(_observer);
_observer = nil;
}
@end
上面一堆的代码,主要实现的逻辑方法在startMonitoring中
- 1、首先判断_isMonitoring,当_isMonitoring为YES时,说明主线程一直处于卡顿状态。
- 2、_isMonitoring为NO时,表示已派发任务到主线程。这时我们把_isMonitoring设置为YES。
-3、 创建监听这,监听主runloop。 - 4、创建子线程lxd_fluecy_monitor_queue。while判断_isMonitoring,为0,表示在指定时间内接收到主线程发出的信号;非0,表示在指定时间内没有接收到主线程发出的信号,主线程可能在执行耗时任务,有可能造成应用程序的卡顿
-5、判断主runloop即将进入休眠,这时候切换到主线程 执行任务。同时我们让当前的子线程休眠一秒。这里为什么要休眠呢?这时因为,切换到主线程 设置timeout为NO,如果在我们设置的时间内能切换成功,就判断他不卡顿。如若1秒以后timeout还是yes,证明主线程在执行耗时操作。没有办法去执行 timeOut = NO;。这个时候我们就要记录卡顿的日志[LXDBacktraceLogger lxd_logMain];
-6.最后 信号量也没办法释放,他会一直等待主线程切换执行,发出信号成功以后才能释放。
使用Instrument工具实时监测App
使用Time Profiler
修改项目为debug模式,然后如下图设置
![](https://img.haomeiwen.com/i9788931/f107f918e8d2a8ad.png)
按步骤打开product-->profile-->time
![](https://img.haomeiwen.com/i9788931/184cf34e44a80653.png)
设置call tree,显示代码位置。
以上就完成了界面卡顿监测。
网友评论