iOS卡頓檢測:FPS及具體定位

前言

項(xiàng)目剛起步的過程中猾瘸,往往時(shí)間緊任務(wù)重,程序員在開發(fā)的時(shí)候丢习,只想著要完成開發(fā)需求牵触,沒有多余的時(shí)間去關(guān)注性能問題。但隨著項(xiàng)目越來越大咐低,功能越來多揽思,卡頓問題越來越嚴(yán)重,用戶體驗(yàn)很不好见擦。解決卡頓的問題钉汗,刻不容緩啊,于是整理了檢測卡頓的一些方法鲤屡,與大家做個(gè)分享损痰,本文主要包含 fpsping 的方式檢測。

一酒来、卡頓原因

GPU卢未、CPU幀率圖.png

在顯示器中是固定的頻率,比如iOS中是每秒60幀(60FPS)堰汉,即每幀16.7ms辽社。從上圖中可以看出,每兩個(gè)VSync信號(hào)之間有時(shí)間間隔(16.7ms)翘鸭,在這個(gè)時(shí)間內(nèi)爹袁,CPU主線程計(jì)算布局,解碼圖片矮固,創(chuàng)建視圖失息,繪制文本譬淳,計(jì)算完成后將內(nèi)容交給GPU,GPU變換盹兢,合成邻梆,渲染,放入幀緩沖區(qū)绎秒。假如16.7ms內(nèi)浦妄,CPU和GPU沒有來得及生產(chǎn)出一幀緩沖,那么這一幀會(huì)被丟棄见芹,顯示器就會(huì)保持不變剂娄,繼續(xù)顯示上一幀內(nèi)容,這就將導(dǎo)致導(dǎo)致畫面卡頓玄呛。所以無論CPU,GPU阅懦,哪個(gè)消耗時(shí)間過長,都會(huì)導(dǎo)致在16.7ms內(nèi)無法生成一幀緩存

簡單來說徘铝,主線程為了達(dá)到接近60fps的繪制效率耳胎,不能在UI線程有單個(gè)超過(1/60s≈16ms)的計(jì)算任務(wù),導(dǎo)致卡頓惕它。

以下操作可能會(huì)引起卡頓:

  • 死鎖:主線程拿到鎖 A怕午,需要獲得鎖 B,而同時(shí)某個(gè)子線程拿了鎖 B淹魄,需要鎖 A郁惜,這樣相互等待就死鎖了。
  • 搶鎖:主線程需要訪問 DB甲锡,而此時(shí)某個(gè)子線程往 DB 插入大量數(shù)據(jù)兆蕉。通常搶鎖的體驗(yàn)是偶爾卡一陣子,過會(huì)就恢復(fù)了搔体。
  • 主線程大量 IO:主線程為了方便直接寫入大量數(shù)據(jù)恨樟,會(huì)導(dǎo)致界面卡頓。
  • 主線程大量計(jì)算:算法不合理疚俱,導(dǎo)致主線程某個(gè)函數(shù)占用大量 CPU劝术。
  • 大量的 UI 繪制:復(fù)雜的 UI、圖文混排等呆奕,帶來大量的 UI 繪制养晋。

二、可視化FPS展示

FPS是Frames Per Second 的簡稱縮寫梁钾,意思是每秒傳輸幀數(shù)绳泉,也就是我們常說的“刷新率”(單位為Hz)。FPS是測量用于保存姆泻、顯示動(dòng)態(tài)視頻的信息數(shù)量零酪。每秒鐘幀數(shù)愈多冒嫡,所顯示的畫面就會(huì)愈流暢,F(xiàn)PS值越低就越卡頓四苇,所以這個(gè)值在一定程度上可以衡量應(yīng)用在圖像繪制渲染處理時(shí)的性能孝凌。一般我們的APP的FPS只要保持在 50-60 之間,用戶體驗(yàn)都是比較流暢的月腋。

我們可以通過CADisplayLink來監(jiān)控我們的FPS蟀架。CADisplayLink是CoreAnimation提供的另一個(gè)類似于NSTimer的類,它總是在屏幕完成一次更新之前啟動(dòng)榆骚,它的接口設(shè)計(jì)的和NSTimer很類似片拍,所以它實(shí)際上就是一個(gè)內(nèi)置實(shí)現(xiàn)的替代,但是和timeInterval以秒為單位不同妓肢,CADisplayLink有一個(gè)整型的frameInterval屬性捌省,指定了間隔多少幀之后才執(zhí)行。默認(rèn)值是1职恳,意味著每次屏幕更新之前都會(huì)執(zhí)行一次所禀。但是如果動(dòng)畫的代碼執(zhí)行起來超過了六十分之一秒方面,你可以指定frameInterval為2放钦,就是說動(dòng)畫每隔一幀執(zhí)行一次(一秒鐘30幀)。

