前言
本文的demo代碼也會更新到github上。
做這個demo思路來源于微信team的:微信iOS卡頓監(jiān)控系統(tǒng)。
主要思路:通過監(jiān)測Runloop的kCFRunLoopAfterWaiting懦尝,用一個子線程去檢查婶肩,一次循環(huán)是否時(shí)間太長律歼。
其中主要涉及到了runloop的原理。關(guān)于整個原理:深入理解RunLoop講解的比較仔細(xì)制圈。
以下就是runloop大概的運(yùn)行方式:
/// 1. 通知Observers鲸鹦,即將進(jìn)入RunLoop
/// 此處有Observer會創(chuàng)建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {
/// 2. 通知 Observers: 即將觸發(fā) Timer 回調(diào)跷跪。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即將觸發(fā) Source (非基于port的,Source0) 回調(diào)吵瞻。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 4. 觸發(fā) Source0 (非基于port的) 回調(diào)橡羞。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
/// 5. GCD處理main block
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);
/// 6. 通知Observers,即將進(jìn)入休眠
/// 此處有Observer釋放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);
/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();
/// 8. 通知Observers庵朝,線程被喚醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);
/// 9. 如果是被Timer喚醒的九府,回調(diào)Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);
/// 9. 如果是被dispatch喚醒的侄旬,執(zhí)行所有調(diào)用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);
/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件喚醒了儡羔,處理這個事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
} while (...);
/// 10. 通知Observers,即將退出RunLoop
/// 此處有Observer釋放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}
其中UI主要集中在__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
和__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);
之前仇冯。
獲取kCFRunLoopBeforeSources
到kCFRunLoopBeforeWaiting
再到kCFRunLoopAfterWaiting
的狀態(tài)就可以知道是否有卡頓的情況苛坚。
NSTimer的實(shí)現(xiàn)
具體代碼如下:
//
// MonitorController.h
// RunloopMonitorDemo
//
// Created by game3108 on 16/4/13.
// Copyright ? 2016年 game3108. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface MonitorController : NSObject
+ (instancetype) sharedInstance;
- (void) startMonitor;
- (void) endMonitor;
- (void) printLogTrace;
@end
//
// MonitorController.m
// RunloopMonitorDemo
//
// Created by game3108 on 16/4/13.
// Copyright ? 2016年 game3108. All rights reserved.
//
#import "MonitorController.h"
#include <libkern/OSAtomic.h>
#include <execinfo.h>
@interface MonitorController(){
CFRunLoopObserverRef _observer;
double _lastRecordTime;
NSMutableArray *_backtrace;
}
@end
@implementation MonitorController
static double _waitStartTime;
+ (instancetype) sharedInstance{
static dispatch_once_t once;
static id sharedInstance;
dispatch_once(&once, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (void) startMonitor{
[self addMainThreadObserver];
[self addSecondaryThreadAndObserver];
}
- (void) endMonitor{
if (!_observer) {
return;
}
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
CFRelease(_observer);
_observer = NULL;
}
#pragma mark printLogTrace
- (void)printLogTrace{
NSLog(@"====================堆棧\n %@ \n",_backtrace);
}
#pragma mark addMainThreadObserver
- (void) addMainThreadObserver {
dispatch_async(dispatch_get_main_queue(), ^{
//建立自動釋放池
@autoreleasepool {
//獲得當(dāng)前thread的Run loop
NSRunLoop *myRunLoop = [NSRunLoop currentRunLoop];
//設(shè)置Run loop observer的運(yùn)行環(huán)境
CFRunLoopObserverContext context = {0, (__bridge void *)(self), NULL, NULL, NULL};
//創(chuàng)建Run loop observer對象
//第一個參數(shù)用于分配observer對象的內(nèi)存
//第二個參數(shù)用以設(shè)置observer所要關(guān)注的事件,詳見回調(diào)函數(shù)myRunLoopObserver中注釋
//第三個參數(shù)用于標(biāo)識該observer是在第一次進(jìn)入run loop時(shí)執(zhí)行還是每次進(jìn)入run loop處理時(shí)均執(zhí)行
//第四個參數(shù)用于設(shè)置該observer的優(yōu)先級
//第五個參數(shù)用于設(shè)置該observer的回調(diào)函數(shù)
//第六個參數(shù)用于設(shè)置該observer的運(yùn)行環(huán)境
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, &myRunLoopObserver, &context);
if (_observer) {
//將Cocoa的NSRunLoop類型轉(zhuǎn)換成Core Foundation的CFRunLoopRef類型
CFRunLoopRef cfRunLoop = [myRunLoop getCFRunLoop];
//將新建的observer加入到當(dāng)前thread的run loop
CFRunLoopAddObserver(cfRunLoop, _observer, kCFRunLoopDefaultMode);
}
}
});
}
void myRunLoopObserver(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
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(@"run loop entry");
break;
//Inside the event processing loop before any timers are processed
case kCFRunLoopBeforeTimers:
NSLog(@"run loop before timers");
break;
//Inside the event processing loop before any sources are processed
case kCFRunLoopBeforeSources:
NSLog(@"run loop before sources");
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:{
_waitStartTime = 0;
NSLog(@"run loop before waiting");
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(@"run loop after waiting");
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(@"run loop exit");
break;
/*
A combination of all the preceding stages
case kCFRunLoopAllActivities:
break;
*/
default:
break;
}
}
#pragma mark addSecondaryThreadAndObserver
- (void) addSecondaryThreadAndObserver{
NSThread *thread = [self secondaryThread];
[self performSelector:@selector(addSecondaryTimer) onThread:thread withObject:nil waitUntilDone:YES];
}
- (NSThread *)secondaryThread {
static NSThread *_secondaryThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_secondaryThread =
[[NSThread alloc] initWithTarget:self
selector:@selector(networkRequestThreadEntryPoint:)
object:nil];
[_secondaryThread start];
});
return _secondaryThread;
}
- (void)networkRequestThreadEntryPoint:(id)__unused object {
@autoreleasepool {
[[NSThread currentThread] setName:@"monitorControllerThread"];
NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
[runLoop run];
}
}
- (void) addSecondaryTimer{
NSTimer *myTimer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(timerFired:) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:myTimer forMode:NSDefaultRunLoopMode];
}
- (void)timerFired:(NSTimer *)timer{
if ( _waitStartTime < 1 ){
return;
}
double currentTime = [[NSDate date] timeIntervalSince1970];
double timeDiff = currentTime - _waitStartTime;
if (timeDiff > 2.0){
if (_lastRecordTime - _waitStartTime < 0.001 && _lastRecordTime != 0){
NSLog(@"last time no :%f %f",timeDiff, _waitStartTime);
return;
}
[self logStack];
_lastRecordTime = _waitStartTime;
}
}
- (void)logStack{
void* callstack[128];
int frames = backtrace(callstack, 128);
char **strs = backtrace_symbols(callstack, frames);
int i;
_backtrace = [NSMutableArray arrayWithCapacity:frames];
for ( i = 0 ; i < frames ; i++ ){
[_backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
}
@end
主要內(nèi)容是首先在主線程注冊了runloop observer的回調(diào)myRunLoopObserver
每次小循環(huán)都會記錄一下kCFRunLoopAfterWaiting
的時(shí)間_waitStartTime
涯贞,并且在kCFRunLoopBeforeWaiting
制空宋渔。
另外開了一個子線程并開啟他的runloop(模仿了AFNetworking的方式)皇拣,并加上一個timer每隔1秒去進(jìn)行監(jiān)測薄嫡。
如果當(dāng)前時(shí)長與_waitStartTime
差距大于2秒毫深,則認(rèn)為有卡頓情況,并記錄了當(dāng)前堆棧信息钉寝。
PS:整個demo寫的比較簡單闸迷,最后獲取堆棧也僅獲取了當(dāng)前線程的堆棧信息([NSThread callStackSymbols]
有同樣效果)腥沽,也在尋找獲取所有線程堆棧的方法,歡迎指點(diǎn)一下师溅。
更新:
了解到 plcrashreporter (github地址) 可以做到獲取所有線程堆棧墓臭。
更新2:
這篇文章也介紹了監(jiān)測卡頓的方法:檢測iOS的APP性能的一些方法
通過Dispatch Semaphore保證同步這里記錄一下。
寫一個Semaphore版本的代碼,也放在github上:
//
// SeMonitorController.h
// RunloopMonitorDemo
//
// Created by game3108 on 16/4/14.
// Copyright ? 2016年 game3108. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface SeMonitorController : NSObject
+ (instancetype) sharedInstance;
- (void) startMonitor;
- (void) endMonitor;
- (void) printLogTrace;
@end
//
// SeMonitorController.m
// RunloopMonitorDemo
//
// Created by game3108 on 16/4/14.
// Copyright ? 2016年 game3108. All rights reserved.
//
#import "SeMonitorController.h"
#import <libkern/OSAtomic.h>
#import <execinfo.h>
@interface SeMonitorController(){
CFRunLoopObserverRef _observer;
dispatch_semaphore_t _semaphore;
CFRunLoopActivity _activity;
NSInteger _countTime;
NSMutableArray *_backtrace;
}
@end
@implementation SeMonitorController
+ (instancetype) sharedInstance{
static dispatch_once_t once;
static id sharedInstance;
dispatch_once(&once, ^{
sharedInstance = [[self alloc] init];
});
return sharedInstance;
}
- (void) startMonitor{
[self registerObserver];
}
- (void) endMonitor{
if (!_observer) {
return;
}
CFRunLoopRemoveObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
CFRelease(_observer);
_observer = NULL;
}
- (void) printLogTrace{
NSLog(@"====================堆棧\n %@ \n",_backtrace);
}
static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
SeMonitorController *instrance = [SeMonitorController sharedInstance];
instrance->_activity = activity;
// 發(fā)送信號
dispatch_semaphore_t semaphore = instrance->_semaphore;
dispatch_semaphore_signal(semaphore);
}
- (void)registerObserver
{
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
_observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
// 創(chuàng)建信號
_semaphore = dispatch_semaphore_create(0);
// 在子線程監(jiān)控時(shí)長
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
// 假定連續(xù)5次超時(shí)50ms認(rèn)為卡頓(當(dāng)然也包含了單次超時(shí)250ms)
long st = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (st != 0)
{
if (_activity==kCFRunLoopBeforeSources || _activity==kCFRunLoopAfterWaiting)
{
if (++_countTime < 5)
continue;
[self logStack];
NSLog(@"something lag");
}
}
_countTime = 0;
}
});
}
- (void)logStack{
void* callstack[128];
int frames = backtrace(callstack, 128);
char **strs = backtrace_symbols(callstack, frames);
int i;
_backtrace = [NSMutableArray arrayWithCapacity:frames];
for ( i = 0 ; i < frames ; i++ ){
[_backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
}
free(strs);
}
@end
用Dispatch Semaphore簡化了代碼復(fù)雜度铸史,更加簡潔琳轿。
參考資料
本文csdn地址
1.微信iOS卡頓監(jiān)控系統(tǒng)
2. iphone——使用run loop對象
3.深入理解RunLoop
4.檢測iOS的APP性能的一些方法
5.iOS實(shí)時(shí)卡頓監(jiān)控