監測APP卡頓


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

img

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

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

  • 監測卡頓:開一個子線程,利用displaylink或者Runloop來監測卡頓;

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

  • 上傳記錄:將卡頓上傳到后台或自定義;

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

    img

二、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

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

img

四、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/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM