Instruments

Instruments

记录下学习的要点,内容来自Practical Instruments

Time Profier

Time Profiler分析原理:它按照固定的时间间隔来跟踪每一个线程的堆栈信息,通过统计比较时间间隔之间的堆栈状态,来推算某个方法执行了多久,并获得一个近似值。其实从根本上来说与我们的原始分析方法异曲同工,只不过其将各个方法消耗的时间统计起来。

这里写图片描述

一些使用的快捷键介绍:

  • Option+滚动 放大或者缩小区域
  • Option+左键 快速的展开

推荐视频

Call Tree选项:
这里写图片描述

  • Separate by Thread: 每个线程应该分开考虑。只有这样你才能揪出那些大量占用CPU的”重”线程
  • Invert Call Tree: 从上倒下跟踪堆栈,这意味着你看到的表中的方法,将已从第0帧开始取样,这通常你是想要的,只有这样你才能看到CPU中话费时间最深的方法.也就是说FuncA{FunB{FunC}} 勾选此项后堆栈以C->B-A 把调用层级最深的C显示在最外面
  • Hide System Libraries: 勾选此项你会显示你app的代码,这是非常有用的. 因为通常你只关心cpu花在自己代码上的时间不是系统上的
  • Flatten Recursion: 递归函数, 每个堆栈跟踪一个条目
  • Top Functions: 一个函数花费的时间直接在该函数中的总和,以及在函数调用该函数所花费的时间的总时间。因此,如果函数A调用B,那么A的时间报告在A花费的时间加上B花费的时间,这非常真有用,因为它可以让你每次下到调用堆栈时挑最大的时间数字,归零在你最耗时的方法。

图片加载的性能问题

可参考:

Time Profier中,可发现图片显示的解码都是在主线程的

这里写图片描述

如果有大量的图片需要解码,则会耗费大量的时间,带来不好的用户体验,所以可以把图片的解码放在后台中
如下的AsyncImageView

import UIKit

class AsyncImageView: UIView {
    private var _image: UIImage?

    var image: UIImage? {
        get {
            return _image
        }

        set {
            _image = newValue

            layer.contents = nil
            guard let image = newValue else { return }

            DispatchQueue.global(qos: .userInitiated).async {
                let decodedImage = self.decodedImage(image)
                DispatchQueue.main.async {
                    self.layer.contents = decodedImage?.cgImage
                }
            }
        }
    }

    func decodedImage(_ image: UIImage) -> UIImage? {
        guard let newImage = image.cgImage else { return nil }
        let cachedImage = AsyncImageView.globalCache.object(forKey: image)
        if let cachedImage = cachedImage as? UIImage {
            return cachedImage
        }
        let colorSpace = CGColorSpaceCreateDeviceRGB()
        let context = CGContext(data: nil, width: newImage.width, height: newImage.height, bitsPerComponent: 8, bytesPerRow: newImage.width * 4, space: colorSpace, bitmapInfo: CGImageAlphaInfo.noneSkipFirst.rawValue)

        context?.draw(newImage, in: CGRect(x: 0, y: 0, width: newImage.width, height: newImage.height))
        let drawnImage = context?.makeImage()

        if let drawnImage = drawnImage {
            let decodedImage = UIImage(cgImage: drawnImage)
            AsyncImageView.globalCache.setObject(decodedImage, forKey: image)

            return decodedImage
        }
        return nil
    }
}

extension AsyncImageView {
    struct Static {
        static var cache = NSCache<AnyObject, AnyObject>()
    }
    class var globalCache: NSCache<AnyObject, AnyObject> {
        get { return Static.cache }
        set { Static.cache = newValue }
    }

}

优化启动

两种启动

  • 冷启动(Cold Launches):当app长时间没有被启动或者device被rebooted
  • 热启动(Warm Launches):是指app需要的一些dylibs任然存在于device的disk缓存中

可参考iOS 启动时优化

main()方法运行之前,发生的事情:

  • dylib loading
  • rebase/binding
  • Obj-C Setup
  • Initializers

可参考优化 App 的启动时间的一些解释

冷启动(Cold launch)耗时才是我们需要测量的重要数据,为了准确测量冷启动耗时,测量前需要重启设备。在 main() 方法执行前测量是很难的,好在 dyld 提供了内建的测量方法:在 XcodeEdit scheme -> Run -> Auguments 将环境变量 DYLD_PRINT_STATISTICS 设为 YES,可以输出main()方法被调用之前耗费了多少时间:

这里写图片描述

控制台输出的内容大概如下,注意运行前重启测试设备:

Total pre-main time: 421.28 milliseconds (100.0%)
         dylib loading time: 252.24 milliseconds (59.8%)
        rebase/binding time:  31.76 milliseconds (7.5%)
            ObjC setup time:  25.56 milliseconds (6.0%)
           initializer time: 111.63 milliseconds (26.4%)
           slowest intializers :
             libSystem.B.dylib :   4.73 milliseconds (1.1%)
                  AFNetworking :  14.82 milliseconds (3.5%)
               AsyncDisplayKit :  77.89 milliseconds (18.4%)
                    Catstagram :  43.14 milliseconds (10.2%)

可发现大部分的时间都花费在了dylib loading。这个测试的工程项目,使用CocoaPods加载了许多动态库,如AFNetworkingAsyncDisplayKit等,如下:

这里写图片描述

现在再热启动一下,输出结果如下:

Total pre-main time: 241.62 milliseconds (100.0%)
         dylib loading time: 149.82 milliseconds (62.0%)
        rebase/binding time:   7.30 milliseconds (3.0%)
            ObjC setup time:  12.63 milliseconds (5.2%)
           initializer time:  71.44 milliseconds (29.5%)
           slowest intializers :
             libSystem.B.dylib :   2.44 milliseconds (1.0%)
                  AFNetworking :   7.52 milliseconds (3.1%)
               AsyncDisplayKit :  55.79 milliseconds (23.0%)
                    Catstagram :  24.86 milliseconds (10.2%)

现在使用静态库试一下,注释掉Podfile中的use_frameworks!pod install之后,如下:

这里写图片描述

重启设备,再运行,冷启动,输出如下:

Total pre-main time: 361.91 milliseconds (100.0%)
         dylib loading time: 222.27 milliseconds (61.4%)
        rebase/binding time:  32.18 milliseconds (8.8%)
            ObjC setup time:  11.53 milliseconds (3.1%)
           initializer time:  95.84 milliseconds (26.4%)
           slowest intializers :
             libSystem.B.dylib :   4.71 milliseconds (1.3%)
                    Catstagram : 157.16 milliseconds (43.4%)

热启动,输出入如下:

Total pre-main time: 194.98 milliseconds (100.0%)
         dylib loading time: 121.41 milliseconds (62.2%)
        rebase/binding time:   1.88 milliseconds (0.9%)
            ObjC setup time:   6.39 milliseconds (3.2%)
           initializer time:  65.22 milliseconds (33.4%)
           slowest intializers :
             libSystem.B.dylib :   2.40 milliseconds (1.2%)
                    Catstagram : 111.07 milliseconds (56.9%)

静态库和动态库的区别,参考OS静态库 【.a 和framework】【超详细】

静态库: 链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。
动态库: 链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。[ios暂时只允许使用系统动态库];
静态库和动态库是相对编译期和运行期的:静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要改静态库;而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。
总结:同一个静态库在不同程序中使用时,每一个程序中都得导入一次,打包时也被打包进去,形成一个程序。而动态库在不同程序中,打包时并没有被打包进去,只在程序运行使用时,才链接载入(如系统的框架如UIKit、Foundation等),所以程序体积会小很多,但是苹果不让使用自己的动态库,否则审核就无法通过。

main()方法之后加载过程

  • UIApplicationMain()
  • application(_:willFinishLaunchingWithOptions:)
  • didFinishLaunchingWithOptions(_:)

当第一个CATransaction is committed,system会认为加载完成

使用Instruments中的Time Profier,来检测下:

这里写图片描述

会发现启动时间达到3.11s,而willFinishLaunchingWithOptions其中的CoolLogger.reportLogs()方法,耗时1.2s之久,需要优化,如下:

        DispatchQueue.global(qos: .background).async {
            CoolLogger.reportLogs()
        }

改进之后,启动时间为106.18ms,如下:

这里写图片描述

内存相关

主要用来检测是否有循环引用、内存泄露、僵尸对象等。
可使用的工具:

  • Instruments中的Allocations、Leaks
  • Xcode的Memory Graph Debug Tool

参考iOS 性能优化:Instruments 工具的救命三招

Memory Graph Debug Tool

如下图示:

这里写图片描述

Core Animation

Core Animation可以用来优化显示

这里主要讲了2个问题,Alpha BlendingOffscreen Rendering(离屏渲染)
其实这些都可以在模拟器中的Debug选项下来调试,可参考浅谈iOS中的视图优化

Alpha Blending

很多情况下,界面都是会出现多个UI控件叠加的情况,如果有透明或者半透明的控件,那么GPU会去计算这些这些layer最终的显示的颜色,也就是我们肉眼所看到的效果

这里写图片描述

Core AnimationDebug Options中勾选Color Blended Layers,如下:

这里写图片描述

在Xcode9中,Debug Options就没有了,参考Xcode9.3之后使用Instruments 调试 Core Animation