@implementation MDFPSLabel {
    CADisplayLink *_link;
    NSUInteger _count;
    NSTimeInterval _lastTime;
}

- (instancetype)initWithFrame:(CGRect)frame {
    self = [super initWithFrame:frame];
    self.textAlignment = NSTextAlignmentCenter;
    self.backgroundColor = [UIColor colorWithWhite:0.000 alpha:0.700];

    // 創(chuàng)建CADisplayLink恭金,設(shè)置代理和回調(diào)
    _link = [CADisplayLink displayLinkWithTarget:[MDWeakProxy proxyWithTarget:self]
                                        selector:@selector(tick:)];
    // 并添加到當(dāng)前runloop的NSRunLoopCommonModes
    [_link addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];
    return self;
}

- (void)dealloc {
    [_link invalidate];
}

// 計(jì)算 fps
- (void)tick:(CADisplayLink *)link {

    if (_lastTime == 0) { // 當(dāng)前時(shí)間戳
        _lastTime = link.timestamp;
        return;
    }

    _count++; // 執(zhí)行次數(shù)
    NSTimeInterval delta = link.timestamp - _lastTime;
    if (delta < 1) return;
    _lastTime = link.timestamp;
    float fps = _count / delta; // fps
    _count = 0;

    // 更新 fps 
    CGFloat progress = fps / 60.0;
    self.text = [NSString stringWithFormat:@"%d",(int)round(fps)];
    self.textColor = [UIColor colorWithHue:0.27 * (progress - 0.2)
                                saturation:1
                                brightness:0.9
                                     alpha:1];
}

@end

更多FPS介紹及demo下載操禀,請(qǐng)參轉(zhuǎn)閱這篇文章:http://www.reibang.com/p/3d3f968c9cf4

三、定位具體位置

1横腿、實(shí)現(xiàn)思路

使用FPS方式只能大概推測出是哪里的問題颓屑,但不能具體定位到具體的位置。最理想的方案是讓UI線程“主動(dòng)匯報(bào)”當(dāng)前耗時(shí)的任務(wù)耿焊,聽起來簡單做起來不輕松揪惦。

我們可以假設(shè)這樣一套機(jī)制:每隔16ms讓UI線程來報(bào)道一次,如果16ms之后UI線程沒來報(bào)道罗侯,那就一定是在執(zhí)行某個(gè)耗時(shí)的任務(wù)器腋。這種抽象的描述翻譯成代碼,可以用如下表述:

我們啟動(dòng)一個(gè)worker線程钩杰,worker線程每隔一小段時(shí)間(delta)ping以下主線程(發(fā)送一個(gè)NSNotification)纫塌,如果主線程此時(shí)有空,必然能接收到這個(gè)通知讲弄,并pong以下(發(fā)送另一個(gè)NSNotification)措左,如果worker線程超過delta時(shí)間沒有收到pong的回復(fù),那么可以推測UI線程必然在處理其他任務(wù)了避除,此時(shí)我們執(zhí)行第二步操作怎披,暫停UI線程胸嘁,并打印出當(dāng)前UI線程的函數(shù)調(diào)用棧。

ping凉逛、pong流程.png

2缴渊、具體實(shí)現(xiàn)

  1. 設(shè)置定時(shí)器:工作線程定時(shí)給主線程發(fā)送 ping 消息
/// 開始監(jiān)聽
- (void)startWatch {
    // 設(shè)置定時(shí)器:定時(shí)給主線程發(fā)送信息
    uint64_t interval = PMainThreadWatcher_Watch_Interval * NSEC_PER_SEC;
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    self.pingTimer = createGCDTimer(interval,
                                    interval / 10000,
                                    queue,
                                    ^{
                                        [self pingMainThread];
                                    });
}

/// 給主線程發(fā)信息
- (void)pingMainThread {
    // 設(shè)置回應(yīng)時(shí)長定時(shí)器
    uint64_t interval = PMainThreadWatcher_Warning_Level * NSEC_PER_SEC;
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    self.pongTimer = createGCDTimer(interval,
                                    interval / 10000,
                                    queue,
                                    ^{
                                        [self onPongTimeout];
                                    });
    
    // 給主線程發(fā)送通知消息
    dispatch_async(dispatch_get_main_queue(), ^{
        NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
        [center postNotificationName:Notification_PMainThreadWatcher_Worker_Ping
                              object:nil];
    });
}

