IOS中判斷卡頓的方案總結(jié)
FPS
FPS (Frames Per Second) 是圖像領(lǐng)域中的定義,表示每秒渲染幀數(shù),通常用于衡量畫(huà)面的流暢度,每秒幀數(shù)越多,則表示畫(huà)面越流暢,60fps 最佳,一般我們的APP的FPS 只要保持在 50-60之間,用戶(hù)體驗(yàn)都是比較流暢的。
監(jiān)測(cè)FPS也有好幾種,這里只說(shuō)最常用的方案,我最早是在YYFPSLabel中看到的。實(shí)現(xiàn)原理實(shí)現(xiàn)原理是向主線程的RunLoop的添加一個(gè)commonModes的CADisplayLink,每次屏幕刷新的時(shí)候都要執(zhí)行CADisplayLink的方法,所以可以統(tǒng)計(jì)1s內(nèi)屏幕刷新的次數(shù),也就是FPS了,下面貼上我用Swift實(shí)現(xiàn)的代碼:
class WeakProxy: NSObject {
weak var target: NSObjectProtocol?
init(target: NSObjectProtocol) {
self.target = target
super.init()
}
override func responds(to aSelector: Selector!) -> Bool {
return (target?.responds(to: aSelector) ?? false) || super.responds(to: aSelector)
}
override func forwardingTarget(for aSelector: Selector!) -> Any? {
return target
}
}
class FPSLabel: UILabel {
var link:CADisplayLink!
//記錄方法執(zhí)行次數(shù)
var count: Int = 0
//記錄上次方法執(zhí)行的時(shí)間,通過(guò)link.timestamp - _lastTime計(jì)算時(shí)間間隔
var lastTime: TimeInterval = 0
var _font: UIFont!
var _subFont: UIFont!
fileprivate let defaultSize = CGSize(width: 55,height: 20)
override init(frame: CGRect) {
super.init(frame: frame)
if frame.size.width == 0 && frame.size.height == 0 {
self.frame.size = defaultSize
}
self.layer.cornerRadius = 5
self.clipsToBounds = true
self.textAlignment = NSTextAlignment.center
self.isUserInteractionEnabled = false
self.backgroundColor = UIColor.white.withAlphaComponent(0.7)
_font = UIFont(name: "Menlo", size: 14)
if _font != nil {
_subFont = UIFont(name: "Menlo", size: 4)
}else{
_font = UIFont(name: "Courier", size: 14)
_subFont = UIFont(name: "Courier", size: 4)
}
link = CADisplayLink(target: WeakProxy.init(target: self), selector: #selector(FPSLabel.tick(link:)))
link.add(to: RunLoop.main, forMode: .commonModes)
}
//CADisplayLink 刷新執(zhí)行的方法
@objc func tick(link: CADisplayLink) {
guard lastTime != 0 else {
lastTime = link.timestamp
return
}
count += 1
let timePassed = link.timestamp - lastTime
//時(shí)間大于等于1秒計(jì)算一次,也就是FPSLabel刷新的間隔,不希望太頻繁刷新
guard timePassed >= 1 else {
return
}
lastTime = link.timestamp
let fps = Double(count) / timePassed
count = 0
let progress = fps / 60.0
let color = UIColor(hue: CGFloat(0.27 * (progress - 0.2)), saturation: 1, brightness: 0.9, alpha: 1)
let text = NSMutableAttributedString(string: "\(Int(round(fps))) FPS")
text.addAttribute(NSAttributedStringKey.foregroundColor, value: color, range: NSRange(location: 0, length: text.length - 3))
text.addAttribute(NSAttributedStringKey.foregroundColor, value: UIColor.white, range: NSRange(location: text.length - 3, length: 3))
text.addAttribute(NSAttributedStringKey.font, value: _font, range: NSRange(location: 0, length: text.length))
text.addAttribute(NSAttributedStringKey.font, value: _subFont, range: NSRange(location: text.length - 4, length: 1))
self.attributedText = text
}
// 把displaylin從Runloop modes中移除
deinit {
link.invalidate()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
RunLoop
其實(shí)FPS中CADisplayLink的使用也是基于RunLoop,都依賴(lài)main RunLoop。我們來(lái)看看
先來(lái)看看簡(jiǎn)版的RunLoop的代碼
// 1.進(jìn)入loop __CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) // 2.RunLoop 即將觸發(fā) Timer 回調(diào)。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers); // 3.RunLoop 即將觸發(fā) Source0 (非port) 回調(diào)。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources); // 4.RunLoop 觸發(fā) Source0 (非port) 回調(diào)。 sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle) // 5.執(zhí)行被加入的block __CFRunLoopDoBlocks(runloop, currentMode); // 6.RunLoop 的線程即將進(jìn)入休眠(sleep)。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting); // 7.調(diào)用 mach_msg 等待接受 mach_port 的消息。線程將進(jìn)入休眠, 直到被下面某一個(gè)事件喚醒。 __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) // 進(jìn)入休眠 // 8.RunLoop 的線程剛剛被喚醒了。 __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting // 9.如果一個(gè) Timer 到時(shí)間了,觸發(fā)這個(gè)Timer的回調(diào) __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time()) // 10.如果有dispatch到main_queue的block,執(zhí)行bloc __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg); // 11.如果一個(gè) Source1 (基于port) 發(fā)出事件了,處理這個(gè)事件 __CFRunLoopDoSource1(runloop, currentMode, source1, msg); // 12.RunLoop 即將退出 __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
我們可以看到RunLoop調(diào)用方法主要集中在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之間,有人可能會(huì)問(wèn)kCFRunLoopAfterWaiting之后也有一些方法調(diào)用,為什么不監(jiān)測(cè)呢,我的理解,大部分導(dǎo)致卡頓的的方法是在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之間,比如source0主要是處理App內(nèi)部事件,App自己負(fù)責(zé)管理(出發(fā)),如UIEvent(Touch事件等,GS發(fā)起到RunLoop運(yùn)行再到事件回調(diào)到UI)、CFSocketRef。開(kāi)辟一個(gè)子線程,然后實(shí)時(shí)計(jì)算 kCFRunLoopBeforeSources 和 kCFRunLoopAfterWaiting 兩個(gè)狀態(tài)區(qū)域之間的耗時(shí)是否超過(guò)某個(gè)閥值,來(lái)斷定主線程的卡頓情況。
這里做法又有點(diǎn)不同,iOS實(shí)時(shí)卡頓監(jiān)控3 是設(shè)置連續(xù)5次超時(shí)50ms認(rèn)為卡頓,戴銘在 GCDFetchFeed4 中設(shè)置的是連續(xù)3次超時(shí)80ms認(rèn)為卡頓的代碼。以下是iOS實(shí)時(shí)卡頓監(jiān)控中提供的代碼:
- (void)start
{
if (observer)
return;
// 信號(hào)
semaphore = dispatch_semaphore_create(0);
// 注冊(cè)RunLoop狀態(tài)觀察
CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
kCFRunLoopAllActivities,
YES,
0,
&runLoopObserverCallBack,
&context);
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 在子線程監(jiān)控時(shí)長(zhǎng)
dispatch_async(dispatch_get_global_queue(0, 0), ^{
while (YES)
{
long st = dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, 50*NSEC_PER_MSEC));
if (st != 0)
{
if (!observer)
{
timeoutCount = 0;
semaphore = 0;
activity = 0;
return;
}
if (activity==kCFRunLoopBeforeSources || activity==kCFRunLoopAfterWaiting)
{
if (++timeoutCount < 5)
continue;
PLCrashReporterConfig *config = [[PLCrashReporterConfig alloc] initWithSignalHandlerType:PLCrashReporterSignalHandlerTypeBSD
symbolicationStrategy:PLCrashReporterSymbolicationStrategyAll];
PLCrashReporter *crashReporter = [[PLCrashReporter alloc] initWithConfiguration:config];
NSData *data = [crashReporter generateLiveReport];
PLCrashReport *reporter = [[PLCrashReport alloc] initWithData:data error:NULL];
NSString *report = [PLCrashReportTextFormatter stringValueForCrashReport:reporter
withTextFormat:PLCrashReportTextFormatiOS];
NSLog(@"------------\n%@\n------------", report);
}
}
timeoutCount = 0;
}
});
}
子線程Ping
但是由于主線程的RunLoop在閑置時(shí)基本處于Before Waiting狀態(tài),這就導(dǎo)致了即便沒(méi)有發(fā)生任何卡頓,這種檢測(cè)方式也總能認(rèn)定主線程處在卡頓狀態(tài)。這套卡頓監(jiān)控方案大致思路為:創(chuàng)建一個(gè)子線程通過(guò)信號(hào)量去ping主線程,因?yàn)閜ing的時(shí)候主線程肯定是在kCFRunLoopBeforeSources和kCFRunLoopAfterWaiting之間。每次檢測(cè)時(shí)設(shè)置標(biāo)記位為YES,然后派發(fā)任務(wù)到主線程中將標(biāo)記位設(shè)置為NO。接著子線程沉睡超時(shí)闕值時(shí)長(zhǎng),判斷標(biāo)志位是否成功設(shè)置成NO,如果沒(méi)有說(shuō)明主線程發(fā)生了卡頓。ANREye5中就是使用子線程Ping的方式監(jiān)測(cè)卡頓的。
@interface PingThread : NSThread
......
@end
@implementation PingThread
- (void)main {
[self pingMainThread];
}
- (void)pingMainThread {
while (!self.cancelled) {
@autoreleasepool {
dispatch_async(dispatch_get_main_queue(), ^{
[_lock unlock];
});
CFAbsoluteTime pingTime = CFAbsoluteTimeGetCurrent();
NSArray *callSymbols = [StackBacktrace backtraceMainThread];
[_lock lock];
if (CFAbsoluteTimeGetCurrent() - pingTime >= _threshold) {
......
}
[NSThread sleepForTimeInterval: _interval];
}
}
}
@end
以下是我用Swift實(shí)現(xiàn)的:
public class CatonMonitor {
enum Constants {
static let timeOutInterval: TimeInterval = 0.05
static let queueTitle = "com.roy.PerformanceMonitor.CatonMonitor"
}
private var queue: DispatchQueue = DispatchQueue(label: Constants.queueTitle)
private var isMonitoring = false
private var semaphore: DispatchSemaphore = DispatchSemaphore(value: 0)
public init() {}
public func start() {
guard !isMonitoring else { return }
isMonitoring = true
queue.async {
while self.isMonitoring {
var timeout = true
DispatchQueue.main.async {
timeout = false
self.semaphore.signal()
}
Thread.sleep(forTimeInterval: Constants.timeOutInterval)
if timeout {
let symbols = RCBacktrace.callstack(.main)
for symbol in symbols {
print(symbol.description)
}
}
self.semaphore.wait()
}
}
}
public func stop() {
guard isMonitoring else { return }
isMonitoring = false
}
}
CPU超過(guò)了80%
這個(gè)是Matrix-iOS 卡頓監(jiān)控提到的:
我們也認(rèn)為 CPU 過(guò)高也可能導(dǎo)致應(yīng)用出現(xiàn)卡頓,所以在子線程檢查主線程狀態(tài)的同時(shí),如果檢測(cè)到 CPU 占用過(guò)高,會(huì)捕獲當(dāng)前的線程快照保存到文件中。目前微信應(yīng)用中認(rèn)為,單核 CPU 的占用超過(guò)了 80%,此時(shí)的 CPU 占用就過(guò)高了。
這種方式一般不能單獨(dú)拿來(lái)作為卡頓監(jiān)測(cè),但可以像微信Matrix一樣配合其他方式一起工作。
戴銘在GCDFetchFeed中如果CPU 的占用超過(guò)了 80%也捕獲函數(shù)調(diào)用棧,以下是代碼:
#define CPUMONITORRATE 80
+ (void)updateCPU {
thread_act_array_t threads;
mach_msg_type_number_t threadCount = 0;
const task_t thisTask = mach_task_self();
kern_return_t kr = task_threads(thisTask, &threads, &threadCount);
if (kr != KERN_SUCCESS) {
return;
}
for (int i = 0; i < threadCount; i++) {
thread_info_data_t threadInfo;
thread_basic_info_t threadBaseInfo;
mach_msg_type_number_t threadInfoCount = THREAD_INFO_MAX;
if (thread_info((thread_act_t)threads[i], THREAD_BASIC_INFO, (thread_info_t)threadInfo, &threadInfoCount) == KERN_SUCCESS) {
threadBaseInfo = (thread_basic_info_t)threadInfo;
if (!(threadBaseInfo->flags & TH_FLAGS_IDLE)) {
integer_t cpuUsage = threadBaseInfo->cpu_usage / 10;
if (cpuUsage > CPUMONITORRATE) {
//cup 消耗大于設(shè)置值時(shí)打印和記錄堆棧
NSString *reStr = smStackOfThread(threads[i]);
SMCallStackModel *model = [[SMCallStackModel alloc] init];
model.stackStr = reStr;
//記錄數(shù)據(jù)庫(kù)中
[[[SMLagDB shareInstance] increaseWithStackModel:model] subscribeNext:^(id x) {}];
// NSLog(@"CPU useage overload thread stack:\n%@",reStr);
}
}
}
}
}
卡頓方法的棧信息
當(dāng)我們得到卡頓的時(shí)間點(diǎn),就要立即拿到卡頓的堆棧,有兩種方式一種是遍歷棧幀,實(shí)現(xiàn)原理我在iOS獲取任意線程調(diào)用棧7寫(xiě)的挺詳細(xì)的,同時(shí)開(kāi)源了代碼RCBacktrace,另一種方式是通過(guò)Signal獲取任意線程調(diào)用棧,實(shí)現(xiàn)原理我在通過(guò)Signal handling(信號(hào)處理)獲取任意線程調(diào)用棧寫(xiě)了,代碼在backtrace-swift,但這種方式在調(diào)試時(shí)比較麻煩,建議用第一種方式。
以上就是IOS中判斷卡頓的方案總結(jié)的詳細(xì)內(nèi)容,更多關(guān)于IOS卡頓檢測(cè)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
iOS自動(dòng)生成表格效果的實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了iOS自動(dòng)生成表格效果的實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-03-03
iOS實(shí)現(xiàn)從通訊錄中選擇聯(lián)系人
這篇文章主要為大家詳細(xì)介紹了iOS實(shí)現(xiàn)從通訊錄中選擇聯(lián)系人,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-08-08
iOS 條碼及二維碼掃描(從相冊(cè)中讀取條形碼/二維碼)及掃碼過(guò)程中遇到的坑
本文主要給大家介紹ios中從手機(jī)相冊(cè)中讀取條形碼和二維碼的問(wèn)題及解決辦法,需要的朋友參考下2017-01-01
iOS block循環(huán)引用詳解及常見(jiàn)誤區(qū)
這篇文章主要介紹了iOS block循環(huán)引用詳解和應(yīng)用,常見(jiàn)誤區(qū)詳解,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-08-08
iOS適配https證書(shū)問(wèn)題(AFNetworking3.0為例)
本篇文章主要介紹了iOS適配https問(wèn)題(AFNetworking3.0為例)。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-02-02
iOS實(shí)現(xiàn)封裝一個(gè)獲取通訊錄的工具類(lèi)詳解
這篇文章主要給大家介紹了關(guān)于iOS如何實(shí)現(xiàn)封裝一個(gè)獲取通訊錄的工具類(lèi)的相關(guān)資料,這是自己平時(shí)封裝的一個(gè)工具類(lèi),使用非常方便,文中給出了詳細(xì)的示例代碼,需要的朋友們可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-10-10

