• 监测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/

    ------------------越是喧嚣的世界,越需要宁静的思考------------------ 合抱之木,生于毫末;九层之台,起于垒土;千里之行,始于足下。 积土成山,风雨兴焉;积水成渊,蛟龙生焉;积善成德,而神明自得,圣心备焉。故不积跬步,无以至千里;不积小流,无以成江海。骐骥一跃,不能十步;驽马十驾,功在不舍。锲而舍之,朽木不折;锲而不舍,金石可镂。蚓无爪牙之利,筋骨之强,上食埃土,下饮黄泉,用心一也。蟹六跪而二螯,非蛇鳝之穴无可寄托者,用心躁也。
  • 相关阅读:
    ACM Dance Recital(dfs+剪枝)
    矩阵快速幂
    分页实现复选框的选中状态
    MemCached 和redis的区别
    调用存储过程传值
    实现js中的时间格式中的T
    实现下载完成
    模态框实现功能后刷新父类页面
    JSON.parse()、JSON.stringify()和eval()的作用
    全选反选珍藏班版
  • 原文地址:https://www.cnblogs.com/feng9exe/p/14987886.html
Copyright © 2020-2023  润新知