就这个示例的项目来说,选中Color Blended Layers后,红色显示的表示就是有图层混合的问题,如下,是未优化之前的显示效果:

这里写图片描述

这里出现问题的原因是,创建label的时候,没有指定backgroundColor,其默认就是clear color,所以会有图层混合的问题。因此简单的解决办法就是为label指定backgroundColor,这里简单的指定一个white就行,跳转后,显示效果如下:

这里写图片描述

离屏渲染

关于离屏渲染不错的文章离屏渲染优化详解:实例示范+性能测试

官方公开的的资料里关于离屏渲染的信息最早是在 2011年的 WWDC, 在多个 session 里都提到了尽量避免会触发离屏渲染的效果,包括:mask, shadow, group opacity, edge antialiasing。

教程的这里例子中,使用了shadow,所以触发了离屏渲染,使用Core Animation工具,勾选Color Offscreen-Renderd Yellow,显示效果如下,黄色区域表示使用了离屏渲染:

这里写图片描述

这里使用NSShadow来代替原来view.layer上的shadow设置:

        for view in [photoDescriptionLabel, userNameLabel] {
            /*
            view.layer.shadowColor = UIColor.lightGray.cgColor
            view.layer.shadowOffset = CGSize(width: 0.0, height: 5.0)
            view.layer.shadowOpacity = 1.0
            view.layer.shadowRadius = 5.0
            */

            let shadow = NSShadow();
            shadow.shadowColor = UIColor.lightGray
            shadow.shadowOffset = CGSize(width: 0.0, height: 5.0)
            shadow.shadowBlurRadius = 5.0
            if let mutableAttributedString = view.attributedText as? NSMutableAttributedString {
                 let range = NSRange(location: 0, length: mutableAttributedString.string.characters.count)
                mutableAttributedString.addAttribute(NSShadowAttributeName, value: shadow, range: range)
            }

        }

调整后的结果如下:
这里写图片描述

圆角

在上面的例子中,还有一个问题,就是头像任然存在离屏渲染的问题,如何解决?
还是通过例子来说明:
1.如下,通过设置layercornerRadius和设置clipsToBounds来说设置圆角

class RoundView: UIImageView {
    override init(frame: CGRect) {
        super.init(frame: frame)

        self.layer.cornerRadius = 100.0
        self.clipsToBounds = true
        self.image = UIImage(named: "profPic")!
        self.contentMode = .scaleAspectFill
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

}

通过Core Animation中的Color Offscreen-Renderd Yellow来查看,结果如下,显示会有离屏渲染的问题:

这里写图片描述

2.第二方式是,自己绘制,通过贝塞尔曲线来裁剪圆角,主要是在draw(_:)中处理:

    override func draw(_ rect: CGRect) {
        let ctx = UIGraphicsGetCurrentContext()
        let bezierPath = UIBezierPath(roundedRect: rect, cornerRadius: 100)
        bezierPath.addClip()
        ctx?.addPath(bezierPath.cgPath)
        image?.draw(in: rect)
    }

此时,显示圆角周围是黑色的,如下:
这里写图片描述

设置isOpaque = false,就可以正常显示了:

这里写图片描述

但是这是又会出现另一个问题,图层混合,在Core Animation中选中选中Color Blended Layers,如下:

这里写图片描述

3.第三种方式,技巧就是在image之上渲染不透明的圆角

class RoundView: UIView {
    var image = UIImage()

    override init(frame: CGRect) {
        super.init(frame: frame)

        image = UIImage(named: "profPic")!
        contentMode = .scaleAspectFill
        roundifyCorners()
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func draw(_ rect: CGRect) {
        image.draw(in: rect)
    }

    func roundifyCorners() {
        let radius: CGFloat = 100.0
        let path = UIBezierPath(roundedRect: bounds, cornerRadius: 0)
        let circlePath = UIBezierPath(roundedRect: bounds, cornerRadius: radius)
        path.append(circlePath)

        let fillLayer = CAShapeLayer()
        fillLayer.path = path.cgPath
        fillLayer.fillRule = kCAFillRuleEvenOdd
        fillLayer.fillColor = UIColor.orange.cgColor
        layer.addSublayer(fillLayer)
    }
}

显示,效果如下,你会看到橙色pre-composited的圆角和一个绿色,non-alpha混合图像

这里写图片描述

可参考的文章:

Energy

影响电量的主要因素:

  • Networking and I/O
  • Timers
  • Location Services
  • Motion

1.在Xcode中的show the debug navigator中的Energy Impact中查看电量

这里写图片描述

2.通过Instruments中的Energy Log模板,并结合Time Profiler一起使用,来查看影响电量的因素

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值