2)主線程收到 ping 消息,并返回 pong 消息

/// 收到從工作線程發(fā)送的Ping通知
- (void)detectPingFromWorkerThread {
    // 回應(yīng)工作線程的通知:發(fā)送 pong 通知
    NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
    [center postNotificationName:Notification_PMainThreadWatcher_Main_Pong
                          object:nil];
}

3)判斷回應(yīng)時(shí)長鱼炒,并做相應(yīng)處理

/// 回應(yīng)超時(shí)
- (void)onPongTimeout {
    [self cancelPongTimer];
    // 暫停主線程衔沼,打印堆棧信息
    printMainThreadCallStack();
}

/// 收到從主線程返回的Pong通知
- (void)detectPongFromMainThread {
    [self cancelPongTimer];
}

/// 取消回應(yīng)時(shí)常定時(shí)器
- (void)cancelPongTimer {
    if (self.pongTimer) {
        dispatch_source_cancel(_pongTimer);
        _pongTimer = nil;
    }
}
  1. 如果超時(shí)則殺掉進(jìn)程
int pthread_kill(pthread_t, int);

殺掉進(jìn)程這里使用 pthread_kill(), 該函數(shù)的API介紹如下

The pthread_kill() function sends the signal sig to thread, a thread in the same process as the caller. The signal is asynchronously directed to thread. If sig is 0, then no signal is sent, but error checking is still performed.

別被名字嚇到,pthread_kill 可不是kill昔瞧,而是向線程發(fā)送signal指蚁,大部分signal的默認(rèn)動(dòng)作是終止進(jìn)程的運(yùn)行。向指定ID的線程發(fā)送sig信號(hào)自晰,如果線程代碼內(nèi)不做處理凝化,則按照信號(hào)默認(rèn)的行為影響整個(gè)進(jìn)程,也就是說酬荞,如果你給一個(gè)線程發(fā)送了SIGQUIT搓劫,但線程卻沒有實(shí)現(xiàn)signal處理函數(shù),則整個(gè)進(jìn)程退出混巧。

static void printMainThreadCallStack() {
    NSLog(@"發(fā)送信號(hào): %d 到主線程", CALLSTACK_SIG);
    // pthread_kill主線程
    pthread_kill(mainThreadID, CALLSTACK_SIG);
}

5)監(jiān)聽信號(hào)枪向,打印堆棧信息

iOS允許在主線程注冊(cè)一個(gè)signal處理函數(shù),當(dāng)調(diào)用pthread_kill函數(shù)時(shí)能收到該信號(hào)咧党,這時(shí)候就可以在signal回調(diào)方法中打印堆棧信息了秘蛔。

/// singal回調(diào)方法
static void thread_singal_handler(int sig) {
    NSLog(@"主線程捕獲信號(hào): %d", sig);
    if (sig != CALLSTACK_SIG) {
        return;
    }
    
    NSArray *callStack = [NSThread callStackSymbols];
    // 代理回調(diào)或打印堆棧信息
    id<PMainThreadWatcherDelegate> del = [PMainThreadWatcher sharedInstance].watchDelegate;
    if (del != nil && [del respondsToSelector:@selector(onMainThreadSlowStackDetected:)]) {
        [del onMainThreadSlowStackDetected:callStack];
    } else {
        NSLog(@"檢測主線程上的耗時(shí)調(diào)用堆棧 \n");
        for (NSString *call in callStack) {
            NSLog(@"%@\n", call);
        }
    }
    
    return;
}

/// 注冊(cè)signal函數(shù)
static void install_signal_handler() {
    // 主線程注冊(cè)一個(gè)signal處理函數(shù)
    signal(CALLSTACK_SIG, thread_singal_handler);
}

注意
signal方法不能調(diào)試,因?yàn)閄code Debug模式運(yùn)行App時(shí)傍衡,App進(jìn)程signal被LLDB Debugger調(diào)試器捕獲深员,導(dǎo)致signal handler無法進(jìn),但UI線程在遇到卡頓的時(shí)候還是能正常被中斷蛙埂。

更多signal函數(shù)用法及解釋倦畅,請(qǐng)轉(zhuǎn)閱這篇文章:

