iOS使用RunLoop监控线上卡慢
原文地址
通过iOS性能优化 我们知道,简单来说App卡慢,就是FPS达不到60帧率,丢帧现象,就会卡慢。但是很多时候,我们只知道丢帧了。具体为什么丢帧,却不是很清楚,那么我们要怎样监控呢,首先我们要明白,要找出卡慢,就是要找出主线程做了什么,而线程消息,是依赖RunLoop的,所以我们可以使用RunLoop来监控。
RunLoop是用来监听输入源,进行调度解决的。假如RunLoop的线程进入睡眠前方法的执行时间过长而导致无法进入睡眠,或者者线程唤醒后接收消息时间过长而无法进入下一步,即可以认为是线程受阻了。假如这个线程是主线程的话,体现出来的就是出现了卡慢。
RunLoop和信号量
我们可以使用CFRunLoopObserverRef来监控NSRunLoop的状态,通过它可以实时取得这些状态值的变化。
runloop
关于runloop,可以参照 RunLoop详解之源码分析 这篇文章详细理解。这里简单总结一下:
runloop的状态
/* Run Loop Observer Activities */typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) { kCFRunLoopEntry = (1UL << 0), // 即将进入Loop kCFRunLoopBeforeTimers = (1UL << 1), //即将解决Timer kCFRunLoopBeforeSources = (1UL << 2), //即将解决Source kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠 kCFRunLoopAfterWaiting = (1UL << 6), //刚从休眠中唤醒 kCFRunLoopExit = (1UL << 7), //即将退出Loop kCFRunLoopAllActivities = 0x0FFFFFFFU //所有状态改变};CFRunLoopObserverRef 的使用流程
设置Runloop observer的运行环境
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};2. 创立Runloop observer对象第一个参数:用于分配observer对象的内存第二个参数:用以设置observer所要关注的事件第三个参数:用于标识该observer是在第一次进入runloop时执行还是每次进入runloop解决时均执行第四个参数:用于设置该observer的优先级第五个参数:用于设置该observer的回调函数第六个参数:用于设置该observer的运行环境 // 创立Runloop observer对象 _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context);3. 将新建的observer加入到当前thread的runloopCFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);4. 将observer从当前thread的runloop中移除CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);5. 释放 observerCFRelease(_observer); _observer = NULL;信号量
关于信号量,可以详细参考 GCD信号量-dispatch_semaphore_t
简单来说,主要有三个函数
dispatch_semaphore_create(long value); // 创立信号量dispatch_semaphore_signal(dispatch_semaphore_t deem); // 发送信号量dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); // 等待信号量dispatch_semaphore_create(long value);和GCD的group等用法一致,这个函数是创立一个dispatch_semaphore_类型的信号量,并且创立的时候需要指定信号量的大小。
dispatch_semaphore_wait(dispatch_semaphore_t dsema, dispatch_time_t timeout); 等待信号量。假如信号量值为0,那么该函数就会一直等待,也就是不返回(相当于阻塞当前线程),直到该函数等待的信号量的值大于等于1,该函数会对信号量的值进行减1操作,而后返回。
dispatch_semaphore_signal(dispatch_semaphore_t deem); 发送信号量。该函数会对信号量的值进行加1操作。
通常等待信号量和发送信号量的函数是成对出现的。并发执行任务时候,在当前任务执行之前,用dispatch_semaphore_wait函数进行等待(阻塞),直到上一个任务执行完毕后且通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),dispatch_semaphore_wait函数收到信号量之后判断信号量的值大于等于1,会再对信号量的值减1,而后当前任务可以执行,执行完毕当前任务后,再通过dispatch_semaphore_signal函数发送信号量(使信号量的值加1),通知执行下一个任务……如此一来,通过信号量,就达到了并发队列中的任务同步执行的要求。
监控卡慢
原理: 利用观察Runloop各种状态变化的持续时间来检测计算能否发生卡慢
一次有效卡慢采用了“N次卡慢超过阈值T”的判定策略,即一个时间段内卡慢的次数累计大于N时才触发采集和上报:举例,卡慢阈值T=500ms、卡慢次数N=1,可以判定为单次耗时较长的一次有效卡慢;而卡慢阈值T=50ms、卡慢次数N=5,可以判定为频次较快的一次有效卡慢
一个开发者,有一个学习的氛围跟一个交流圈子特别重要,这是一个我的iOS交流群:1012951431, 分享BAT,阿里面试题、面试经验,探讨技术, 大家一起交流学习成长!希望帮助开发者少走弯路。
主要代码
// minimumstatic const NSInteger MXRMonitorRunloopMinOneStandstillMillisecond = 20;static const NSInteger MXRMonitorRunloopMinStandstillCount = 1;// default// 超过多少毫秒为一次卡慢static const NSInteger MXRMonitorRunloopOneStandstillMillisecond = 50;// 多少次卡慢纪录为一次有效卡慢static const NSInteger MXRMonitorRunloopStandstillCount = 1;@interface YZMonitorRunloop(){ CFRunLoopObserverRef _observer; // 观察者 dispatch_semaphore_t _semaphore; // 信号量 CFRunLoopActivity _activity; // 状态}@property (nonatomic, assign) BOOL isCancel; //f能否取消检测@property (nonatomic, assign) NSInteger countTime; // 耗时次数@property (nonatomic, strong) NSMutableArray *backtrace;-(void)registerObserver{// 1. 设置Runloop observer的运行环境 CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL}; // 2. 创立Runloop observer对象 // 第一个参数:用于分配observer对象的内存// 第二个参数:用以设置observer所要关注的事件// 第三个参数:用于标识该observer是在第一次进入runloop时执行还是每次进入runloop解决时均执行// 第四个参数:用于设置该observer的优先级// 第五个参数:用于设置该observer的回调函数// 第六个参数:用于设置该observer的运行环境 _observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &runLoopObserverCallBack, &context); // 3. 将新建的observer加入到当前thread的runloop CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes); // 创立信号 dispatchSemaphore的知识参考:https://www.songma.com/p/24ffa819379c _semaphore = dispatch_semaphore_create(0); ////Dispatch Semaphore保证同步 __weak __typeof(self) weakSelf = self; // dispatch_queue_t queue = dispatch_queue_create("kadun", NULL); // 在子线程监控时长 dispatch_async(dispatch_get_global_queue(0, 0), ^{ // dispatch_async(queue, ^{ __strong __typeof(weakSelf) strongSelf = weakSelf; if (!strongSelf) { return; } while (YES) { if (strongSelf.isCancel) { return; } // N次卡慢超过阈值T记录为一次卡慢 // 等待信号量:假如信号量是0,则阻塞当前线程;假如信号量大于0,则此函数会把信号量-1,继续执行线程。此处超时时间设为limitMillisecond 毫秒。 // 返回值:假如线程是唤醒的,则返回非0,否则返回0 long semaphoreWait = dispatch_semaphore_wait(self->_semaphore, dispatch_time(DISPATCH_TIME_NOW, strongSelf.limitMillisecond * NSEC_PER_MSEC)); if (semaphoreWait != 0) { // 假如 RunLoop 的线程,进入睡眠前方法的执行时间过长而导致无法进入睡眠(kCFRunLoopBeforeSources),或者者线程唤醒后接收消息时间过长(kCFRunLoopAfterWaiting)而无法进入下一步的话,即可以认为是线程受阻。 //两个runloop的状态,BeforeSources和AfterWaiting这两个状态区间时间能够监测到能否卡慢 if (self->_activity == kCFRunLoopBeforeSources || self->_activity == kCFRunLoopAfterWaiting) { if (++strongSelf.countTime < strongSelf.standstillCount){ NSLog(@"%ld",strongSelf.countTime); continue; } [strongSelf logStack]; [strongSelf printLogTrace]; NSString *backtrace = [YZCallStack yz_backtraceOfMainThread]; NSLog(@"++++%@",backtrace); [[YZLogFile sharedInstance] writefile:backtrace]; if (strongSelf.callbackWhenStandStill) { strongSelf.callbackWhenStandStill(); } } } strongSelf.countTime = 0; } });}demo测试
我把demo放在了github demo地址
使用时候,只要要
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { // Override point for customization after application launch. [[YZMonitorRunloop sharedInstance] startMonitor]; [YZMonitorRunloop sharedInstance].callbackWhenStandStill = ^{ NSLog(@"eagle.检测到卡慢了"); }; return YES;}控制器中,每次点击屏幕,休眠1秒钟,如下
#import "ViewController.h"@interface ViewController ()@end@implementation ViewController- (void)viewDidLoad { [super viewDidLoad]; }- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{ usleep(1 * 1000 * 1000); // 1秒 }@end点击屏幕之后,打印如下
YZMonitorRunLoopDemo[10288:1915706] ==========检测到卡慢之后调用堆栈========== ( "0 YZMonitorRunLoopDemo 0x00000001022c653c -[YZMonitorRunloop logStack] + 96", "1 YZMonitorRunLoopDemo 0x00000001022c62a0 __36-[YZMonitorRunloop registerObserver]_block_invoke + 484", "2 libdispatch.dylib 0x00000001026ab6f0 _dispatch_call_block_and_release + 24", "3 libdispatch.dylib 0x00000001026acc74 _dispatch_client_callout + 16", "4 libdispatch.dylib 0x00000001026afad4 _dispatch_queue_override_invoke + 876", "5 libdispatch.dylib 0x00000001026bddc8 _dispatch_root_queue_drain + 372", "6 libdispatch.dylib 0x00000001026be7ac _dispatch_worker_thread2 + 156", "7 libsystem_pthread.dylib 0x00000001b534d1b4 _pthread_wqthread + 464", "8 libsystem_pthread.dylib 0x00000001b534fcd4 start_wqthread + 4") libsystem_kernel.dylib 0x1b52ca400 __semwait_signal + 8libsystem_c.dylib 0x1b524156c nanosleep + 212libsystem_c.dylib 0x1b5241444 usleep + 64YZMonitorRunLoopDemo 0x1022c18dc -[ViewController touchesBegan:withEvent:] + 76UIKitCore 0x1e1f4fcdc <redacted> + 336UIKitCore 0x1e1f4fb78 <redacted> + 60UIKitCore 0x1e1f5e0f8 <redacted> + 1584UIKitCore 0x1e1f5f52c <redacted> + 3140UIKitCore 0x1e1f3f59c <redacted> + 340UIKitCore 0x1e2005714 <redacted> + 1768UIKitCore 0x1e2007e40 <redacted> + 4828UIKitCore 0x1e2001070 <redacted> + 152CoreFoundation 0x1b56bf018 <redacted> + 24CoreFoundation 0x1b56bef98 <redacted> + 88CoreFoundation 0x1b56be880 <redacted> + 176CoreFoundation 0x1b56b97就可定位到卡慢位置
-[ViewController touchesBegan:withEvent:]
卡慢日志写入本地
上面已经监控到了卡慢,和调用堆栈。假如是debug模式下,可以直接看日志,假如想在线上查看的话,可以写入本地,而后上传到服务器
写入本地数据库
创立本地路径
-(NSString *)getLogPath{ NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory,NSUserDomainMask,YES); NSString *homePath = [paths objectAtIndex:0]; NSString *filePath = [homePath stringByAppendingPathComponent:@"Caton.log"]; return filePath;}假如是第一次写入,带上设施信息,手机型号等信息
NSString *filePath = [self getLogPath]; NSFileManager *fileManager = [NSFileManager defaultManager]; if(![fileManager fileExistsAtPath:filePath]) //假如不存在 { NSString *str = @"卡慢日志"; NSString *systemVersion = [NSString stringWithFormat:@"手机版本: %@",[YZAppInfoUtil iphoneSystemVersion]]; NSString *iphoneType = [NSString stringWithFormat:@"手机型号: %@",[YZAppInfoUtil iphoneType]]; str = [NSString stringWithFormat:@"%@\n%@\n%@",str,systemVersion,iphoneType]; [str writeToFile:filePath atomically:YES encoding:NSUTF8StringEncoding error:nil]; }假如本地文件已经存在,就先判断大小能否过大,决定能否直接写入,还是先上传到服务器
float filesize = -1.0; if ([fileManager fileExistsAtPath:filePath]) { NSDictionary *fileDic = [fileManager attributesOfItemAtPath:filePath error:nil]; unsigned long long size = [[fileDic objectForKey:NSFileSize] longLongValue]; filesize = 1.0 * size / 1024; } NSLog(@"文件大小 filesize = %lf",filesize); NSLog(@"文件内容 %@",string); NSLog(@" ---------------------------------"); if (filesize > (self.MAXFileLength > 0 ? self.MAXFileLength:DefaultMAXLogFileLength)) { // 上传到服务器 NSLog(@" 上传到服务器"); [self update]; [self clearLocalLogFile]; [self writeToLocalLogFilePath:filePath contentStr:string]; }else{ NSLog(@"继续写入本地"); [self writeToLocalLogFilePath:filePath contentStr:string]; }压缩日志,上传服务器
由于都是文本数据,所以我们可以压缩之后,打打降低占用空间,而后进行上传,上传成功之后,删除本地,而后继续写入,等待下次写日志
压缩工具
使用 SSZipArchive具体使用起来也很简单,
// UnzippingNSString *zipPath = @"path_to_your_zip_file";NSString *destinationPath = @"path_to_the_folder_where_you_want_it_unzipped";[SSZipArchive unzipFileAtPath:zipPath toDestination:destinationPath];// ZippingNSString *zippedPath = @"path_where_you_want_the_file_created";NSArray *inputPaths = [NSArray arrayWithObjects: [[NSBundle mainBundle] pathForResource:@"photo1" ofType:@"jpg"], [[NSBundle mainBundle] pathForResource:@"photo2" ofType:@"jpg"] nil];[SSZipArchive createZipFileAtPath:zippedPath withFilesAtPaths:inputPaths];代码中
NSString *zipPath = [self getLogZipPath]; NSString *password = nil; NSMutableArray *filePaths = [[NSMutableArray alloc] init]; [filePaths addObject:[self getLogPath]]; BOOL success = [SSZipArchive createZipFileAtPath:zipPath withFilesAtPaths:filePaths withPassword:password.length > 0 ? password : nil]; if (success) { NSLog(@"压缩成功"); }else{ NSLog(@"压缩失败"); }具体假如上传到服务器,使用者可以用AFN等将本地的 zip文件上传到文件服务器就可,就不赘述了。
至此,我们做到了,用runloop,监控卡慢,写入日志,而后压缩上传服务器,删除本地的过程。
详细代码见demo地址
另外,假如你想一起进阶,不妨增加一下交流群1012951431,选择加入一起交流,一起学习。期待你的加入!


1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » iOS使用RunLoop监控线上卡慢