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 提供了内建的测量方法:在 Xcode
中 Edit 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加载了许多动态库,如AFNetworking
、AsyncDisplayKit
等,如下:
现在再热启动一下,输出结果如下:
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 Blending
和Offscreen Rendering
(离屏渲染)
其实这些都可以在模拟器中的Debug选项下来调试,可参考浅谈iOS中的视图优化
Alpha Blending
很多情况下,界面都是会出现多个UI控件叠加的情况,如果有透明或者半透明的控件,那么GPU
会去计算这些这些layer
最终的显示的颜色,也就是我们肉眼所看到的效果
在Core Animation
的Debug 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.如下,通过设置layer
的cornerRadius
和设置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
一起使用,来查看影响电量的因素