06-iOS 性能优化|性能指标监测

3,689 阅读7分钟

前言

在项目中,直接面向用户的客户端往往是一个项目的门面。因此,在项目开发建设的过程中,为了交付用户体验较佳的客户端App,保障产品交付质量。往往需要我们开发者关注客户端软件的性能指标问题。因此,我们要对应用的性能优化专题有所研究!!
我们通常关注的性能指标有:

  • 页面卡顿
  • 耗电、发热
  • 网络优化
  • 应用启动
  • 安装包瘦身

我们在开发建设项目过程中,可以粗略划分为几个阶段:开发阶段测试阶段维护阶段:

  • 开发阶段,我们要掌握性能调试性能监测的手段,从而保障,在当前稳定版本的客户端软件,有一个比较合理的性能保障;
  • 测试阶段,测试团队等若干同事往往会给我们提出一些用户体验上的反馈和建议,因此,我们需要掌握性能调试的手段,从而改造出比较符合团队要求的产品;
  • 在上线维护阶段,针对已经上线的应用,我们的开发团队要有线上性能监控的能力,从而及时收集不满足性能指标要求的业务交互场景和步骤,捕获具体问题进行分析,从而以此为依据作为有效迭代优化我们客户端的有力助力。

为此,我们本次将会用几篇文章,围绕一些常见的性能指标,去关注 如何调试、如何监测、如何改进处理问题:

一、概述

本文主要是针对 维护阶段 这两个线上场景,围绕常见的几个性能指标要点:页面卡顿离屏渲染耗电优化内存泄露App启动优化,展开来陈述如何利用代码插件平台工具进行性能指标监测的。关于相关的同一主题的其它要点,待来日按需逐个补充完善。

二、卡顿的检测

平时所说的“卡顿”主要是因为在主线程执行了比较耗时的操作

1.FPS监控

FPS的监控,参照YYKit中的YYFPSLabel,主要是通过CADisplayLink实现。借助link的时间差,来计算一次刷新刷新所需的时间,然后通过刷新次数 / 时间差得到刷新频次,并判断是否其范围,通过显示不同的文字颜色来表示卡顿严重程度。代码实现如下

class HPFPSLabel: UILabel {

    fileprivate var link: CADisplayLink = {
        let link = CADisplayLink.init()
        return link
    }()
    
    fileprivate var count: Int = 0
    fileprivate var lastTime: TimeInterval = 0.0
    fileprivate var fpsColor: UIColor = {
        return UIColor.green
    }()
    fileprivate var fps: Double = 0.0
    
    override init(frame: CGRect) {
        var f = frame
        if f.size == CGSize.zero {
            f.size = CGSize(width: 80.0, height: 22.0)
        }
        
        super.init(frame: f)
        
        self.textColor = UIColor.white
        self.textAlignment = .center
        self.font = UIFont.init(name: "Menlo", size: 12)
        self.backgroundColor = UIColor.lightGray
        //通过虚拟类
        link = CADisplayLink.init(target: CJLWeakProxy(target:self), selector: #selector(tick(_:)))
        link.add(to: RunLoop.current, forMode: RunLoop.Mode.common)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    deinit {
        link.invalidate()
    }
    
    @objc func tick(_ link: CADisplayLink){
        guard lastTime != 0 else {
            lastTime = link.timestamp
            return
        }
        
        count += 1
        //时间差
        let detla = link.timestamp - lastTime
        guard detla >= 1.0 else {
            return
        }
        
        lastTime = link.timestamp
        //刷新次数 / 时间差 = 刷新频次
        fps = Double(count) / detla
        let fpsText = "(String.init(format: "%.2f", fps)) FPS"
        count = 0
        
        let attrMStr = NSMutableAttributedString(attributedString: NSAttributedString(string: fpsText))
        if fps > 55.0 {
            //流畅
            fpsColor = UIColor.green
        }else if (fps >= 50.0 && fps <= 55.0){
            //一般
            fpsColor = UIColor.yellow
        }else{
            //卡顿
            fpsColor = UIColor.red
        }
        
        attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: fpsColor], range: NSMakeRange(0, attrMStr.length - 3))
        attrMStr.setAttributes([NSAttributedString.Key.foregroundColor: UIColor.white], range: NSMakeRange(attrMStr.length - 3, 3))
        
        DispatchQueue.main.async {
            self.attributedText = attrMStr
        }
    }

} 

2.主线程卡顿监控

我们可以添加Observer到主线程RunLoop中,通过监听RunLoop状态切换的耗时,以达到监控卡顿的目的

实现思路:

在整个运行循环中,需要监听的主要就是RunLoop结束休眠到处理Source0的这段时间,如果时间过长,就证明有耗时操作

