本文首發於個人我的博客html
關於性能優化,我以前寫過iOS性能優化,通過優化以後,咱們的APP,冷啓動,從2.7秒優化到了0.6秒。git
關RunLoop,寫過RunLoop詳解之源碼分析,以及詳解RunLoop與多線程 ,那麼使用RunLoop如何來監控性能卡頓呢。 經過iOS性能優化 咱們知道,簡單來講App卡頓,就是FPS達不到60幀率,丟幀現象,就會卡頓。可是不少時候,咱們只知道丟幀了。具體爲何丟幀,卻不是很清楚,那麼咱們要怎麼監控呢,首先咱們要明白,要找出卡頓,就是要找出主線程作了什麼,而線程消息,是依賴RunLoop的,因此咱們可使用RunLoop來監控。github
RunLoop是用來監聽輸入源,進行調度處理的。若是RunLoop的線程進入睡眠前方法的執行時間過長而致使沒法進入睡眠,或者線程喚醒後接收消息時間過長而沒法進入下一步,就能夠認爲是線程受阻了。若是這個線程是主線程的話,表現出來的就是出現了卡頓。數據庫
咱們可使用CFRunLoopObserverRef來監控NSRunLoop的狀態,經過它能夠實時得到這些狀態值的變化。swift
關於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 的使用流程bash
CFRunLoopObserverContext context = {0, (__bridge void *)self, NULL, NULL};
複製代碼
第一個參數:用於分配observer對象的內存
第二個參數:用以設置observer所要關注的事件
第三個參數:用於標識該observer是在第一次進入runloop時執行仍是每次進入runloop處理時均執行
第四個參數:用於設置該observer的優先級
第五個參數:用於設置該observer的回調函數
第六個參數:用於設置該observer的運行環境
// 建立Runloop observer對象
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
複製代碼
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
複製代碼
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
複製代碼
CFRelease(_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),通知執行下一個任務......如此一來,經過信號量,就達到了併發隊列中的任務同步執行的要求。併發
一次有效卡頓採用了「N次卡頓超過閾值T」的斷定策略,即一個時間段內卡頓的次數累計大於N時才觸發採集和上報:舉例,卡頓閾值T=500ms、卡頓次數N=1,能夠斷定爲單次耗時較長的一次有效卡頓;而卡頓閾值T=50ms、卡頓次數N=5,能夠斷定爲頻次較快的一次有效卡頓
// minimum
static 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.jianshu.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放在了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 + 8
libsystem_c.dylib 0x1b524156c nanosleep + 212
libsystem_c.dylib 0x1b5241444 usleep + 64
YZMonitorRunLoopDemo 0x1022c18dc -[ViewController touchesBegan:withEvent:] + 76
UIKitCore 0x1e1f4fcdc <redacted> + 336
UIKitCore 0x1e1f4fb78 <redacted> + 60
UIKitCore 0x1e1f5e0f8 <redacted> + 1584
UIKitCore 0x1e1f5f52c <redacted> + 3140
UIKitCore 0x1e1f3f59c <redacted> + 340
UIKitCore 0x1e2005714 <redacted> + 1768
UIKitCore 0x1e2007e40 <redacted> + 4828
UIKitCore 0x1e2001070 <redacted> + 152
CoreFoundation 0x1b56bf018 <redacted> + 24
CoreFoundation 0x1b56bef98 <redacted> + 88
CoreFoundation 0x1b56be880 <redacted> + 176
CoreFoundation 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具體使用起來也很簡單,
// Unzipping
NSString *zipPath = @"path_to_your_zip_file";
NSString *destinationPath = @"path_to_the_folder_where_you_want_it_unzipped";
[SSZipArchive unzipFileAtPath:zipPath toDestination:destinationPath];
// Zipping
NSString *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地址
參考資料 :
更多資料,歡迎關注我的公衆號,不定時分享各類技術文章。