本章節(jié)根據(jù)該文改編:http://mrpeak.cn/blog/ui-detect/ 。原文有對(duì)應(yīng)的 Demo 绣的,可點(diǎn)擊查看下載叠赐。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市被辑,隨后出現(xiàn)的幾起案子燎悍,更是在濱河造成了極大的恐慌,老刑警劉巖盼理,帶你破解...
    沈念sama閱讀 207,248評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谈山,死亡現(xiàn)場離奇詭異,居然都是意外死亡宏怔,警方通過查閱死者的電腦和手機(jī)奏路,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,681評(píng)論 2 381
  • 文/潘曉璐 我一進(jìn)店門畴椰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鸽粉,你說我怎么就攤上這事斜脂。” “怎么了触机?”我有些...
    開封第一講書人閱讀 153,443評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵帚戳,是天一觀的道長。 經(jīng)常有香客問我儡首,道長片任,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,475評(píng)論 1 279
  • 正文 為了忘掉前任蔬胯,我火速辦了婚禮对供,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘氛濒。我一直安慰自己产场,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,458評(píng)論 5 374
  • 文/花漫 我一把揭開白布舞竿。 她就那樣靜靜地躺著京景,像睡著了一般。 火紅的嫁衣襯著肌膚如雪炬灭。 梳的紋絲不亂的頭發(fā)上醋粟,一...
    開封第一講書人閱讀 49,185評(píng)論 1 284
  • 那天靡菇,我揣著相機(jī)與錄音重归,去河邊找鬼。 笑死厦凤,一個(gè)胖子當(dāng)著我的面吹牛鼻吮,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播较鼓,決...
    沈念sama閱讀 38,451評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼椎木,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了博烂?” 一聲冷哼從身側(cè)響起香椎,我...
    開封第一講書人閱讀 37,112評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎禽篱,沒想到半個(gè)月后畜伐,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,609評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡躺率,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,083評(píng)論 2 325
  • 正文 我和宋清朗相戀三年玛界,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了万矾。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,163評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡慎框,死狀恐怖良狈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情笨枯,我是刑警寧澤薪丁,帶...
    沈念sama閱讀 33,803評(píng)論 4 323
  • 正文 年R本政府宣布,位于F島的核電站馅精,受9級(jí)特大地震影響窥突,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜硫嘶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,357評(píng)論 3 307
  • 文/蒙蒙 一阻问、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧沦疾,春花似錦称近、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,357評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至忆畅,卻和暖如春衡未,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背家凯。 一陣腳步聲響...
    開封第一講書人閱讀 31,590評(píng)論 1 261
  • 我被黑心中介騙來泰國打工缓醋, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人绊诲。 一個(gè)月前我還...
    沈念sama閱讀 45,636評(píng)論 2 355
  • 正文 我出身青樓送粱,卻偏偏與公主長得像,于是被迫代替她去往敵國和親掂之。 傳聞我的和親對(duì)象是個(gè)殘疾皇子抗俄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,925評(píng)論 2 344

推薦閱讀更多精彩內(nèi)容

  • Swift1> Swift和OC的區(qū)別1.1> Swift沒有地址/指針的概念1.2> 泛型1.3> 類型嚴(yán)謹(jǐn) 對(duì)...
    cosWriter閱讀 11,089評(píng)論 1 32
  • iOS設(shè)備雖然在硬件和軟件層面一直在優(yōu)化,但還是有不少坑會(huì)導(dǎo)致UI線程的卡頓世舰。對(duì)于程序員來說动雹,除了增加自身知識(shí)儲(chǔ)備...
    skogt閱讀 422評(píng)論 0 6
  • java 接口的意義-百度 規(guī)范、擴(kuò)展跟压、回調(diào) 抽象類的意義-樂視 為其子類提供一個(gè)公共的類型封裝子類中得重復(fù)內(nèi)容定...
    交流電1582閱讀 2,209評(píng)論 0 11
  • OC與Swift如何實(shí)現(xiàn)混編 1胰蝠、 Swift項(xiàng)目中使用OC 在Swift中引用OC需要借助橋接文件xx brid...
    MichealZJ閱讀 163評(píng)論 0 0
  • 6月14日與15日,在網(wǎng)友淇媽的精心組織下,我們開啟了出色時(shí)尚私塾的兩日學(xué)習(xí)姊氓。 我是朱老師的粉絲丐怯。來學(xué)習(xí)的目地很明...
    Lyric_3220閱讀 332評(píng)論 0 1