检测主线程每次执行消息循环的时间,当这个时间大于规定的阈值时,就记为发生了一次卡顿。这个也是微信卡顿三方matrix的原理

以下是一个简易版RunLoop监控的实现

class HPBlockMonitor: NSObject {
    
    static let share = LLBlockMonitor.init()
    
    fileprivate var semaphore: DispatchSemaphore!
    fileprivate var timeoutCount: Int!
    fileprivate var activity: CFRunLoopActivity!
    
    private override init() {
        super.init()
    }

    
    public func start(){
        //监控两个状态
        registerObserver()
        
        //启动监控
        startMonitor()
    }
}

fileprivate extension LLBlockMonitor{
    
    func registerObserver(){
        let controllerPointer = Unmanaged<LLBlockMonitor>.passUnretained(self).toOpaque()
        var context: CFRunLoopObserverContext = CFRunLoopObserverContext(version: 0, info: controllerPointer, retain: nil, release: nil, copyDescription: nil)
        let observer: CFRunLoopObserver = CFRunLoopObserverCreate(nil, CFRunLoopActivity.allActivities.rawValue, true, 0, { (observer, activity, info) in
            
            guard info != nil else{
                return
            }
            
            let monitor: LLBlockMonitor = Unmanaged<LLBlockMonitor>.fromOpaque(info!).takeUnretainedValue()
            monitor.activity = activity
            let sem: DispatchSemaphore = monitor.semaphore
            sem.signal()
            
        }, &context)
        
        CFRunLoopAddObserver(CFRunLoopGetMain(), observer, CFRunLoopMode.commonModes)
    }
    
    func  startMonitor(){
        //创建信号
        semaphore = DispatchSemaphore(value: 0)
        //在子线程监控时长
        DispatchQueue.global().async {
            while(true){
                // 超时时间是 1 秒,没有等到信号量,st 就不等于 0, RunLoop 所有的任务
                let st = self.semaphore.wait(timeout: DispatchTime.now()+1.0)
                if st != DispatchTimeoutResult.success {
                    //监听两种状态kCFRunLoopBeforeSources 、kCFRunLoopAfterWaiting,
                    if self.activity == CFRunLoopActivity.beforeSources || self.activity == CFRunLoopActivity.afterWaiting {
                        
                        self.timeoutCount += 1
                        
                        if self.timeoutCount < 2 {
                            print("timeOutCount = (self.timeoutCount)")
                            continue
                        }
                        // 一秒左右的衡量尺度 很大可能性连续来 避免大规模打印!
                        print("检测到超过两次连续卡顿")
                    }
                }
                self.timeoutCount = 0
            }
        }
    }
} 

使用时,直接调用即可

HPBlockMonitor.share.start() 

三、使用第三方库

我们开发过程中,也可以使用一些不错的第三方库来做性能指标的监测。比如:腾讯的matrix滴滴DoraemonKit。都是很不错的框架

1.DoraaemonKit简单介绍

DoraaemonKit支持以下多种性能指标的监测:

  1. 【帧率】  App 帧率信息提供波形图查看功能,让帧率监控的趋势更加明显;
  2. 【CPU】  App CPU 使用率信息提供波形图查看功能,让 CPU 监控的趋势更加形象;
  3. 【内存】  App 内存使用量信息提供波形图查看功能,让内存监控的趋势更加鲜明;
  4. 【流量监控】  拦截 App 内部流量信息,提供波形图展示、流量概要展示、流量列表展示、流量筛选、流量详情,对流量信息统一拦截,成为我们 App 中自带的 "Charles";
  5. 【卡顿】  锁定 App 出现卡顿的时刻,打印出对应的代码调用堆栈;
  6. 【大图检测】  通过流量监测,找出所有的大小超标的图片,避免下载大图造成的流量浪费和渲染大图带来的CPU消耗。
  7. 【启动耗时】  无侵入的统计出App启动过程的总共耗时;
  8. 【UI层级检查】  检查出每一个页面中层级最深的元素;
  9. 【函数耗时】  从函数级别分析app性能瓶颈;
  10. 【Load】  找出所有的Load方法,并给出耗时分析;(iOS独有)
  11. 【内存泄漏】  找出App中所有的内存泄漏的问题。

2.DoraaemonKit支持多个平台

一套框架能够支持一个项目组的多个平台,这是一件非常合乎团队管理者心意的事情。因为这保障了整个团队输出的质量!!!

image.png

3.DoraaemonKit底层实现

DoraaemonKit的作者真的很有心,还和广大开发者们分享了其底层实现的代码

总结

本文 简单介绍了 项目 性能调试工具 Instruments 的基本使用,目前只对Allocations调试这一块 做了简单介绍 。
接下来会用几篇文章,围绕几个常见的性能问题,展开对 性能调试工具 Instrument 的 其它模块的使用介绍:

相关系列文章

Instruments