市面上的iOS卡頓分析方案有三種:監(jiān)控FPS、監(jiān)控RunLoop、ping主線程。
方案一:監(jiān)控FPS
一般來(lái)說(shuō),我們約定60FPS即為流暢。那么反過(guò)來(lái),如果App在運(yùn)行期間出現(xiàn)了掉幀,即可認(rèn)為出現(xiàn)了卡頓。
監(jiān)控FPS的方案幾乎都是基于CADisplayLink實(shí)現(xiàn)的。簡(jiǎn)單介紹一下CADisplayLink:CADisplayLink是一個(gè)和屏幕刷新率保持一致的定時(shí)器,一但 CADisplayLink 以特定的模式注冊(cè)到runloop之后,每當(dāng)屏幕需要刷新的時(shí)候,runloop就會(huì)調(diào)用CADisplayLink綁定的target上的selector。
可以通過(guò)向RunLoop中添加CADisplayLink,根據(jù)其回調(diào)來(lái)計(jì)算出當(dāng)前畫(huà)面的幀數(shù)。
#import "FPSMonitor.h"
#import <UIKit/UIKit.h>
@interface FPSMonitor ()
@property (nonatomic, strong) CADisplayLink* link;
@property (nonatomic, assign) NSInteger count;
@property (nonatomic, assign) NSTimeInterval lastTime;
@end
@implementation FPSMonitor
- (void)beginMonitor {
_link = [CADisplayLink displayLinkWithTarget:self selector:@selector(fpsInfoCaculate:)];
[_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)fpsInfoCaculate:(CADisplayLink *)sender {
if (_lastTime == 0) {
_lastTime = sender.timestamp;
return;
}
_count++;
double deltaTime = sender.timestamp - _lastTime;
if (deltaTime >= 1) {
NSInteger FPS = _count / deltaTime;
_lastTime = sender.timestamp;
_count = 0;
NSLog(@"FPS: %li", (NSInteger)ceill(FPS + 0.5));
}
}
@end
FPS的好處就是直觀,小手一劃后FPS下降了,說(shuō)明頁(yè)面的某處有性能問(wèn)題。壞處就是只知道這是頁(yè)面的某處,不能準(zhǔn)確定位到具體的堆棧。
方案二:監(jiān)控RunLoop
首先來(lái)介紹下什么是RunLoop。RunLoop是維護(hù)其內(nèi)部事件循環(huán)的一個(gè)對(duì)象,它在程序運(yùn)行過(guò)程中重復(fù)的做著一些事情,例如接收消息、處理消息、休眠等等。
所謂的事件循環(huán),就是對(duì)事件/消息進(jìn)行管理,沒(méi)有消息時(shí),休眠線程以避免資源消耗,從用戶(hù)態(tài)切換到內(nèi)核態(tài)。
有事件/消息需要進(jìn)行處理時(shí),立即喚醒線程,回到用戶(hù)態(tài)進(jìn)行處理。
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
int main(int argc, char * argv[]) {
NSString * appDelegateClassName;
@autoreleasepool {
appDelegateClassName = NSStringFromClass([AppDelegate class]);
}
return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}
UIApplicationMain函數(shù)內(nèi)部會(huì)啟動(dòng)主線程的RunLoop,使得iOS程序持續(xù)運(yùn)行。
iOS系統(tǒng)中有兩套API來(lái)使用RunLoop,NSRunLoop(CFRunLoopRef的封裝)和CFRunLoopRef。Foundation框架是不開(kāi)源的,可以通過(guò)開(kāi)源的CoreFoundation來(lái)分析RunLoop內(nèi)部實(shí)現(xiàn)。
RunLoop對(duì)象底層就是一個(gè)CFRunLoopRef結(jié)構(gòu)體,內(nèi)部數(shù)據(jù)如下:
struct __CFRunLoop {
pthread_t _pthread; // 與RunLoop一一對(duì)應(yīng)的線程
CFMutableSetRef _commonModes; // 存儲(chǔ)著NSString(mode名稱(chēng))的集合
CFMutableSetRef _commonModeItems; // 存儲(chǔ)著被標(biāo)記為commonMode的Source0/Source1/Timer/Observer
CFRunLoopModeRef _currentMode; // RunLoop當(dāng)前的運(yùn)行模式
CFMutableSetRef _modes; // 存儲(chǔ)著RunLoop所有的 Mode(CFRunLoopModeRef)模式
// 其他屬性略
};
struct __CFRunLoopMode {
CFStringRef _name; // mode 類(lèi)型,如:NSDefaultRunLoopMode
CFMutableSetRef _sources0; // 事件源 sources0
CFMutableSetRef _sources1; // 事件源 sources1
CFMutableArrayRef _observers; // 觀察者
CFMutableArrayRef _timers; // 定時(shí)器
// 其他屬性略
};
Source0被添加到RunLoop上時(shí)并不會(huì)主動(dòng)喚醒線程,需要手動(dòng)去喚醒。Source0負(fù)責(zé)對(duì)觸摸事件的處理以及performSeletor:onThread:。
Source1具備喚醒線程的能力,使用的是基于Port的線程間通信。Source1負(fù)責(zé)捕獲系統(tǒng)事件,并將事件交由Source0處理。
struct __CFRunLoopSource {
CFRuntimeBase _base;
uint32_t _bits;
pthread_mutex_t _lock;
CFIndex _order; /* immutable */
CFMutableBagRef _runLoops;
union {
CFRunLoopSourceContext version0; // 表示 sources0
CFRunLoopSourceContext1 version1; // 表示 sources1
} _context;
};
__CFRunLoopTimer和NSTimer是免費(fèi)橋接toll-free bridged的。
performSelector:WithObject:afterDelay:方法會(huì)創(chuàng)建timer并添加到RunLoop中。
struct __CFRunLoopTimer {
CFRuntimeBase _base;
uint16_t _bits;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFMutableSetRef _rlModes;
CFAbsoluteTime _nextFireDate;
CFTimeInterval _interval; /* immutable */
CFTimeInterval _tolerance; /* mutable */
uint64_t _fireTSR; /* TSR units */
CFIndex _order; /* immutable */
CFRunLoopTimerCallBack _callout; /* immutable */
CFRunLoopTimerContext _context; /* immutable, except invalidation */
};
RunLoopObserver用于監(jiān)聽(tīng)RunLoop的六種狀態(tài)。CFRunLoopObserver中的_activities用于保存RunLoop的活動(dòng)狀態(tài),當(dāng)狀態(tài)發(fā)生改變時(shí),通過(guò)回調(diào)函數(shù)_callout函數(shù)通知所有observer。
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即將進(jìn)入 RunLoop
kCFRunLoopBeforeTimers = (1UL << 1), // 即將處理 Timers
kCFRunLoopBeforeSources = (1UL << 2), // 即將處理 Sources
kCFRunLoopBeforeWaiting = (1UL << 5), // 即將進(jìn)入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 剛從休眠中喚醒
kCFRunLoopExit = (1UL << 7), // 即將退出 RunLoop
kCFRunLoopAllActivities = 0x0FFFFFFFU // 以上所有狀態(tài)
};
struct __CFRunLoopObserver {
CFRuntimeBase _base;
pthread_mutex_t _lock;
CFRunLoopRef _runLoop;
CFIndex _rlCount;
CFOptionFlags _activities; /* immutable */
CFIndex _order; /* immutable */
CFRunLoopObserverCallBack _callout; /* immutable */
CFRunLoopObserverContext _context; /* immutable, except invalidation */
};
簡(jiǎn)單過(guò)一下RunLoop的源碼。
void CFRunLoopRun(void) { /* DOES CALLOUT */
int32_t result;
do {
result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
CHECK_FOR_FORK();
} while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
簡(jiǎn)單來(lái)看RunLoop是個(gè) do..while循環(huán),下面來(lái)看看循環(huán)中具體干了哪些事情。
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { /* DOES CALLOUT */
CHECK_FOR_FORK();
if (__CFRunLoopIsDeallocating(rl)) return kCFRunLoopRunFinished;
__CFRunLoopLock(rl);
//根據(jù)modeName來(lái)查找本次運(yùn)行的mode
CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
// 如果沒(méi)找到mode 或者 mode里沒(méi)有任何的事件,就此停止,不再循環(huán)
if (NULL == currentMode || __CFRunLoopModeIsEmpty(rl, currentMode, rl->_currentMode)) {
Boolean did = false;
if (currentMode) __CFRunLoopModeUnlock(currentMode);
__CFRunLoopUnlock(rl);
return did ? kCFRunLoopRunHandledSource : kCFRunLoopRunFinished;
}
CFRunLoopModeRef previousMode = rl->_currentMode;
rl->_currentMode = currentMode;
int32_t result = kCFRunLoopRunFinished;
// 通知 observers 即將進(jìn)入RunLoop
if (currentMode->_observerMask & kCFRunLoopEntry ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
// RunLoop具體要做的事情
result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
// 通知 observers 即將退出RunLoop
if (currentMode->_observerMask & kCFRunLoopExit ) __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
__CFRunLoopModeUnlock(currentMode);
__CFRunLoopPopPerRunData(rl, previousPerRun);
rl->_currentMode = previousMode;
__CFRunLoopUnlock(rl);
return result;
}
從上面可以看到RunLoop除了通知observers即將進(jìn)入/退出外,其他具體要做的事情都寫(xiě)在了__CFRunLoopRun中。
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {
uint64_t startTSR = mach_absolute_time();
// 狀態(tài)判斷
if (__CFRunLoopIsStopped(rl)) {
__CFRunLoopUnsetStopped(rl);
return kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
rlm->_stopped = false;
return kCFRunLoopRunStopped;
}
// 初始化timeout_timer代碼 略
int32_t retVal = 0;
do {
__CFPortSet waitSet = rlm->_portSet;
__CFRunLoopUnsetIgnoreWakeUps(rl);
// 通知 observers 即將處理Timer
if (rlm->_observerMask & kCFRunLoopBeforeTimers) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
// 通知 observers 即將處理Sources
if (rlm->_observerMask & kCFRunLoopBeforeSources) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
// 處理主隊(duì)列異步的block
__CFRunLoopDoBlocks(rl, rlm);
// 處理Source0
Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
if (sourceHandledThisLoop) {
// 處理block
__CFRunLoopDoBlocks(rl, rlm);
}
Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);
didDispatchPortLastTime = false;
if (MACH_PORT_NULL != dispatchPort && !didDispatchPortLastTime) {
// 判斷有無(wú)Source1
if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
// 有Source1就跳轉(zhuǎn)到handle_msg
goto handle_msg;
}
}
// 通知 observers 即將進(jìn)入休眠
if (!poll && (rlm->_observerMask & kCFRunLoopBeforeWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
__CFRunLoopSetSleeping(rl);
__CFPortSetInsert(dispatchPort, waitSet);
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
CFAbsoluteTime sleepStart = poll ? 0.0 : CFAbsoluteTimeGetCurrent();
if (kCFUseCollectableAllocator) {
memset(msg_buffer, 0, sizeof(msg_buffer));
}
msg = (mach_msg_header_t *)msg_buffer;
// 休眠,等待消息來(lái)喚醒線程
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
rl->_sleepTime += (poll ? 0.0 : (CFAbsoluteTimeGetCurrent() - sleepStart));
__CFPortSetRemove(dispatchPort, waitSet);
__CFRunLoopSetIgnoreWakeUps(rl);
__CFRunLoopUnsetSleeping(rl);
//通知 observers RunLoop剛從休眠中喚醒
if (!poll && (rlm->_observerMask & kCFRunLoopAfterWaiting)) __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);
// 跳轉(zhuǎn)標(biāo)志 handle_msg
handle_msg:;
__CFRunLoopSetIgnoreWakeUps(rl);
if (MACH_PORT_NULL == livePort) {
CFRUNLOOP_WAKEUP_FOR_NOTHING();
// handle nothing
} else if (livePort == rl->_wakeUpPort) {
CFRUNLOOP_WAKEUP_FOR_WAKEUP();
}
#if USE_MK_TIMER_TOO
// 被Timer喚醒
else if (rlm->_timerPort != MACH_PORT_NULL && livePort == rlm->_timerPort) {
CFRUNLOOP_WAKEUP_FOR_TIMER();
//處理Timer
if (!__CFRunLoopDoTimers(rl, rlm, mach_absolute_time())) {
// Re-arm the next timer
__CFArmNextTimerInMode(rlm, rl);
}
}
#endif
// 被GCD喚醒
else if (livePort == dispatchPort) {
CFRUNLOOP_WAKEUP_FOR_DISPATCH();
__CFRunLoopModeUnlock(rlm);
__CFRunLoopUnlock(rl);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)6, NULL);
// 處理GCD
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
_CFSetTSD(__CFTSDKeyIsInGCDMainQ, (void *)0, NULL);
__CFRunLoopLock(rl);
__CFRunLoopModeLock(rlm);
sourceHandledThisLoop = true;
didDispatchPortLastTime = true;
} else {
// 被Source1喚醒
CFRUNLOOP_WAKEUP_FOR_SOURCE();
voucher_t previousVoucher = _CFSetTSD(__CFTSDKeyMachMessageHasVoucher, (void *)voucherCopy, os_release);
CFRunLoopSourceRef rls = __CFRunLoopModeFindSourceForMachPort(rl, rlm, livePort);
_CFSetTSD(__CFTSDKeyMachMessageHasVoucher, previousVoucher, os_release);
}
// 處理Block
__CFRunLoopDoBlocks(rl, rlm);
// 處理返回值
if (sourceHandledThisLoop && stopAfterHandle) {
// 進(jìn)入loop時(shí)參數(shù)標(biāo)記為處理完事件就返回
retVal = kCFRunLoopRunHandledSource;
} else if (timeout_context->termTSR < mach_absolute_time()) {
// 超出傳入?yún)?shù)標(biāo)記的超時(shí)時(shí)間
retVal = kCFRunLoopRunTimedOut;
} else if (__CFRunLoopIsStopped(rl)) {
// 被外部調(diào)用者強(qiáng)行停止
__CFRunLoopUnsetStopped(rl);
retVal = kCFRunLoopRunStopped;
} else if (rlm->_stopped) {
// 自動(dòng)停止
rlm->_stopped = false;
retVal = kCFRunLoopRunStopped;
} else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
// mode為空,沒(méi)有source0、source1、timer、observers
retVal = kCFRunLoopRunFinished;
}
} while (0 == retVal);
if (timeout_timer) {
dispatch_source_cancel(timeout_timer);
dispatch_release(timeout_timer);
} else {
free(timeout_context);
}
return retVal;
}
整體流程如下圖所示。

