1. 程式人生 > 其它 >監測APP卡頓

監測APP卡頓

一、UI更新原理和卡頓原因

在 VSync 訊號到來後,系統圖形服務會通過 CADisplayLink 等機制通知 App,App 主執行緒開始在 CPU 中計算顯示內容,比如檢視的建立、佈局計算、圖片解碼、文字繪製等。隨後 CPU 會將計算好的內容提交到 GPU 去,由 GPU 進行變換、合成、渲染。隨後 GPU 會把渲染結果提交到幀緩衝區去,等待下一次 VSync 訊號到來時顯示到螢幕上。由於垂直同步的機制,如果在一個 VSync 時間內,CPU 或者 GPU 沒有完成內容提交,則那一幀就會被丟棄,等待下一次機會再顯示,而這時顯示屏會保留之前的內容不變。這就是介面卡頓的原因。

所以,卡頓造成的原因分為CPU卡頓和GPU卡頓,CPU卡頓可以用CADisplayLink來檢測,UI更新卡頓可以用Runloop的mode來檢測

  • 監測卡頓:開一個子執行緒,利用displaylink或者Runloop來監測卡頓;

  • 收集堆疊:將卡頓時的堆疊收集起來;

  • 上傳記錄:將卡頓上傳到後臺或自定義;

    這裡我引用一張微信開發團隊的監測流程圖:

二、Runloop檢測卡頓

首先我們來看一個Runloop的執行方式,如下

int32_t __CFRunLoopRun()
{
    // 通知即將進入runloop
  	//建立AutoreleasePool: _objc_autoreleasePoolPush();
    __CFRunLoopDoObservers(KCFRunLoopEntry);
    
    do
    {
        // 通知將要處理timer和source
        __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
        __CFRunLoopDoObservers(kCFRunLoopBeforeSources);
        
        // 處理非延遲的主執行緒呼叫
        __CFRunLoopDoBlocks();
        // 處理UIEvent事件
        __CFRunLoopDoSource0();
        
        // GCD dispatch main queue
        CheckIfExistMessagesInMainDispatchQueue();
        
        // 即將進入休眠
      	//釋放並新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
        __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
        
        // 等待核心mach_msg事件
        mach_port_t wakeUpPort = SleepAndWaitForWakingUpPorts();
        
        // Zzz...
        
        // 從等待中醒來
        __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
                
        if (wakeUpPort == timerPort){// 處理因timer的喚醒
          __CFRunLoopDoTimers();
        }else if (wakeUpPort == mainDispatchQueuePort){// 處理非同步方法喚醒,如dispatch_async
          __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
        } else{// UI重新整理,動畫顯示
          __CFRunLoopDoSource1();
        }   
        // 再次確保是否有同步的方法需要呼叫
        __CFRunLoopDoBlocks();
        
    } while (!stop && !timeout);
    
    // 通知即將退出runloop
  	//釋放AutoreleasePool: _objc_autoreleasePoolPop();
    __CFRunLoopDoObservers(CFRunLoopExit);
}
Objective

UI更新一般kCFRunLoopBeforeSourceskCFRunLoopBeforeWaiting之間,所以我們監測他們之間的時間段就能知道UI是否卡頓了

- (void)startMoniter{
	//新增監聽
    CFRunLoopObserverContext context = {0,(__bridge void*)self,NULL,NULL};
    _observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                        kCFRunLoopAllActivities,
                                        YES,
                                        0,
                                        &runLoopObserverCallBack,
                                        &context);
    CFRunLoopAddObserver(CFRunLoopGetMain(), _observer, kCFRunLoopCommonModes);
    
    // 建立訊號
    _semaphore = dispatch_semaphore_create(0);
    
    // 在子執行緒監控時長
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        while (YES)
        {
            NSLog(@"smooth--monitering");
            //100ms則將堆疊記錄下來
            long st = dispatch_semaphore_wait(_semaphore, dispatch_time(DISPATCH_TIME_NOW, 100*NSEC_PER_MSEC));
            if (st != 0)
            {
                if (_activity==kCFRunLoopBeforeSources || _activity==kCFRunLoopAfterWaiting)
                {
                    [self logStack];
                }
            }
        }
    });
}

static void runLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    SmoothMoniter *instrance = [SmoothMoniter sharedInstance];
    instrance.activity = activity;
    dispatch_semaphore_t semaphore = instrance.semaphore;
    dispatch_semaphore_signal(semaphore);
}
Plaintext

三、收集堆疊

收集堆疊資訊以用來分析卡頓引起的程式碼

#import <libkern/OSAtomic.h>
#import <execinfo.h>
Plaintext
- (void)logStack{
    void* callstack[128];
    int frames = backtrace(callstack, 128);
    char **strs = backtrace_symbols(callstack, frames);
    int i;
    NSMutableArray *backtrace = [NSMutableArray arrayWithCapacity:frames];
    for ( i = 0 ; i < frames ; i++ ){
        [backtrace addObject:[NSString stringWithUTF8String:strs[i]]];
    }
    free(strs);
}
Plaintext

可以得到類似於下方的堆疊記錄

四、DisplayLink檢測卡頓

一但 CADisplayLink 以特定的模式註冊到runloop之後,每當螢幕需要重新整理的時候,runloop就會呼叫CADisplayLink繫結的target上的selector,這時target可以讀到 CADisplayLink 的每次呼叫的時間戳,用來準備下一幀顯示需要的資料。所以通過比較dispalylink的更新時間就可以知道是否存在卡頓

- (void)updateTime{
    if (!_last_time) {
        _last_time = self.displayLink.timestamp;
        return;
    }
    _count ++;
    CFTimeInterval current = self.displayLink.timestamp;
    CFTimeInterval period = current - _last_time;
    if (period > 1 ) {
        NSLog(@"FPS:%@",@(_count));
        _count = 0;
        _last_time = self.displayLink.timestamp;
    }
    
}

- (CADisplayLink *)displayLink{
    if (!_displayLink) {
        _displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(updateTime)];
        [_displayLink addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
    }
    return _displayLink;
}
Plaintext

五、上傳記錄

1、頻率以及流量:是否所有的使用者都要做統計?上傳的頻率?檔案壓縮以減少流量?這些問題都要根據實際情況作好準備。

2、上傳位置,一種是自己建立後臺來統計這些卡頓,嫌麻煩的話是利用第三方平臺、如友盟(統計崩潰比較多)、聽雲、OneApm、博睿,都大同小異。

六、程式碼

上面的程式碼可以在smoothMonitor下載

http://www.helloted.com/ios/2016/10/06/smoothMonitor/

------------------越是喧囂的世界,越需要寧靜的思考------------------ 合抱之木,生於毫末;九層之臺,起於壘土;千里之行,始於足下。 積土成山,風雨興焉;積水成淵,蛟龍生焉;積善成德,而神明自得,聖心備焉。故不積跬步,無以至千里;不積小流,無以成江海。騏驥一躍,不能十步;駑馬十駕,功在不捨。鍥而舍之,朽木不折;鍥而不捨,金石可鏤。蚓無爪牙之利,筋骨之強,上食埃土,下飲黃泉,用心一也。蟹六跪而二螯,非蛇鱔之穴無可寄託者,用心躁也。