根據(jù)這張圖可以看出:RunLoop在BeforeSources和AfterWaiting后會(huì)進(jìn)行任務(wù)的處理??梢栽诖藭r(shí)阻塞監(jiān)控線程并設(shè)置超時(shí)時(shí)間,若超時(shí)后RunLoop的狀態(tài)仍為RunLoop在BeforeSources或AfterWaiting,表明此時(shí)RunLoop仍然在處理任務(wù),主線程發(fā)生了卡頓。
- (void)beginMonitor {
self.dispatchSemaphore = dispatch_semaphore_create(0);
// 第一個(gè)監(jiān)控,監(jiān)控是否處于 運(yùn)行狀態(tài)
CFRunLoopObserverContext context = {0, (__bridge void *) self, NULL, NULL, NULL};
self.runLoopBeginObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
LONG_MIN,
&myRunLoopBeginCallback,
&context);
// 第二個(gè)監(jiān)控,監(jiān)控是否處于 睡眠狀態(tài)
self.runLoopEndObserver = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
LONG_MAX,
&myRunLoopEndCallback,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopBeginObserver, kCFRunLoopCommonModes);
CFRunLoopAddObserver(CFRunLoopGetMain(), self.runLoopEndObserver, kCFRunLoopCommonModes);
// 創(chuàng)建子線程監(jiān)控
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//子線程開(kāi)啟一個(gè)持續(xù)的loop用來(lái)進(jìn)行監(jiān)控
while (YES) {
long semaphoreWait = dispatch_semaphore_wait(self.dispatchSemaphore, dispatch_time(DISPATCH_TIME_NOW, 17 * NSEC_PER_MSEC));
if (semaphoreWait != 0) {
if (!self.runLoopBeginObserver || !self.runLoopEndObserver) {
self.timeoutCount = 0;
self.dispatchSemaphore = 0;
self.runLoopBeginActivity = 0;
self.runLoopEndActivity = 0;
return;
}
// 兩個(gè)runloop的狀態(tài),BeforeSources和AfterWaiting這兩個(gè)狀態(tài)區(qū)間時(shí)間能夠檢測(cè)到是否卡頓
if ((self.runLoopBeginActivity == kCFRunLoopBeforeSources || self.runLoopBeginActivity == kCFRunLoopAfterWaiting) ||
(self.runLoopEndActivity == kCFRunLoopBeforeSources || self.runLoopEndActivity == kCFRunLoopAfterWaiting)) {
// 出現(xiàn)三次出結(jié)果
if (++self.timeoutCount < 2) {
continue;
}
NSLog(@"調(diào)試:監(jiān)測(cè)到卡頓");
} // end activity
}// end semaphore wait
self.timeoutCount = 0;
}// end while
});
}
// 第一個(gè)監(jiān)控,監(jiān)控是否處于 運(yùn)行狀態(tài)
void myRunLoopBeginCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
RunLoopMonitor2* lagMonitor = (__bridge RunLoopMonitor2 *)info;
lagMonitor.runLoopBeginActivity = activity;
dispatch_semaphore_t semaphore = lagMonitor.dispatchSemaphore;
dispatch_semaphore_signal(semaphore);
}
// 第二個(gè)監(jiān)控,監(jiān)控是否處于 睡眠狀態(tài)
void myRunLoopEndCallback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
RunLoopMonitor2* lagMonitor = (__bridge RunLoopMonitor2 *)info;
lagMonitor.runLoopEndActivity = activity;
dispatch_semaphore_t semaphore = lagMonitor.dispatchSemaphore;
dispatch_semaphore_signal(semaphore);
}
方案三:Ping主線程
Ping主線程的核心思想是向主線程發(fā)送一個(gè)信號(hào),一定時(shí)間內(nèi)收到了主線程的回復(fù),即表示當(dāng)前主線程流暢運(yùn)行。沒(méi)有收到主線程的回復(fù),即表示當(dāng)前主線程在做耗時(shí)運(yùn)算,發(fā)生了卡頓。
目前昆蟲(chóng)線上使用的就是這套方案。
self.semaphore = dispatch_semaphore_create(0);
- (void)main {
//判斷是否需要上報(bào)
__weak typeof(self) weakSelf = self;
void (^ verifyReport)(void) = ^() {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (strongSelf.reportInfo.length > 0) {
if (strongSelf.handler) {
double responseTimeValue = floor([[NSDate date] timeIntervalSince1970] * 1000);
double duration = responseTimeValue - strongSelf.startTimeValue;
if (DEBUG) {
NSLog(@"卡了%f,堆棧為--%@", duration, strongSelf.reportInfo);
}
strongSelf.handler(@{
@"title": [InsectUtil dateFormatNow].length > 0 ? [InsectUtil dateFormatNow] : @"",
@"duration": [NSString stringWithFormat:@"%.2f",duration],
@"content": strongSelf.reportInfo
});
}
strongSelf.reportInfo = @"";
}
};
while (!self.cancelled) {
if (_isApplicationInActive) {
self.mainThreadBlock = YES;
self.reportInfo = @"";
self.startTimeValue = floor([[NSDate date] timeIntervalSince1970] * 1000);
dispatch_async(dispatch_get_main_queue(), ^{
self.mainThreadBlock = NO;
dispatch_semaphore_signal(self.semaphore);
});
[NSThread sleepForTimeInterval:(self.threshold/1000)];
if (self.isMainThreadBlock) {
self.reportInfo = [InsectBacktraceLogger insect_backtraceOfMainThread];
}
dispatch_semaphore_wait(self.semaphore, DISPATCH_TIME_FOREVER);
//卡頓超時(shí)情況;
verifyReport();
} else {
[NSThread sleepForTimeInterval:(self.threshold/1000)];
}
}
}
總結(jié)
| 方案 | 優(yōu)點(diǎn) | 缺點(diǎn) | 實(shí)現(xiàn)復(fù)雜性 |
|---|---|---|---|
| FPS | 直觀 | 無(wú)法準(zhǔn)確定位卡頓堆棧 | 簡(jiǎn)單 |
| RunLoop Observer | 能定位卡頓堆棧 | 不能記錄卡頓時(shí)間,定義卡頓的閾值不好控制 | 復(fù)雜 |
| Ping Main Thread | 能定位卡頓堆棧,能記錄卡頓時(shí)間 | 一直ping主線程,費(fèi)電 | 中等 |